From 7c2298df35e08f1d8af52a76252ca29ddfbbaf40 Mon Sep 17 00:00:00 2001 From: sBubshait Date: Tue, 29 Jul 2025 09:22:40 +0300 Subject: [PATCH 1/3] feat: implement /updateUser endpoint in backend --- .../wesal/controller/AuthController.java | 29 ++++++++++++++++--- .../wesal/wesal/dto/UpdateUserRequest.java | 10 +++++++ .../wesal/wesal/service/UserService.java | 22 ++++++++++++-- 3 files changed, 55 insertions(+), 6 deletions(-) diff --git a/backend/src/main/java/online/wesal/wesal/controller/AuthController.java b/backend/src/main/java/online/wesal/wesal/controller/AuthController.java index 2479b64..3da8b40 100644 --- a/backend/src/main/java/online/wesal/wesal/controller/AuthController.java +++ b/backend/src/main/java/online/wesal/wesal/controller/AuthController.java @@ -69,18 +69,39 @@ public class AuthController { } @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> updateUser(@Valid @RequestBody UpdateUserRequest request) { try { User user = userService.updateUser( request.getFcmToken(), request.getDisplayName(), request.getAvatar(), - request.getPassword() + request.getPassword(), + request.getUsername() ); - return ResponseEntity.ok(Map.of("status", 200, "user", user)); + + // Create user data without password + Map 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) { - return ResponseEntity.badRequest().body(Map.of("status", 400, "message", e.getMessage())); + return ResponseEntity.ok(Map.of( + "status", false, + "message", e.getMessage() + )); } } diff --git a/backend/src/main/java/online/wesal/wesal/dto/UpdateUserRequest.java b/backend/src/main/java/online/wesal/wesal/dto/UpdateUserRequest.java index 7556c3a..942e892 100644 --- a/backend/src/main/java/online/wesal/wesal/dto/UpdateUserRequest.java +++ b/backend/src/main/java/online/wesal/wesal/dto/UpdateUserRequest.java @@ -10,6 +10,8 @@ public class UpdateUserRequest { private String avatar; + private String username; + @Size(min = 6, message = "Password must be at least 8 characters long") private String password; @@ -46,4 +48,12 @@ public class UpdateUserRequest { public void setPassword(String password) { this.password = password; } + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } } \ No newline at end of file diff --git a/backend/src/main/java/online/wesal/wesal/service/UserService.java b/backend/src/main/java/online/wesal/wesal/service/UserService.java index 9dd0f91..244f340 100644 --- a/backend/src/main/java/online/wesal/wesal/service/UserService.java +++ b/backend/src/main/java/online/wesal/wesal/service/UserService.java @@ -56,8 +56,9 @@ public class UserService { 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(); + boolean wasNotActivated = !user.isActivated(); if (fcmToken != null && !fcmToken.trim().isEmpty()) { user.setFcmToken(fcmToken); @@ -75,6 +76,23 @@ public class UserService { 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; } } \ No newline at end of file From 481615260c3b2757fe5575d9c5df92a13484b2d1 Mon Sep 17 00:00:00 2001 From: sBubshait Date: Sun, 3 Aug 2025 10:06:34 +0300 Subject: [PATCH 2/3] feat: Editing Profiles --- frontend/lib/main.dart | 58 ++- frontend/lib/screens/edit_profile_screen.dart | 428 ++++++++++++++++++ .../notification_permission_screen.dart | 285 ++++++------ frontend/lib/screens/pages/profile_page.dart | 199 ++++---- .../lib/services/invitations_service.dart | 85 +++- frontend/lib/services/user_service.dart | 55 +++ 6 files changed, 823 insertions(+), 287 deletions(-) create mode 100644 frontend/lib/screens/edit_profile_screen.dart diff --git a/frontend/lib/main.dart b/frontend/lib/main.dart index 43ff9ec..f28a2ee 100644 --- a/frontend/lib/main.dart +++ b/frontend/lib/main.dart @@ -3,7 +3,7 @@ import 'package:firebase_core/firebase_core.dart'; import 'firebase_options.dart'; import 'screens/home_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/user_service.dart'; import 'services/app_lifecycle_service.dart'; @@ -53,11 +53,25 @@ class _SplashScreenState extends State { if (isLoggedIn) { final userResult = await UserService.getCurrentUser(); if (userResult['success'] == true) { - // Start polling services now that user is logged in and going to main screen - AppLifecycleService.startAllPolling(); - Navigator.of(context).pushReplacement( - MaterialPageRoute(builder: (context) => HomeScreen()), - ); + final userData = userResult['data']; + + // Check if user needs onboarding (activated = 0) + 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 { await AuthService.logout(); Navigator.of(context).pushReplacement( @@ -344,14 +358,32 @@ class _SignInPageState extends State { if (result['success'] == true) { final userResult = await UserService.getCurrentUser(forceRefresh: true); - // Start polling services now that user is logged in - AppLifecycleService.startAllPolling(); + if (userResult['success'] == true) { + final userData = userResult['data']; + + // Check if user needs onboarding (activated = 0) + 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 + AppLifecycleService.startAllPolling(); - Navigator.of(context).pushReplacement( - MaterialPageRoute( - builder: (context) => NotificationPermissionScreen(), - ), - ); + Navigator.of(context).pushReplacement( + MaterialPageRoute( + builder: (context) => NotificationPermissionScreen(), + ), + ); + } + } else { + _showErrorAlert('Failed to load user data'); + } } else { _showErrorAlert(result['message'] ?? 'Login failed'); } diff --git a/frontend/lib/screens/edit_profile_screen.dart b/frontend/lib/screens/edit_profile_screen.dart new file mode 100644 index 0000000..c86a945 --- /dev/null +++ b/frontend/lib/screens/edit_profile_screen.dart @@ -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? userData; + final bool isOnboarding; + + const EditProfileScreen({ + Key? key, + this.userData, + this.isOnboarding = false, + }) : super(key: key); + + @override + _EditProfileScreenState createState() => _EditProfileScreenState(); +} + +class _EditProfileScreenState extends State { + final _formKey = GlobalKey(); + 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 _saveProfile() async { + if (!_formKey.currentState!.validate()) { + return; + } + + setState(() { + _isLoading = true; + }); + + try { + final Map 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(0xFF6A4C93)), + ), + ) + : Text( + widget.isOnboarding ? 'Complete Setup' : 'Save Changes', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + ), + ), + ), + ), + ], + ), + ), + ), + ), + ); + } +} \ No newline at end of file diff --git a/frontend/lib/screens/notification_permission_screen.dart b/frontend/lib/screens/notification_permission_screen.dart index 3f0cffc..5a54bc9 100644 --- a/frontend/lib/screens/notification_permission_screen.dart +++ b/frontend/lib/screens/notification_permission_screen.dart @@ -80,165 +80,166 @@ class _NotificationPermissionScreenState @override Widget build(BuildContext context) { return PopScope( - canPop: false, // Prevent back navigation from notification permission screen + canPop: + false, // Prevent back navigation from notification permission screen child: Scaffold( - body: Container( - decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - colors: [Color(0xFF32B0A5), Color(0xFF4600B9)], - stops: [0.0, 0.5], + body: Container( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [Color(0xFF32B0A5), Color(0xFF4600B9)], + stops: [0.0, 0.5], + ), ), - ), - child: SafeArea( - child: Padding( - padding: EdgeInsets.all(24), - child: Column( - children: [ - Expanded( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, + child: SafeArea( + child: Padding( + padding: EdgeInsets.all(24), + child: Column( + children: [ + Expanded( + child: Column( + 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: [ 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, + 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(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), - 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), + TextButton( + onPressed: _isLoading ? null : _skipNotifications, + child: Text( + 'Skip for now', + style: TextStyle( + color: Colors.white.withOpacity(0.8), + fontSize: 16, + decoration: TextDecoration.underline, ), ), - 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(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, - ), - ), - ), - ], - ), - ], + ], + ), ), ), ), - ), ), ); } diff --git a/frontend/lib/screens/pages/profile_page.dart b/frontend/lib/screens/pages/profile_page.dart index 12c0dbb..d5e5116 100644 --- a/frontend/lib/screens/pages/profile_page.dart +++ b/frontend/lib/screens/pages/profile_page.dart @@ -9,6 +9,9 @@ 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'; class ProfilePage extends StatefulWidget { @override @@ -168,6 +171,19 @@ class _ProfilePageState extends State { ).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( @@ -185,6 +201,12 @@ class _ProfilePageState extends State { foregroundColor: Color(0xFF6A4C93), elevation: 0, actions: [ + if (userData != null) + IconButton( + onPressed: _navigateToEditProfile, + icon: Icon(Icons.edit), + tooltip: 'Edit Profile', + ), IconButton( onPressed: _navigateToSettings, icon: Icon(Icons.settings), @@ -338,10 +360,6 @@ class SettingsPage extends StatefulWidget { } class _SettingsPageState extends State { - String? fcmToken; - final TextEditingController _tokenController = TextEditingController(); - bool isLoading = false; - // Admin user creation Map? userData; bool isLoadingUser = true; @@ -353,52 +371,18 @@ class _SettingsPageState extends State { @override void initState() { super.initState(); - _loadFCMToken(); _loadUserData(); _generatePassword(); } @override void dispose() { - _tokenController.dispose(); _emailController.dispose(); _passwordController.dispose(); _displayNameController.dispose(); super.dispose(); } - Future _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 _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 _loadUserData() async { setState(() { @@ -715,17 +699,18 @@ class _SettingsPageState extends State { Container( width: double.infinity, + height: 56, child: ElevatedButton.icon( onPressed: () => _showCreateAccountDialog(context), - icon: Icon(Icons.person_add, size: 18), + icon: Icon(Icons.person_add, size: 20), label: Text('Create an Account'), style: ElevatedButton.styleFrom( backgroundColor: Color(0xFF6A4C93), foregroundColor: Colors.white, 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 { ], Text( - 'Development Tools', + 'App Information', style: TextStyle( fontSize: 20, fontWeight: FontWeight.w600, @@ -743,92 +728,88 @@ class _SettingsPageState extends State { ), SizedBox(height: 16), - Text( - 'FCM Token:', - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.w500, - color: Colors.black87, + 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: 8), - if (isLoading) - Center( - child: CircularProgressIndicator( - valueColor: AlwaysStoppedAnimation(Color(0xFF6A4C93)), + SizedBox(height: 16), + + Container( + width: double.infinity, + height: 56, + child: ElevatedButton.icon( + onPressed: () => Navigator.of(context).push( + MaterialPageRoute(builder: (context) => InformationScreen()), ), - ) - else - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Container( - decoration: BoxDecoration( - 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...', - ), - ), + 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), ), - SizedBox(height: 8), - - 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), - ), - ), - ), - ], - ), - ], + padding: EdgeInsets.symmetric(horizontal: 16), + ), ), + ), SizedBox(height: 32), 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('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( 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)), style: ElevatedButton.styleFrom( backgroundColor: Colors.white, side: BorderSide(color: Colors.red), shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), + borderRadius: BorderRadius.circular(12), ), - padding: EdgeInsets.symmetric(vertical: 16), + padding: EdgeInsets.symmetric(horizontal: 16), ), ), ), diff --git a/frontend/lib/services/invitations_service.dart b/frontend/lib/services/invitations_service.dart index 8a0e0b7..648be1b 100644 --- a/frontend/lib/services/invitations_service.dart +++ b/frontend/lib/services/invitations_service.dart @@ -7,21 +7,32 @@ import 'cache_service.dart'; class InvitationsService { static bool _pollingStarted = false; + /// Get all invitations with caching - returns cached data if available and fresh - static Future> getAllInvitations({bool forceRefresh = false}) async { + static Future> getAllInvitations({ + bool forceRefresh = false, + }) async { // Return cached data if available and not forcing refresh if (!forceRefresh) { - final cached = CacheService.getCached(CacheService.invitationsAllKey); - if (cached != null && !CacheService.isCacheExpired(CacheService.invitationsAllKey, CacheService.invitationsCacheDuration)) { + final cached = CacheService.getCached( + CacheService.invitationsAllKey, + ); + if (cached != null && + !CacheService.isCacheExpired( + CacheService.invitationsAllKey, + CacheService.invitationsCacheDuration, + )) { return { 'success': true, 'message': 'Loaded from cache', 'invitations': cached, }; } - + // Return stale data while fetching fresh data in background - final stale = CacheService.getCachedStale(CacheService.invitationsAllKey); + final stale = CacheService.getCachedStale( + CacheService.invitationsAllKey, + ); if (stale != null && !forceRefresh) { // Trigger background refresh _fetchAndCacheInvitations(); @@ -37,7 +48,9 @@ class InvitationsService { return await _fetchAndCacheInvitations(); } - static Future> _fetchAndCacheInvitations([bool fromPolling = false]) async { + static Future> _fetchAndCacheInvitations([ + bool fromPolling = false, + ]) async { try { final response = await HttpService.get( ApiConstants.getAllInvitationsEndpoint, @@ -49,22 +62,30 @@ class InvitationsService { if (invitationsResponse.status) { final invitationsData = invitationsResponse.data; - + // Only cache when not called from polling to prevent conflicts if (!fromPolling) { - CacheService.setCached(CacheService.invitationsAllKey, invitationsData); + CacheService.setCached( + CacheService.invitationsAllKey, + invitationsData, + ); } return { - 'success': true, + 'success': true, 'invitations': invitationsData, 'message': invitationsResponse.message ?? '', }; } else { return { 'success': false, - 'message': invitationsResponse.message ?? 'Failed to fetch invitations', - 'invitations': InvitationsData(created: [], accepted: [], available: []), + 'message': + invitationsResponse.message ?? 'Failed to fetch invitations', + 'invitations': InvitationsData( + created: [], + accepted: [], + available: [], + ), }; } } else if (response.statusCode == 401 || response.statusCode == 403) { @@ -72,13 +93,21 @@ class InvitationsService { return { 'success': false, 'message': 'Session expired. Please login again.', - 'invitations': InvitationsData(created: [], accepted: [], available: []), + 'invitations': InvitationsData( + created: [], + accepted: [], + available: [], + ), }; } else { return { 'success': false, 'message': 'Server error (${response.statusCode})', - 'invitations': InvitationsData(created: [], accepted: [], available: []), + 'invitations': InvitationsData( + created: [], + accepted: [], + available: [], + ), }; } } catch (e) { @@ -86,7 +115,11 @@ class InvitationsService { return { 'success': false, '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 static Stream getInvitationsStream() { - return CacheService.getStream(CacheService.invitationsAllKey); + return CacheService.getStream( + CacheService.invitationsAllKey, + ); } /// Check if polling is active @@ -161,7 +196,9 @@ class InvitationsService { } } - static Future> createInvitation(Map invitationData) async { + static Future> createInvitation( + Map invitationData, + ) async { try { final response = await HttpService.post( ApiConstants.createInvitationEndpoint, @@ -209,7 +246,9 @@ class InvitationsService { } } - static Future> getInvitationDetails(int invitationId) async { + static Future> getInvitationDetails( + int invitationId, + ) async { try { final response = await HttpService.get( '${ApiConstants.invitationsEndpoint}/get?id=$invitationId', @@ -218,15 +257,15 @@ class InvitationsService { if (response.statusCode == 200) { final responseData = jsonDecode(response.body); if (responseData['status'] == true) { - final invitationDetails = InvitationDetails.fromJson(responseData['data']); - return { - 'success': true, - 'data': invitationDetails, - }; + final invitationDetails = InvitationDetails.fromJson( + responseData['data'], + ); + return {'success': true, 'data': invitationDetails}; } else { return { 'success': false, - 'message': responseData['message'] ?? 'Failed to get invitation details', + 'message': + responseData['message'] ?? 'Failed to get invitation details', }; } } else if (response.statusCode == 401) { diff --git a/frontend/lib/services/user_service.dart b/frontend/lib/services/user_service.dart index b97927a..d93cc9d 100644 --- a/frontend/lib/services/user_service.dart +++ b/frontend/lib/services/user_service.dart @@ -165,6 +165,61 @@ class UserService { } } + static Future> updateUser({ + String? displayName, + String? username, + String? avatar, + }) async { + try { + final Map 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> createUser({ required String email, required String password, From 45228099bd876a82c06eeed0d29c7622439e3941 Mon Sep 17 00:00:00 2001 From: sBubshait Date: Sun, 3 Aug 2025 10:06:53 +0300 Subject: [PATCH 3/3] feat: Settings page add Information and Help pages --- frontend/app_info.txt | 11 ++ frontend/assets/app_info.txt | 16 ++ frontend/lib/screens/information_screen.dart | 181 +++++++++++++++++++ frontend/lib/screens/support_screen.dart | 163 +++++++++++++++++ frontend/pubspec.yaml | 2 + 5 files changed, 373 insertions(+) create mode 100644 frontend/app_info.txt create mode 100644 frontend/assets/app_info.txt create mode 100644 frontend/lib/screens/information_screen.dart create mode 100644 frontend/lib/screens/support_screen.dart diff --git a/frontend/app_info.txt b/frontend/app_info.txt new file mode 100644 index 0000000..89f5596 --- /dev/null +++ b/frontend/app_info.txt @@ -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! \ No newline at end of file diff --git a/frontend/assets/app_info.txt b/frontend/assets/app_info.txt new file mode 100644 index 0000000..6a0710c --- /dev/null +++ b/frontend/assets/app_info.txt @@ -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! \ No newline at end of file diff --git a/frontend/lib/screens/information_screen.dart b/frontend/lib/screens/information_screen.dart new file mode 100644 index 0000000..abb583b --- /dev/null +++ b/frontend/lib/screens/information_screen.dart @@ -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 { + String appInfo = ''; + bool isLoading = true; + + @override + void initState() { + super.initState(); + _loadAppInfo(); + } + + Future _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( + 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, + ), + ), + ), + ), + ], + ), + ), + ), + ), + ); + } +} diff --git a/frontend/lib/screens/support_screen.dart b/frontend/lib/screens/support_screen.dart new file mode 100644 index 0000000..9df5818 --- /dev/null +++ b/frontend/lib/screens/support_screen.dart @@ -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, + ), + ), + ), + ), + ], + ), + ), + ), + ), + ); + } +} \ No newline at end of file diff --git a/frontend/pubspec.yaml b/frontend/pubspec.yaml index b2c33a1..63ab27a 100644 --- a/frontend/pubspec.yaml +++ b/frontend/pubspec.yaml @@ -25,6 +25,8 @@ dev_dependencies: flutter: uses-material-design: true + assets: + - assets/app_info.txt fonts: - family: Blaka fonts: