From c75ae680b027341a7a978ab35310ed3d2686b1bd Mon Sep 17 00:00:00 2001 From: venbaittech Date: Thu, 12 Dec 2024 13:51:27 +0530 Subject: [PATCH] all functonality added in new router flow --- .run/main.dart.run.xml | 6 + lib/config/my_router.dart | 26 +- .../auth_verification/changepassword.dart | 28 +- .../auth_verification/registration.dart | 3 +- lib/presentation/Screens/profilepage.dart | 172 +++- .../routes/auth_routes/login_route.dart | 246 ++--- .../Drawer Items/edit_profile.dart | 837 ++++++++++++++++++ .../drawer_routes/Drawer Items/feedback.dart | 307 ++++--- .../Drawer Items/manage_users.dart | 308 ++++--- .../drawer_routes/custom_drawer_routes.dart | 34 +- 10 files changed, 1528 insertions(+), 439 deletions(-) create mode 100644 .run/main.dart.run.xml create mode 100644 lib/presentation/routes/drawer_routes/Drawer Items/edit_profile.dart diff --git a/.run/main.dart.run.xml b/.run/main.dart.run.xml new file mode 100644 index 00000000..4767aff8 --- /dev/null +++ b/.run/main.dart.run.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/lib/config/my_router.dart b/lib/config/my_router.dart index 62b87e6f..806ed3e4 100644 --- a/lib/config/my_router.dart +++ b/lib/config/my_router.dart @@ -303,6 +303,7 @@ import '../presentation/Screens/auth_verification/otp_verification.dart'; import '../presentation/Screens/profilepage.dart'; import '../presentation/routes/auth_routes/login_route.dart'; import '../presentation/routes/bottom_bar_routes/tab_routes/home_route.dart'; +import '../presentation/routes/drawer_routes/Drawer Items/edit_profile.dart'; import '../presentation/routes/drawer_routes/Drawer Items/feedback.dart'; import '../presentation/routes/drawer_routes/Drawer Items/manage_users.dart'; @@ -311,7 +312,7 @@ final GoRouter router = GoRouter( GoRoute( path: '/', //builder: (context, state) => LoginRoute(), - builder: (context, state) => MyHomePage(), + builder: (context, state) => LoginRoute(), ), GoRoute( path: '/myhomepage', @@ -328,10 +329,11 @@ final GoRouter router = GoRouter( ), ), GoRoute( - path: '/changepassword', - builder: (context, state) => Changepassword( - userId: '', - ), + path: '/changepassword/:userId', + builder: (context, state) { + final userId = state.pathParameters['userId']!; + return Changepassword(userId: userId); + }, ), GoRoute( path: '/confirmpasswd', @@ -347,10 +349,11 @@ final GoRouter router = GoRouter( builder: (context, state) => FeedbackForm(), ), GoRoute( - path: '/profile', - builder: (context, state) => ProfileScreen( - userId: '', - ), + path: '/profile/:userId', + builder: (context, state) { + final userId = state.pathParameters['userId']!; + return ProfileScreen(userId: userId); + }, ), // GoRoute( // path: '/manageuser', @@ -360,10 +363,13 @@ final GoRouter router = GoRouter( // return ManageUserRouter(title: title); // }, // ), + GoRoute( + path: '/editProfile', + builder: (context, state) => EditProfile(), + ), GoRoute( path: '/manageuser', builder: (context, state) => ManageUserRouter(), ), ], ); - diff --git a/lib/presentation/Screens/auth_verification/changepassword.dart b/lib/presentation/Screens/auth_verification/changepassword.dart index 702da9a2..9b86c2e1 100644 --- a/lib/presentation/Screens/auth_verification/changepassword.dart +++ b/lib/presentation/Screens/auth_verification/changepassword.dart @@ -58,7 +58,7 @@ class _ResetPasswordScreenState extends State { //print('userDetails ->: $userData'); if (userData['items'].isNotEmpty) { // Email exists; retrieve user ID - final userId = userData['items'][0]['id']; + // final userId = userData['items'][0]['id']; // Proceed with OTP request final otpResponse = await http.post( @@ -87,17 +87,15 @@ class _ResetPasswordScreenState extends State { ); // Navigate to the Email Verification screen - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => EmailVerificationScreen( - email: email, - userId: widget.userId, // Pass user ID to the next screen - otp: otp, - otpId: otpId, - sendVerificationCode: sendVerificationCode, - ), - ), + context.go( + '/mailverification', + extra: { + 'email': email, + 'userId': widget.userId, // Pass user ID to the next screen + 'otp': otp, + 'otpId': otpId, + 'sendVerificationCode': sendVerificationCode, + }, ); } else { final error = @@ -132,6 +130,12 @@ class _ResetPasswordScreenState extends State { } } + @override + void initState() { + super.initState(); + print(widget.userId); + } + @override void dispose() { _emailController.dispose(); diff --git a/lib/presentation/Screens/auth_verification/registration.dart b/lib/presentation/Screens/auth_verification/registration.dart index 66689b16..97204028 100644 --- a/lib/presentation/Screens/auth_verification/registration.dart +++ b/lib/presentation/Screens/auth_verification/registration.dart @@ -6,7 +6,6 @@ import 'package:pocketbase/pocketbase.dart'; import 'package:uae_stat/domain/use_cases/language.dart'; import 'package:uae_stat/presentation/Screens/profilepage.dart'; -import '../../../infrastructure/services/pocketbase_service.dart'; import '../../routes/auth_routes/login_route.dart'; class RegisterScreen extends StatefulWidget { @@ -194,7 +193,7 @@ class _RegisterScreenState extends State { 'email': _emailController.text, 'password': _passwordController.text, 'passwordConfirm': _passwordController.text, - // 'verified': true, + 'status': 'Pending', }, headers: { 'Authorization': adminToken }); diff --git a/lib/presentation/Screens/profilepage.dart b/lib/presentation/Screens/profilepage.dart index b5eae4ff..76b9622b 100644 --- a/lib/presentation/Screens/profilepage.dart +++ b/lib/presentation/Screens/profilepage.dart @@ -9,8 +9,12 @@ import 'package:http/http.dart' as http; import 'package:image_picker/image_picker.dart'; import 'package:intl/intl.dart'; import 'package:pocketbase/pocketbase.dart'; +import 'package:uae_stat/domain/use_cases/language.dart'; +import 'package:uae_stat/presentation/routes/bottom_bar_routes/tab_routes/home_route.dart'; +import '../components/my_bottom_nav_bar.dart'; import 'auth_verification/changepassword.dart'; +import 'demo_home.dart'; class ProfileScreen extends StatefulWidget { final String userId; // Add this field to hold the user ID @@ -23,6 +27,25 @@ class ProfileScreen extends StatefulWidget { class _ProfileScreenState extends State { final _pb = PocketBase('https://pb.venbait.in'); // final _pb = PocketBase('http://127.0.0.1:8090'); + + // Add focus nodes and hint states + final List _focusNodes = List.generate(4, (_) => FocusNode()); + final List _showHints = [ + true, + true, + true, + true + ]; // One for each TextField + + String _getControllerText(int index) { + if (index == 0) return _usernameController.text; + if (index == 1) return _emailController.text; + if (index == 2) return _fullNameController.text; + if (index == 3) return _dateController.text; + + return ''; + } + final _formKey = GlobalKey(); final TextEditingController _fullNameController = TextEditingController(); final TextEditingController _dateController = TextEditingController(); @@ -52,12 +75,58 @@ class _ProfileScreenState extends State { @override void initState() { super.initState(); - print(widget.userId); + // print(widget.userId); + + // Add listeners for focus nodes + for (int i = 0; i < _focusNodes.length; i++) { + _focusNodes[i].addListener(() { + setState(() { + // Hide hint when focused and text is not empty + _showHints[i] = + !_focusNodes[i].hasFocus && (_getControllerText(i).isEmpty); + }); + }); + + // Listen to text changes + if (i == 0) { + _usernameController.addListener(() { + setState(() { + _showHints[i] = + !_focusNodes[i].hasFocus && (_getControllerText(i).isEmpty); + }); + }); + } else if (i == 1) { + _emailController.addListener(() { + setState(() { + _showHints[i] = + !_focusNodes[i].hasFocus && (_getControllerText(i).isEmpty); + }); + }); + } else if (i == 1) { + _fullNameController.addListener(() { + setState(() { + _showHints[i] = + !_focusNodes[i].hasFocus && (_getControllerText(i).isEmpty); + }); + }); + } else if (i == 2) { + _dateController.addListener(() { + setState(() { + _showHints[i] = + !_focusNodes[i].hasFocus && (_getControllerText(i).isEmpty); + }); + }); + } + } + _fetchUserData(); // Call the function to fetch user data } @override void dispose() { + for (var focusNode in _focusNodes) { + focusNode.dispose(); + } _fullNameController.dispose(); _dateController.dispose(); super.dispose(); @@ -68,6 +137,7 @@ class _ProfileScreenState extends State { final adminAuth = await _pb.admins .authWithPassword('pb@venbainfotech.com', 'pb@venbainfotech.com'); final adminToken = adminAuth.token; + print('adminToken- ${adminToken}'); final userDetailsResponse = await _pb.collection('users').getOne( widget.userId, headers: { @@ -125,7 +195,7 @@ class _ProfileScreenState extends State { // Function to validate the DOB field String? _validateDob(String? value) { if (value == null || value.isEmpty) { - return 'Please select your date of birth'; + return 'Required'; } final DateTime selectedDate = _selectedDate!; @@ -140,7 +210,7 @@ class _ProfileScreenState extends State { String? _validateDropdown(String? value) { if (value == null || value.isEmpty) { - return 'Please select an option'; + return 'Required'; } return null; } @@ -201,6 +271,7 @@ class _ProfileScreenState extends State { _resetFormFields(); ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text("Profile updated successfully!"))); + context.go('/myhomepage'); } catch (error) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text("Failed to update profile: $error"))); @@ -299,8 +370,9 @@ class _ProfileScreenState extends State { SizedBox(height: 10), TextFormField( controller: _usernameController, + focusNode: _focusNodes[0], decoration: InputDecoration( - hintText: 'Mohammad Hassan', + hintText: _showHints[0] ? 'Mohammad Hassan' : null, hintStyle: TextStyle(color: Colors.grey), border: OutlineInputBorder( borderRadius: BorderRadius.circular(8), @@ -322,8 +394,10 @@ class _ProfileScreenState extends State { SizedBox(height: 10), TextFormField( controller: _emailController, + focusNode: _focusNodes[1], decoration: InputDecoration( - hintText: 'mohammad.hassan@fcsc.gov.ae', + hintText: + _showHints[1] ? 'mohammad.hassan@fcsc.gov.ae' : null, hintStyle: TextStyle(color: Colors.grey), border: OutlineInputBorder( borderRadius: BorderRadius.circular(8), @@ -349,15 +423,19 @@ class _ProfileScreenState extends State { if (value == null || value.isEmpty) { return 'Required'; } - final nameRegex = RegExp(r"^[a-zA-Z\s]+$"); + + //RegExp(r"^[a-zA-Z\s]+$"); + final nameRegex = + RegExp(r"^[a-zA-Z0-9'\-\u0627\u0639~\u00C0-\u00FF]+$"); if (!nameRegex.hasMatch(value)) { return 'Invalid Characters'; } return null; }, controller: _fullNameController, + focusNode: _focusNodes[2], decoration: InputDecoration( - hintText: "Mohammad", + hintText: _showHints[2] ? 'Enter the Full Name' : null, hintStyle: TextStyle(color: Colors.grey), border: OutlineInputBorder( borderRadius: BorderRadius.circular(8), @@ -381,11 +459,14 @@ class _ProfileScreenState extends State { SizedBox(height: 10), TextFormField( controller: _dateController, + focusNode: _focusNodes[3], decoration: InputDecoration( border: OutlineInputBorder( borderRadius: BorderRadius.circular(8), ), - hintText: 'Select your Date of Birth', + hintText: + _showHints[3] ? 'Select your Date of Birth' : null, + //hintText: 'Select your Date of Birth', suffixIcon: const Icon(Icons.arrow_drop_down_sharp), ), readOnly: true, @@ -503,7 +584,7 @@ class _ProfileScreenState extends State { Padding( padding: const EdgeInsets.only(left: 10.0), child: Text( - 'Required', + 'Please agree to terms and conditions', style: TextStyle( color: Colors.red[700], fontSize: 12, @@ -516,15 +597,13 @@ class _ProfileScreenState extends State { Center( child: GestureDetector( onTap: () { - context.go('/confirmpasswd'); + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => + Changepassword(userId: widget.userId)), + ); }, - // { - // Navigator.push( - // context, - // MaterialPageRoute( - // builder: (context) => Changepassword(userId: '',)), - // ); - // }, child: Text( "Change Password", style: TextStyle( @@ -536,38 +615,35 @@ class _ProfileScreenState extends State { SizedBox(height: 20), ElevatedButton.icon( onPressed: () { - context.go('/myhomepage'); + if ((_formKey.currentState?.validate() ?? false) && + (isChecked)) { + _formKey.currentState?.save(); + showConfirmationDialog(context); + } else { + setState(() { + showError = + !isChecked; // Show error if the checkbox is not checked + }); + } + // if ((_formKey.currentState?.validate() ?? false) && (isChecked)) { + // _formKey.currentState?.save(); + // showConfirmationDialog(context); + // // if (isChecked) { + // // _formKey.currentState?.save(); + // // // _confirmSaveProfile(); + // // showConfirmationDialog(context); + // // } + // else { + // setState(() { + // showError = !isChecked; // Show error if the checkbox is not checked + // }); + + // ScaffoldMessenger.of(context).showSnackBar( + // SnackBar(content: Text('Please accept the terms and conditions.',style: TextStyle(color: Colors.white),)), + // ); + //} + //} }, - // { - // if ((_formKey.currentState?.validate() ?? false) && - // (isChecked)) { - // _formKey.currentState?.save(); - // showConfirmationDialog(context); - // } else { - // setState(() { - // showError = - // !isChecked; // Show error if the checkbox is not checked - // }); - // } - // // if ((_formKey.currentState?.validate() ?? false) && (isChecked)) { - // // _formKey.currentState?.save(); - // // showConfirmationDialog(context); - // // // if (isChecked) { - // // // _formKey.currentState?.save(); - // // // // _confirmSaveProfile(); - // // // showConfirmationDialog(context); - // // // } - // // else { - // // setState(() { - // // showError = !isChecked; // Show error if the checkbox is not checked - // // }); - // - // // ScaffoldMessenger.of(context).showSnackBar( - // // SnackBar(content: Text('Please accept the terms and conditions.',style: TextStyle(color: Colors.white),)), - // // ); - // //} - // //} - // }, icon: Icon( Icons.save, color: Colors.white, diff --git a/lib/presentation/routes/auth_routes/login_route.dart b/lib/presentation/routes/auth_routes/login_route.dart index ecbf1200..a0ea8df4 100644 --- a/lib/presentation/routes/auth_routes/login_route.dart +++ b/lib/presentation/routes/auth_routes/login_route.dart @@ -3,6 +3,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:go_router/go_router.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:shared_preferences/shared_preferences.dart'; import 'package:the_validator/the_validator.dart'; import 'package:uae_stat/config/my_theme.dart'; import 'package:uae_stat/domain/use_cases/auth_use_case.dart'; @@ -17,7 +18,6 @@ import 'package:uae_stat/presentation/components/space.dart'; import 'package:uae_stat/presentation/components/themed_text_field.dart'; import 'package:uae_stat/presentation/routes/bottom_bar_routes/tab_routes/home_route.dart'; -import '../../Screens/profilepage.dart'; import '../../Screens/auth_verification/registration.dart'; import 'package:pocketbase/pocketbase.dart'; @@ -25,6 +25,7 @@ class LoginRoute extends HookConsumerWidget { final pb = PocketBase('https://pb.venbait.in'); // final _pb = PocketBase('http://127.0.0.1:8090'); LoginRoute({super.key}); + dynamic userData; static final formKey = GlobalKey(); @@ -45,21 +46,38 @@ class LoginRoute extends HookConsumerWidget { 'Authorization': 'Bearer $adminToken', }, ); + print('userDetailsLogin: $userDetailsResponse'); - // Check if 'is_profile_completed' is true or false - if (userDetailsResponse != null && - userDetailsResponse.data['is_profile_completed'] != null) { - return userDetailsResponse.data['is_profile_completed']; - } else { - return false; // Default to false if the field is missing or response is null + // Check if user is verified + final bool? isVerified = userDetailsResponse.data['verified']; + final bool? isUserMailVerified = + userDetailsResponse.data['user_mail_verify']; + + // Check admin and email verification statuses + if (isVerified == false) { + throw Exception('Admin not approved'); } + if (isUserMailVerified == false) { + throw Exception('Email not verified'); + } + userData = userDetailsResponse; + // Check if 'is_profile_completed' is true + final bool isProfileCompleted = + userDetailsResponse.data['is_profile_completed'] ?? false; + + return isProfileCompleted; } catch (e) { print('Error fetching user details: $e'); - return false; // Return false on error + rethrow; } } + Future saveUserId(String userId) async { + final prefs = await SharedPreferences.getInstance(); + await prefs.setString('userId', userId); // Save userId locally + } + @override Widget build(BuildContext context, WidgetRef ref) { final emailCtl = useTextEditingController(); @@ -157,105 +175,121 @@ class LoginRoute extends HookConsumerWidget { final loginBtn = SizedBox( width: double.infinity, child: ElevatedButton( - onPressed: () { - context.go('/profile'); + onPressed: () async { + final isValid = formKey.currentState!.validate(); + if (!isValid) return; + final session = await context.loaderWithErrorDialog( + () => ref + .read( + authUseCaseProvider.notifier, + ) + .login( + emailCtl.text, + pwCtl.text, + ), + errorDialogBuilder: ( + error, [ + StackTrace? stackTrace, + ]) { + if (error == LoginError.invalidEmailPw) { + return context.simpleDialog( + title: context.translate( + 'Incorrect credentials', + 'أوراق غير صحيحة', + ), + content: context.translate( + 'Your email or password is invalid. Please try again.', + 'بريدك الإلكتروني أو كلمة المرور غير صالحة. حاول مرة اخرى.', + ), + ); + } + // if (error == LoginError.emailAddressNotVerified) { + // return context.simpleDialog( + // title: context.translate( + // 'Verification Error', + // 'خطأ التحقق', + // ), + // content: context.translate( + // '${emailCtl.text} is not a verified email address. Please check your email for a verification link.', + // '${emailCtl.text} ليس عنوان بريد إلكتروني تم التحقق منه. يرجى التحقق من بريدك الإلكتروني للحصول على رابط التحقق.', + // ), + // extraAction: ElevatedButton( + // onPressed: () async { + // Navigator.of( + // context, + // rootNavigator: true, + // ).pop(); + // await context.loaderWithErrorDialog( + // () => ref + // .read(authUseCaseProvider.notifier) + // .requestVerificationEmail(emailCtl.text), + // ); + // if (!context.mounted) return; + // context.simpleDialog( + // title: 'Email Re-sent', + // content: + // 'We\'ve sent you the verification email at ${emailCtl.text} again.', + // ); + // }, + // child: Text( + // context.translate( + // 'I did not receive an email', + // 'لم أتلق بريدًا إلكترونيًا', + // ), + // ), + // ), + // ); + // } + return context.simpleDialog(); + }, + ); + + if (!context.mounted || session == null) return; + final userId = session.id; + if (userId.isNotEmpty) { + await saveUserId(userId); + } + try { + final bool isProfileComplete = await profileStatus(userId); + print(isProfileComplete); + if (isProfileComplete) { + print('home'); + context.go('/myhomepage'); + } else { + print('profile'); + if (userId != null && userId.isNotEmpty) { + context.go('/profile/$userId'); + } else { + print('Error: userId is null or empty.'); + } + } + } catch (e) { + // Handle specific errors based on their message + if (e.toString().contains('Admin not approved')) { + context.simpleDialog( + title: context.translate( + 'Admin Approval Required', 'موافقة المسؤول مطلوبة'), + content: context.translate( + 'Your account has not been approved by the admin.', + 'لم تتم الموافقة على حسابك من قبل المسؤول.', + ), + ); + } else if (e.toString().contains('Email not verified')) { + context.simpleDialog( + title: context.translate( + 'Email Not Verified', 'البريد الإلكتروني غير مُحقق'), + content: context.translate( + 'Your email address is not verified. Please check your email.', + 'عنوان بريدك الإلكتروني غير مُحقق. يرجى التحقق من بريدك الإلكتروني.', + ), + ); + } else { + print('Unexpected error: $e'); + } + } + + // context.go('/${context.language}/${BottomNavBarItem.home.routePath}'); }, - // async { - // final isValid = formKey.currentState!.validate(); - // if (!isValid) return; - // final session = await context.loaderWithErrorDialog( - // () => ref - // .read( - // authUseCaseProvider.notifier, - // ) - // .login( - // emailCtl.text, - // pwCtl.text, - // ), - // errorDialogBuilder: ( - // error, [ - // StackTrace? stackTrace, - // ]) { - // if (error == LoginError.invalidEmailPw) { - // return context.simpleDialog( - // title: context.translate( - // 'Incorrect credentials', - // 'أوراق غير صحيحة', - // ), - // content: context.translate( - // 'Your email or password is invalid. Please try again.', - // 'بريدك الإلكتروني أو كلمة المرور غير صالحة. حاول مرة اخرى.', - // ), - // ); - // } - // if (error == LoginError.emailAddressNotVerified) { - // return context.simpleDialog( - // title: context.translate( - // 'Verification Error', - // 'خطأ التحقق', - // ), - // content: context.translate( - // '${emailCtl.text} is not a verified email address. Please check your email for a verification link.', - // '${emailCtl.text} ليس عنوان بريد إلكتروني تم التحقق منه. يرجى التحقق من بريدك الإلكتروني للحصول على رابط التحقق.', - // ), - // extraAction: ElevatedButton( - // onPressed: () { - // context.go('/profile'); - // }, - // // async { - // // Navigator.of( - // // context, - // // rootNavigator: true, - // // ).pop(); - // // await context.loaderWithErrorDialog( - // // () => ref - // // .read(authUseCaseProvider.notifier) - // // .requestVerificationEmail(emailCtl.text), - // // ); - // // if (!context.mounted) return; - // // context.simpleDialog( - // // title: 'Email Re-sent', - // // content: - // // 'We\'ve sent you the verification email at ${emailCtl.text} again.', - // // ); - // // }, - // child: Text( - // context.translate( - // 'I did not receive an email', - // 'لم أتلق بريدًا إلكترونيًا', - // ), - // ), - // ), - // ); - // } - // return context.simpleDialog(); - // }, - // ); - // // if (!context.mounted || session == null) return; - // // Extract userId from the session - // // final userId = session.id; - // // - // // final bool isUpdate = await profileStatus(userId); - // // - // // print('Is profile completed: $isUpdate'); - // - // // if (isUpdate) { - // // Navigator.pushReplacement( - // // context, - // // MaterialPageRoute(builder: (context) => DemoHome()), - // // ); - // // } - // // else { - // - // // Navigator.pushReplacement( - // // context, - // // MaterialPageRoute( - // // builder: (context) => ProfileScreen(userId: userId)), - // // ); - // //} - // - // // context.go('/${context.language}/${BottomNavBarItem.home.routePath}'); - // }, style: ButtonStyle( shape: WidgetStatePropertyAll( RoundedRectangleBorder( diff --git a/lib/presentation/routes/drawer_routes/Drawer Items/edit_profile.dart b/lib/presentation/routes/drawer_routes/Drawer Items/edit_profile.dart new file mode 100644 index 00000000..3d74ef02 --- /dev/null +++ b/lib/presentation/routes/drawer_routes/Drawer Items/edit_profile.dart @@ -0,0 +1,837 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:go_router/go_router.dart'; +import 'package:http/http.dart' as http; +import 'package:image_picker/image_picker.dart'; +import 'package:intl/intl.dart'; +import 'package:pocketbase/pocketbase.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +import '../../../Screens/auth_verification/changepassword.dart'; +import '../custom_drawer_routes.dart'; + +class EditProfile extends StatefulWidget { + @override + State createState() => _EditProfileState(); +} + +class _EditProfileState extends State { + final _pb = PocketBase('https://pb.venbait.in'); + // final _pb = PocketBase('http://127.0.0.1:8090'); + bool _isProfileCompleted = false; + + // Add focus nodes and hint states + final List _focusNodes = List.generate(4, (_) => FocusNode()); + final List _showHints = [ + true, + true, + true, + true + ]; // One for each TextField + + String _getControllerText(int index) { + if (index == 0) return _usernameController.text; + if (index == 1) return _emailController.text; + if (index == 2) return _fullNameController.text; + if (index == 3) return _dateController.text; + + return ''; + } + + final _formKey = GlobalKey(); + final TextEditingController _fullNameController = TextEditingController(); + final TextEditingController _dateController = TextEditingController(); + final TextEditingController _usernameController = TextEditingController(); + final TextEditingController _emailController = TextEditingController(); + // To keep track of the selected date + DateTime? _selectedDate; + + // Date format for the display + final DateFormat _dateFormat = DateFormat('yyyy-MM-dd'); + final List _countries = [ + 'United Arab Emirates', + 'United States', + 'India', + 'Canada' + ]; + String? _selectedCountry; + bool isChecked = false; + bool showError = false; + final _picker = ImagePicker(); + File? _profileImage; + String _avatarUrl = ''; + + // Regular expression to validate Full Name (no special characters) + final RegExp _nameRegExp = RegExp(r"^[a-zA-Z\s]+$"); + late Future userDetails; + dynamic userId; + + @override + void initState() { + super.initState(); + + // Add listeners for focus nodes + for (int i = 0; i < _focusNodes.length; i++) { + _focusNodes[i].addListener(() { + setState(() { + // Hide hint when focused and text is not empty + _showHints[i] = + !_focusNodes[i].hasFocus && (_getControllerText(i).isEmpty); + }); + }); + + // Listen to text changes + if (i == 0) { + _usernameController.addListener(() { + setState(() { + _showHints[i] = + !_focusNodes[i].hasFocus && (_getControllerText(i).isEmpty); + }); + }); + } else if (i == 1) { + _emailController.addListener(() { + setState(() { + _showHints[i] = + !_focusNodes[i].hasFocus && (_getControllerText(i).isEmpty); + }); + }); + } else if (i == 1) { + _fullNameController.addListener(() { + setState(() { + _showHints[i] = + !_focusNodes[i].hasFocus && (_getControllerText(i).isEmpty); + }); + }); + } else if (i == 2) { + _dateController.addListener(() { + setState(() { + _showHints[i] = + !_focusNodes[i].hasFocus && (_getControllerText(i).isEmpty); + }); + }); + } + } + checkUserId(); + } + + @override + void dispose() { + for (var focusNode in _focusNodes) { + focusNode.dispose(); + } + _fullNameController.dispose(); + _dateController.dispose(); + super.dispose(); + } + + Future getUserId() async { + final prefs = await SharedPreferences.getInstance(); + return prefs.getString('userId'); // Retrieve the userId + } + + Future checkUserId() async { + userId = await getUserId(); + //userId = 'tplu4by67phkfoq'; + if (userId != null && userId.isNotEmpty) { + print('User ID: $userId'); + _fetchUserData(); + } else { + print('No userId found'); + // Handle case where userId is not available + } + } + + Future _fetchUserData() async { + try { + final adminAuth = await _pb.admins + .authWithPassword('pb@venbainfotech.com', 'pb@venbainfotech.com'); + final adminToken = adminAuth.token; + print('adminToken- ${adminToken}'); + final userDetailsResponse = await _pb.collection('users').getOne( + userId, + headers: { + 'Authorization': 'Bearer $adminToken', + }, + ); + print('userDetails: $userDetailsResponse'); + setState(() { + _usernameController.text = userDetailsResponse.data['username'] ?? ''; + _emailController.text = userDetailsResponse.data['email'] ?? ''; + _fullNameController.text = userDetailsResponse.data['full_name'] ?? ''; + _selectedCountry = userDetailsResponse.data['country_region'] ?? ''; + _isProfileCompleted = + userDetailsResponse.data['is_profile_completed'] ?? false; + //_isProfileCompleted = true ; + + // Parse and format the date + String dateString = userDetailsResponse.data['dob'] ?? ''; + if (dateString.isNotEmpty) { + DateTime dob = DateTime.parse(dateString); + _dateController.text = + DateFormat('dd/MM/yyyy').format(dob); // Format to dd/mm/yyyy + } else { + _dateController.text = ''; + } + + // Set the avatar URL + String avatarFilename = userDetailsResponse.data['avatar'] ?? ''; + String recordId = userId; + String collectionId = + userDetailsResponse.data['collectionId'] ?? '_pb_users_auth_'; + + if (avatarFilename.isNotEmpty && recordId.isNotEmpty) { + _avatarUrl = + 'https://pb.venbait.in/api/files/$collectionId/$recordId/$avatarFilename'; + } else { + _avatarUrl = ''; // Reset to default or empty + } + + // String avatarFilename = userDetailsResponse.data['avatar'] ?? ''; + // //print("PROFILE AVAILABLE -$avatarFilename"); + // String recordId = userId; + // //String recordId = userDetailsResponse.data['id'] ?? ""; + // print("PROFILE AVAILABLE -${userDetailsResponse.data['id']}"); + // //print("User ID: $recordId"); + + //String collectionId = userDetailsResponse.data['collectionId'] ?? '_pb_users_auth_'; + + // Ensure recordId and avatarFilename are valid + // if (avatarFilename.isNotEmpty && recordId.isNotEmpty) { + // print("PROFILE AVAILABLE"); + // String _avatarUrl = 'https://pb.venbait.in/api/files/$collectionId/$recordId/$avatarFilename'; + // print("PROFILE AVAILABLE1- $_avatarUrl"); + // //_avatarUrl = File(imageUrl); // This won't work directly for a URL, you need to download the image first + // } else { + // print("PROFILE NOT AVAILABLE"); + // } + }); + } catch (e) { + print('Error fetching user details: $e'); + } + } + + void _pickImage() async { + final XFile? pickedFile = + await _picker.pickImage(source: ImageSource.gallery); + if (pickedFile != null) { + setState(() { + _profileImage = File(pickedFile.path); + }); + } + } + + // Function to open the date picker + Future _pickDate() async { + final DateTime today = DateTime.now(); + final DateTime initialDate = _selectedDate ?? + today.subtract( + const Duration(days: 365 * 18)); // Default to 18 years ago + final DateTime firstDate = today.subtract(const Duration( + days: 365 * 100)); // Allow picking dates back to 100 years ago + final DateTime lastDate = today; // Allow picking dates up to today + + // Updated Date format to DD/MM/YYYY + final DateFormat _dateFormat = DateFormat('dd/MM/yyyy'); + + final DateTime? pickedDate = await showDatePicker( + context: context, + initialDate: initialDate, + firstDate: firstDate, + lastDate: lastDate, + ); + + if (pickedDate != null && pickedDate != _selectedDate) { + setState(() { + _selectedDate = pickedDate; + _dateController.text = _dateFormat.format(pickedDate); + }); + } + } + + // Function to validate the DOB field + String? _validateDob(String? value) { + if (value == null || value.isEmpty) { + return 'Required'; + } + + final DateTime selectedDate = _selectedDate!; + final DateTime today = DateTime.now(); + + // Check if the selected date is in the future + if (selectedDate.isAfter(today)) { + return 'Date of birth cannot be in the future'; + } + return null; + } + + String? _validateDropdown(String? value) { + if (value == null || value.isEmpty) { + return 'Required'; + } + return null; + } + + void _toggleCheckbox(bool? value) { + setState(() { + isChecked = value ?? false; + }); + } + + void showConfirmationDialog(BuildContext context) async { + final result = await showDialog( + context: context, + builder: (context) => const ConfirmationDialog(), + ); + + if (result == true) { + // Validate only the country field + if (_validateDropdown(_selectedCountry) == null) { + try { + String userID = userId; + + print("ShowConfirmationuserID - $userID "); + // Retrieve data from the country/region field + String countryRegion = + _selectedCountry ?? ''; // Ensure the country is selected + + // Create a multipart request + final uri = + Uri.parse('${_pb.baseUrl}/api/collections/users/records/$userID'); + final request = http.MultipartRequest('PATCH', uri); + + // Add fields to the request + request.fields['country_region'] = + countryRegion; // Only update country here + + // If profile image exists, add it + if (_profileImage != null) { + request.files.add(await http.MultipartFile.fromPath( + 'avatar', + _profileImage!.path, + )); + } + + // Add headers (e.g., authorization) + request.headers['Authorization'] = 'Bearer ${_pb.authStore.token}'; + + // Send the request + final response = await request.send(); + print(response); + + // Handle response + if (response.statusCode == 200) { + _resetFormFields(); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text("Profile updated successfully!")), + ); + context.go('/myhomepage'); + } else { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: + Text("Failed to update profile: ${response.statusCode}")), + ); + } + } catch (error) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text("Failed to update profile: $error")), + ); + } + } else { + // Show an error if the country is invalid + setState(() { + showError = true; + }); + } + } + } + + void _resetFormFields() { + print('reset'); + setState(() { + // Reset all text controllers + _fullNameController.clear(); + _dateController.clear(); + _selectedCountry = null; + isChecked = false; + + // Reset profile image + _profileImage = null; + + // Reset form validation state + _formKey.currentState?.reset(); + }); + } + + @override + Widget build(BuildContext context) { + return BaseScaffold( + title: Text("Profile"), + body: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.only(left: 10.0, right: 10.0, top: 3.0), + child: Column( + children: [ + SingleChildScrollView( + child: Form( + key: _formKey, + child: Padding( + padding: const EdgeInsets.all(20.0), + child: Column( + children: [ + CircleAvatar( + radius: 50, + backgroundImage: _profileImage != null + ? FileImage( + _profileImage!) // If a local file is selected + : _avatarUrl.isNotEmpty + ? NetworkImage(_avatarUrl) // Load from URL + : AssetImage( + "assets/edit_profile/profile.png") + as ImageProvider, + + //backgroundImage: NetworkImage(_avatarUrl) as ImageProvider, + child: Align( + alignment: Alignment.bottomRight, + child: GestureDetector( + onTap: _pickImage, // Call `_pickImage` on tap + child: CircleAvatar( + radius: 15, + backgroundColor: Colors.white, + child: Icon( + Icons.camera_alt, + size: 15, + color: Colors.grey, + ), + ), + ), + ), + ), + SizedBox(height: 20), + Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Text( + "User Name", + style: TextStyle( + fontSize: 16, fontWeight: FontWeight.w500), + ), + ], + ), + SizedBox(height: 10), + TextFormField( + controller: _usernameController, + focusNode: _focusNodes[0], + decoration: InputDecoration( + hintText: _showHints[0] ? 'Mohammad Hassan' : null, + hintStyle: TextStyle(color: Colors.grey), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + ), + enabled: false, + ), + ), + SizedBox(height: 10), + Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Text( + "E-mail", + style: TextStyle( + fontSize: 16, fontWeight: FontWeight.w500), + ), + ], + ), + SizedBox(height: 10), + TextFormField( + controller: _emailController, + focusNode: _focusNodes[1], + decoration: InputDecoration( + hintText: _showHints[1] + ? 'mohammad.hassan@fcsc.gov.ae' + : null, + hintStyle: TextStyle(color: Colors.grey), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + ), + enabled: false, + ), + ), + SizedBox(height: 10), + Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Text( + "Full Name", + style: TextStyle( + fontSize: 16, fontWeight: FontWeight.w500), + ), + ], + ), + SizedBox(height: 10), + TextFormField( + enabled: false, + validator: (value) { + if (value == null || value.isEmpty) { + return 'Required'; + } + + //RegExp(r"^[a-zA-Z\s]+$"); + final nameRegex = RegExp( + r"^[a-zA-Z0-9'\-\u0627\u0639~\u00C0-\u00FF]+$"); + if (!nameRegex.hasMatch(value)) { + return 'Invalid Characters'; + } + return null; + }, + controller: _fullNameController, + focusNode: _focusNodes[2], + decoration: InputDecoration( + hintText: _showHints[2] ? 'Mohammad' : null, + hintStyle: TextStyle(color: Colors.grey), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + ), + // counterText: '', + enabled: !_isProfileCompleted, + ), + maxLength: + 40, // Set the maximum length to 20 characters + maxLengthEnforcement: MaxLengthEnforcement.enforced, + ), + SizedBox(height: 10), + Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Text( + "Date of Birth*", + style: TextStyle( + fontSize: 16, fontWeight: FontWeight.w500), + ), + ], + ), + SizedBox(height: 10), + TextFormField( + controller: _dateController, + focusNode: _focusNodes[3], + decoration: InputDecoration( + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + ), + hintText: _showHints[3] + ? 'Select your Date of Birth' + : null, + //hintText: 'Select your Date of Birth', + suffixIcon: const Icon(Icons.arrow_drop_down_sharp), + enabled: !_isProfileCompleted, + ), + readOnly: true, + onTap: _pickDate, + validator: _validateDob, + ), + SizedBox(height: 10), + Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Text( + "Country/Region*", + style: TextStyle( + fontSize: 16, fontWeight: FontWeight.w500), + ), + ], + ), + SizedBox(height: 10), + DropdownButtonFormField( + value: _selectedCountry, + decoration: InputDecoration( + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + ), + labelText: 'Select', + ), + items: _countries + .map((item) => DropdownMenuItem( + value: item, + child: Text(item), + )) + .toList(), + onChanged: (String? newValue) { + setState(() { + _selectedCountry = newValue; + }); + }, + validator: _validateDropdown, + ), + SizedBox(height: 20), + if (!_isProfileCompleted) // Conditional rendering + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Checkbox( + value: isChecked, + onChanged: (value) { + setState(() { + isChecked = value ?? false; + showError = false; + }); + }, + side: BorderSide( + color: + showError ? Colors.red : Colors.grey, + width: 1.5, + ), + ), + Expanded( + child: Column( + mainAxisAlignment: + MainAxisAlignment.start, + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + SizedBox(height: 10), + Text.rich( + TextSpan( + text: 'I agree to the ', + style: + TextStyle(color: Colors.black), + children: [ + TextSpan( + text: 'Terms & Conditions', + style: TextStyle( + color: Colors.blue, + decoration: + TextDecoration.underline, + ), + recognizer: + TapGestureRecognizer() + ..onTap = () { + // Add action for Terms & Conditions tap + }, + ), + TextSpan( + text: ' and ', + style: TextStyle( + color: Colors.black), + ), + TextSpan( + text: 'Privacy Policy', + style: TextStyle( + color: Colors.blue, + decoration: + TextDecoration.underline, + ), + recognizer: + TapGestureRecognizer() + ..onTap = () { + // Add action for Privacy Policy tap + }, + ), + TextSpan( + text: ' of FCSC.', + style: TextStyle( + color: Colors.black), + ), + ], + ), + textAlign: TextAlign.start, + maxLines: 2, + overflow: TextOverflow.visible, + softWrap: true, + ), + ], + ), + ), + ], + ), + if (showError) + Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Padding( + padding: + const EdgeInsets.only(left: 10.0), + child: Text( + 'Please agree to terms and conditions', + style: TextStyle( + color: Colors.red[700], + fontSize: 12, + ), + ), + ), + ], + ), + ], + ), + SizedBox(height: 20), + Center( + child: GestureDetector( + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => + Changepassword(userId: userId)), + ); + }, + child: Text( + "Change Password", + style: TextStyle( + color: Colors.blue, + decoration: TextDecoration.underline), + ), + ), + ), + SizedBox(height: 20), + ElevatedButton.icon( + onPressed: () { + showConfirmationDialog(context); + + // if (_formKey.currentState?.validate() ?? false){ + // _formKey.currentState?.save(); + // showConfirmationDialog(context); + // } else { + // setState(() { + // showError = + // !isChecked; // Show error if the checkbox is not checked + // }); + // } + }, + icon: Icon( + Icons.save, + color: Colors.white, + ), + label: Text( + 'Save', + style: TextStyle(color: Colors.white), + ), + style: ElevatedButton.styleFrom( + backgroundColor: Color(0xFF92722A), + minimumSize: Size(double.infinity, 50), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + ), + ], + ), + ), + ), + ), + ], + ), + ), + ), + //bottomNavigationBar: MyBottomNavBar(), + ); + } +} + +class ConfirmationDialog extends StatelessWidget { + const ConfirmationDialog({Key? key}) : super(key: key); + @override + Widget build(BuildContext context) { + return AlertDialog( + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + "Are you sure you want to save this page?", + style: const TextStyle( + fontSize: 14, + color: Color(0xFF898C81), + fontFamily: 'Roboto', + fontWeight: FontWeight.w400, + ), + textAlign: TextAlign.center, + ), + SizedBox(height: 8), + Text.rich( + TextSpan( + text: "Once saved, you will not be able to change your ", + style: TextStyle( + fontSize: 14, + color: Color(0xFF898C81), + fontFamily: 'Roboto', + fontWeight: FontWeight.w400, + ), + children: [ + TextSpan( + text: "Country", + style: + TextStyle(fontWeight: FontWeight.w700), // Bold for "name" + ), + TextSpan( + text: " or ", + ), + TextSpan( + text: "Profile Image", + style: TextStyle( + fontWeight: FontWeight.w700), // Bold for "date of birth" + ), + TextSpan( + text: ".", + ), + ], + ), + textAlign: TextAlign.center, + ) + ], + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + contentPadding: const EdgeInsets.fromLTRB(24, 20, 24, 0), + actionsPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + actions: [ + Padding( + padding: const EdgeInsets.all(8.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + OutlinedButton( + onPressed: () { + Navigator.of(context).pop(false); + }, + style: OutlinedButton.styleFrom( + side: BorderSide(color: Color(0xFF92722A)), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(7)), + ), // Set the border color here + ), + child: Text( + "Cancel", + style: TextStyle(color: Color(0xFF92722A)), + ), + ), + ElevatedButton( + onPressed: () => Navigator.of(context).pop(true), + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFF92722A), // Brown color + foregroundColor: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + child: const Text( + 'Confirm', + style: TextStyle( + fontWeight: FontWeight.w500, + ), + ), + ), + ], + ), + ), + SizedBox( + height: 5, + ) + ], + ); + } +} diff --git a/lib/presentation/routes/drawer_routes/Drawer Items/feedback.dart b/lib/presentation/routes/drawer_routes/Drawer Items/feedback.dart index 5d4ce688..7934ed17 100644 --- a/lib/presentation/routes/drawer_routes/Drawer Items/feedback.dart +++ b/lib/presentation/routes/drawer_routes/Drawer Items/feedback.dart @@ -17,7 +17,6 @@ import '../../../../config/my_theme.dart'; import '../../../../domain/use_cases/preferences_use_case.dart'; import '../../../components/my_toggle.dart'; - class FeedbackForm extends StatefulWidget { const FeedbackForm({super.key}); @@ -28,7 +27,7 @@ class FeedbackForm extends StatefulWidget { class _FeedbackFormState extends State with WidgetsBindingObserver { final _pb = - PocketBase('https://pb.venbait.in'); // Initialize PocketBase client + PocketBase('https://pb.venbait.in'); // Initialize PocketBase client // final _pb = // PocketBase('http://127.0.0.1:8090'); // Initialize PocketBase client @@ -43,34 +42,35 @@ class _FeedbackFormState extends State bool _isFeedbackFailed = false; // New flag for failed submission bool _isSmileySelected = true; // Track if smiley is selected dynamic configEmail; + dynamic userId; List> get _emojiOptions => [ - { - "icon": Icons.sentiment_very_dissatisfied, - "label": context.translate("Terrible", "رهيب"), // Translate here - "value": 1, - }, - { - "icon": Icons.sentiment_dissatisfied, - "label": context.translate("Bad", "سيء"), // Translate here - "value": 2, - }, - { - "icon": Icons.sentiment_neutral, - "label": context.translate("Okay", "تمام"), // Translate here - "value": 3, - }, - { - "icon": Icons.sentiment_satisfied, - "label": context.translate("Good", "جيد"), // Translate here - "value": 4, - }, - { - "icon": Icons.sentiment_very_satisfied, - "label": context.translate("Amazing", "مدهش"), // Translate here - "value": 5, - }, - ]; + { + "icon": Icons.sentiment_very_dissatisfied, + "label": context.translate("Terrible", "رهيب"), // Translate here + "value": 1, + }, + { + "icon": Icons.sentiment_dissatisfied, + "label": context.translate("Bad", "سيء"), // Translate here + "value": 2, + }, + { + "icon": Icons.sentiment_neutral, + "label": context.translate("Okay", "تمام"), // Translate here + "value": 3, + }, + { + "icon": Icons.sentiment_satisfied, + "label": context.translate("Good", "جيد"), // Translate here + "value": 4, + }, + { + "icon": Icons.sentiment_very_satisfied, + "label": context.translate("Amazing", "مدهش"), // Translate here + "value": 5, + }, + ]; static const int _characterLimit = 1200; final RegExp _allowedCharacters = RegExp(r'^[a-zA-Z0-9 .,!?-]*$'); @@ -83,7 +83,23 @@ class _FeedbackFormState extends State WidgetsBinding.instance.addObserver(this); _loadFeedbackText(); _feedbackController.addListener(_handleTextChange); - fetchEmailConfiguration(); + // fetchEmailConfiguration(); + checkUserId(); + } + + Future getUserId() async { + final prefs = await SharedPreferences.getInstance(); + return prefs.getString('userId'); // Retrieve the userId + } + + Future checkUserId() async { + userId = await getUserId(); + if (userId != null && userId.isNotEmpty) { + print('User ID: $userId'); + } else { + print('No userId found'); + // Handle case where userId is not available + } } @override @@ -217,8 +233,8 @@ class _FeedbackFormState extends State child: _isFeedbackSubmitted ? _buildThankYouMessage() : _isFeedbackFailed - ? _buildFailureMessage() - : _buildFeedbackForm(), + ? _buildFailureMessage() + : _buildFeedbackForm(), ), ), ); @@ -303,7 +319,7 @@ class _FeedbackFormState extends State _buildRatingRow( context.translate('Ease of use', 'سهولة الاستخدام'), _easeOfUseRating, - (rating) { + (rating) { setState(() { _easeOfUseRating = rating; }); @@ -312,7 +328,7 @@ class _FeedbackFormState extends State _buildRatingRow( context.translate('Quality', 'جودة'), _qualityRating, - (rating) { + (rating) { setState(() { _qualityRating = rating; }); @@ -321,7 +337,7 @@ class _FeedbackFormState extends State _buildRatingRow( context.translate('Design', 'تصميم'), _designRating, - (rating) { + (rating) { setState(() { _designRating = rating; }); @@ -330,7 +346,7 @@ class _FeedbackFormState extends State _buildRatingRow( context.translate('Redundant', 'متكرر'), _redundancyRating, - (rating) { + (rating) { setState(() { _redundancyRating = rating; }); @@ -351,7 +367,7 @@ class _FeedbackFormState extends State ), ), errorText: - _hasError ? _errorMessage : null, // Show error below field + _hasError ? _errorMessage : null, // Show error below field ), onChanged: (text) { // Trigger re-validation and character limit on each change @@ -428,7 +444,7 @@ class _FeedbackFormState extends State const SizedBox(height: 30), ElevatedButton( onPressed: () => context.go('/myhomepage'), - //context.go('/${context.language}/${BottomNavBarItem.home.routePath}'), + //context.go('/${context.language}/${BottomNavBarItem.home.routePath}'), child: const Text("Go to Home"), ), ], @@ -495,10 +511,10 @@ class _FeedbackFormState extends State } Widget _buildRatingRow( - String label, - double rating, - Function(double) onRatingUpdate, - ) { + String label, + double rating, + Function(double) onRatingUpdate, + ) { return Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ @@ -548,12 +564,13 @@ class _FeedbackFormState extends State } final feedbackData = { + "userId": userId, "ease_of_use": _easeOfUseRating, "quality": _qualityRating, "design": _designRating, "redundancy": _redundancyRating, "emoji_rating": _emojiOptions[_selectedEmojiIndex!] - ["label"], // Emoji rating + ["label"], // Emoji rating "feedback": _feedbackController.text, }; @@ -568,7 +585,7 @@ class _FeedbackFormState extends State final createdTime = response.created; // Send email - await sendFeedbackEmail(feedbackData, createdTime); + // await sendFeedbackEmail(feedbackData, createdTime); await _removeFeedbackText(); ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text("Feedback submitted successfully!")), @@ -598,107 +615,107 @@ class _FeedbackFormState extends State _feedbackController.clear(); } - Future fetchEmailConfiguration() async { - try { - // Fetch data from the email_configuration collection - final response = - await _pb.collection('email_configuration').getFullList(); - - // Filter the data where the label is "Feedback" - final feedbackConfig = response.firstWhere( - (item) => - item.data['label'] == 'Feedback', // Accessing the 'data' property - ); - - // Check if a match is found - if (feedbackConfig != null) { - configEmail = feedbackConfig.data['email']; // Access the 'email' value - } else { - print('No feedback configuration found.'); - } - } catch (e) { - print('Error fetching email configuration: $e'); - } - } - - Future sendFeedbackEmail( - Map feedbackData, createdTime) async { - String username = 'emailapikey'; // Your SMTP username (API key) - String password = - 'PHtE6r0MFu66jTQp8BAFsP7sH5TwNd4v/+02KwBH5ItACvAES01Tot4okDawqhoiB/FEHfaey4Nvteyf5ePQJG28YW9OCWqyqK3sx/VYSPOZsbq6x00auVwYd0zUVY7pe9ds0yLTvNraNA=='; // Your SMTP password - - final smtpServer = SmtpServer('smtp.zeptomail.in', - port: 587, - username: username, - password: password, - ssl: false, // Use TLS - ignoreBadCertificate: - true); // Set to true if you're testing with a self-signed certificate - - // Check if additional feedback was provided - String additionalFeedback = feedbackData["feedback"]?.isNotEmpty == true - ? feedbackData["feedback"] - : "No additional feedback provided."; - - // Format separately for date and time - // Parse the string to DateTime - // Parse the input as UTC and convert to DateTime in UTC timezone - DateTime feedbackDateTime = DateTime.parse(createdTime).toUtc(); - - // Format date as DD-MM-YYYY in UTC - final String formattedDate = - DateFormat('dd-MM-yyyy').format(feedbackDateTime); - - // Format to the desired output: "dd.MM.yyyy HH:mm 'UTC'" - final DateFormat formatter = DateFormat("dd.MM.yyyy HH:mm 'UTC'"); - String formattedDateTime = formatter.format(feedbackDateTime); - - String email = configEmail; // Direct assignment - // Create the email message - final message = Message() - ..from = Address('bbone@venbait.in', 'FCSC') - ..recipients.add(email) // Set the recipient email - ..subject = 'UAE Stats Feedback' - ..text = ''' - Dear [App Owner/Admin], - - You have received new feedback from a user through the mobile application. - - User Details: - - 1. Name: Guest - 2. Date of Submission: $formattedDate - 3. Time of Submission: $formattedDateTime - - Feedback: - - 1. How was your experience with us today? Rating: ${feedbackData["emoji_rating"]} - 2. How did we perform in key areas? - 1. Ease of Use: ${feedbackData["ease_of_use"]} - 2. Quality: ${feedbackData["quality"]} - 3. Design: ${feedbackData["design"]} - 4. Redundancy: ${feedbackData["redundancy"]} - 3. Additional Feedback: - 1. $additionalFeedback - - Thank you, - The FCSC App Team - '''; - - try { - // Send the email - print("Message details:"); - print("From: ${message.from}"); - print("To: ${message.recipients}"); - print("Subject: ${message.subject}"); - print("Body: ${message.text}"); - final sendReport = await send(message, smtpServer); - print('Message sent: ' + sendReport.toString()); - } catch (e) { - print('Message not sent: $e'); - // Handle the error as needed - } - } + // Future fetchEmailConfiguration() async { + // try { + // // Fetch data from the email_configuration collection + // final response = + // await _pb.collection('email_configuration').getFullList(); + // + // // Filter the data where the label is "Feedback" + // final feedbackConfig = response.firstWhere( + // (item) => + // item.data['label'] == 'Feedback', // Accessing the 'data' property + // ); + // + // // Check if a match is found + // if (feedbackConfig != null) { + // configEmail = feedbackConfig.data['email']; // Access the 'email' value + // } else { + // print('No feedback configuration found.'); + // } + // } catch (e) { + // print('Error fetching email configuration: $e'); + // } + // } + // + // Future sendFeedbackEmail( + // Map feedbackData, createdTime) async { + // String username = 'emailapikey'; // Your SMTP username (API key) + // String password = + // 'PHtE6r0MFu66jTQp8BAFsP7sH5TwNd4v/+02KwBH5ItACvAES01Tot4okDawqhoiB/FEHfaey4Nvteyf5ePQJG28YW9OCWqyqK3sx/VYSPOZsbq6x00auVwYd0zUVY7pe9ds0yLTvNraNA=='; // Your SMTP password + // + // final smtpServer = SmtpServer('smtp.zeptomail.in', + // port: 587, + // username: username, + // password: password, + // ssl: false, // Use TLS + // ignoreBadCertificate: + // true); // Set to true if you're testing with a self-signed certificate + // + // // Check if additional feedback was provided + // String additionalFeedback = feedbackData["feedback"]?.isNotEmpty == true + // ? feedbackData["feedback"] + // : "No additional feedback provided."; + // + // // Format separately for date and time + // // Parse the string to DateTime + // // Parse the input as UTC and convert to DateTime in UTC timezone + // DateTime feedbackDateTime = DateTime.parse(createdTime).toUtc(); + // + // // Format date as DD-MM-YYYY in UTC + // final String formattedDate = + // DateFormat('dd-MM-yyyy').format(feedbackDateTime); + // + // // Format to the desired output: "dd.MM.yyyy HH:mm 'UTC'" + // final DateFormat formatter = DateFormat("dd.MM.yyyy HH:mm 'UTC'"); + // String formattedDateTime = formatter.format(feedbackDateTime); + // + // String email = configEmail; // Direct assignment + // // Create the email message + // final message = Message() + // ..from = Address('bbone@venbait.in', 'FCSC') + // ..recipients.add(email) // Set the recipient email + // ..subject = 'UAE Stats Feedback' + // ..text = ''' + // Dear [App Owner/Admin], + // + // You have received new feedback from a user through the mobile application. + // + // User Details: + // + // 1. Name: Guest + // 2. Date of Submission: $formattedDate + // 3. Time of Submission: $formattedDateTime + // + // Feedback: + // + // 1. How was your experience with us today? Rating: ${feedbackData["emoji_rating"]} + // 2. How did we perform in key areas? + // 1. Ease of Use: ${feedbackData["ease_of_use"]} + // 2. Quality: ${feedbackData["quality"]} + // 3. Design: ${feedbackData["design"]} + // 4. Redundancy: ${feedbackData["redundancy"]} + // 3. Additional Feedback: + // 1. $additionalFeedback + // + // Thank you, + // The FCSC App Team + // '''; + // + // try { + // // Send the email + // print("Message details:"); + // print("From: ${message.from}"); + // print("To: ${message.recipients}"); + // print("Subject: ${message.subject}"); + // print("Body: ${message.text}"); + // final sendReport = await send(message, smtpServer); + // print('Message sent: ' + sendReport.toString()); + // } catch (e) { + // print('Message not sent: $e'); + // // Handle the error as needed + // } + // } @override void dispose() { @@ -763,7 +780,7 @@ class LangToggle extends ConsumerWidget { ref.read(preferencesUseCaseProvider.notifier).updatePreferences( (prefs) => prefs.copyWith(language: languageAfter), - ); + ); await onLoadFeedback(); // Reload feedback text after toggling language }, diff --git a/lib/presentation/routes/drawer_routes/Drawer Items/manage_users.dart b/lib/presentation/routes/drawer_routes/Drawer Items/manage_users.dart index 79704ebf..7d87d47a 100644 --- a/lib/presentation/routes/drawer_routes/Drawer Items/manage_users.dart +++ b/lib/presentation/routes/drawer_routes/Drawer Items/manage_users.dart @@ -1,50 +1,25 @@ import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; +import 'package:pocketbase/pocketbase.dart'; import 'package:uae_stat/presentation/components/my_bottom_nav_bar.dart'; import 'package:uae_stat/presentation/components/my_drawer.dart'; import 'package:uae_stat/presentation/routes/drawer_routes/custom_drawer_routes.dart'; class ManageUserRouter extends StatefulWidget { -late final Widget title; + late final Widget title; @override State createState() => _ManageUserRouterState(); } class _ManageUserRouterState extends State { - // Sample data for the table - final List userData = List.generate( - 5, - (index) => User( - userName: 'Conan Keller ${index + 1}', - emailId: 'ConanKeller${index + 1}@gmail.com', - registrationDate: '18/07/2024', - status: 'Pending', - ), - ); - - // Sorting state + final _pb = PocketBase('https://pb.venbait.in'); + List userData = []; int _sortColumnIndex = 0; bool _isAscending = true; + List filteredUserData = []; - // Method to sort the data based on a column - void _sort(Comparable Function(User user) getField, int columnIndex, - bool ascending) { - setState(() { - _sortColumnIndex = columnIndex; - _isAscending = ascending; - userData.sort((a, b) { - final aValue = getField(a); - final bValue = getField(b); - return ascending - ? Comparable.compare(aValue, bValue) - : Comparable.compare(bValue, aValue); - }); - }); - } - - // Define dropdown items final List statusOptions = ['Approved', 'Denied', 'Pending']; - // Method to get status color Color getStatusColor(String status) { switch (status) { case 'Approved': @@ -58,9 +33,80 @@ class _ManageUserRouterState extends State { } } + @override + void initState() { + super.initState(); + fetchUnverifiedUsers(); + } + + Future fetchUnverifiedUsers() async { + try { + await _pb.admins + .authWithPassword('pb@venbainfotech.com', 'pb@venbainfotech.com'); + final result = await _pb.collection('users').getFullList( + filter: 'verified = false', + ); + + setState(() { + userData = result.map((record) { + final createdDate = + DateTime.parse(record.created); // Parse the created date + final formattedDate = DateFormat('dd/MM/yyyy').format(createdDate); + return User( + id: record.id, // Correctly passing the ID + userName: record.getStringValue('username'), + emailId: record.getStringValue('email'), + registrationDate: formattedDate, + status: record.getStringValue('status'), + ); + }).toList(); + filteredUserData = List.from(userData); + }); + } catch (e) { + print('Error fetching unverified users: $e'); + } + } + + // Method to filter the list based on the search input + void filterUsers(String query) { + print('Filterd User: $query'); + setState(() { + filteredUserData = userData.where((user) { + return user.userName.toLowerCase().contains(query.toLowerCase()) || + user.emailId.toLowerCase().contains(query.toLowerCase()) || + user.registrationDate.contains(query) || + user.status.toLowerCase().contains(query.toLowerCase()); + }).toList(); + }); + } + + void _sort( + Comparable? Function(User user) getField, + int columnIndex, + bool ascending, + ) { + setState(() { + _sortColumnIndex = columnIndex; + _isAscending = ascending; + + filteredUserData.sort((a, b) { + final aValue = getField(a); + final bValue = getField(b); + if (aValue == null || bValue == null) { + return 0; // Handle null-safe sorting if needed + } + return ascending + ? Comparable.compare(aValue, bValue) + : Comparable.compare(bValue, aValue); + }); + }); + } + //Method to show a confirmation dialog when status is changed Future _showConfirmationDialog(User user, String newStatus) async { + print('user $user'); double myheight = MediaQuery.of(context).size.height; + return showDialog( context: context, barrierDismissible: false, // User must tap button to dismiss dialog @@ -69,7 +115,8 @@ class _ManageUserRouterState extends State { shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(5.0), // Rounded corners ), - contentPadding: EdgeInsets.zero, // Ensure no padding issues with close icon + contentPadding: + EdgeInsets.zero, // Ensure no padding issues with close icon content: Stack( children: [ Padding( @@ -77,7 +124,9 @@ class _ManageUserRouterState extends State { child: Column( mainAxisSize: MainAxisSize.min, children: [ - SizedBox(height: myheight/30,), + SizedBox( + height: myheight / 30, + ), Text( 'Are you sure you want to change \n the status to $newStatus?', style: TextStyle(fontSize: 15), @@ -114,25 +163,45 @@ class _ManageUserRouterState extends State { ), side: BorderSide( color: Colors.blue, // Set the outline color - width: 2.0, // Set the border width + width: 2.0, // Set the border width ), ), child: Text('No'), ), TextButton( - onPressed: () { - setState(() { - user.status = newStatus; // Change status - }); - Navigator.of(context).pop(); + onPressed: () async { + try { + // Update status in PocketBase + await _pb.collection('users').update( + user.id, // User's unique ID + body: { + 'status': newStatus, // Update status + }, + ); + + // Update local state + // setState(() { + // user.status = newStatus; + // }); + fetchUnverifiedUsers(); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Status updated successfully!')), + ); + } catch (e) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Error updating status: $e')), + ); + } + Navigator.of(context).pop(); // Close dialog }, style: TextButton.styleFrom( - backgroundColor: Colors.blue, // Set background color - foregroundColor: Colors.white, - shape: RoundedRectangleBorder( - borderRadius: - BorderRadius.circular(10.0), // Set text color - )), + backgroundColor: Colors.blue, // Set background color + foregroundColor: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: + BorderRadius.circular(10.0), // Set text color + ), + ), child: Text( 'Yes', style: TextStyle(color: Colors.white), @@ -142,7 +211,6 @@ class _ManageUserRouterState extends State { ) ], ); - }, ); } @@ -165,71 +233,97 @@ class _ManageUserRouterState extends State { borderRadius: BorderRadius.all(Radius.circular(20)), ), ), + onChanged: filterUsers, ), SizedBox(height: myheight / 40), SingleChildScrollView( scrollDirection: Axis.horizontal, - child: DataTable( - sortColumnIndex: _sortColumnIndex, - sortAscending: _isAscending, - columns: [ - DataColumn( - label: const Text('User Name'), - onSort: (columnIndex, ascending) => _sort( - (user) => user.userName, columnIndex, ascending), - numeric: false, - ), - DataColumn( - label: const Text('Email ID'), - onSort: (columnIndex, ascending) => - _sort((user) => user.emailId, columnIndex, ascending), - numeric: false, - ), - DataColumn( - label: const Text('Registration Date'), - onSort: (columnIndex, ascending) => _sort( - (user) => user.registrationDate, - columnIndex, - ascending), - numeric: false, - ), - DataColumn( - label: const Text('Status'), - onSort: (columnIndex, ascending) => - _sort((user) => user.status, columnIndex, ascending), - numeric: false, - ), - ], - rows: userData.map((user) { - return DataRow( - cells: [ - DataCell(Text(user.userName)), - DataCell(Text(user.emailId)), - DataCell(Text(user.registrationDate)), - DataCell( - DropdownButton( - value: user.status, - items: statusOptions.map((status) { - return DropdownMenuItem( - value: status, - child: Text( - status, - style: - TextStyle(color: getStatusColor(status)), + child: ConstrainedBox( + constraints: BoxConstraints( + minWidth: MediaQuery.of(context).size.width, + ), + child: DataTable( + sortColumnIndex: _sortColumnIndex, + sortAscending: _isAscending, + columns: [ + DataColumn( + label: const Text('User Name'), + onSort: (columnIndex, ascending) => _sort( + (user) => user.userName, columnIndex, ascending), + ), + DataColumn( + label: const Text('Email ID'), + onSort: (columnIndex, ascending) => _sort( + (user) => user.emailId, columnIndex, ascending), + ), + DataColumn( + label: const Text('Registration Date'), + onSort: (columnIndex, ascending) { + _sort( + (user) => DateTime.tryParse( + user.registrationDate), // Convert to DateTime + columnIndex, + ascending, + ); + }, + ), + DataColumn( + label: const Text('Status'), + onSort: (columnIndex, ascending) => _sort( + (user) => user.status, columnIndex, ascending), + ), + ], + rows: filteredUserData.isEmpty + ? [ + DataRow( + cells: List.generate( + 4, // Ensure it matches the number of DataColumns + (index) => DataCell( + index == 0 + ? Text( + 'No results found', + style: TextStyle( + fontStyle: FontStyle.italic), + ) + : const Text( + ''), // Empty cells for other columns + placeholder: true, ), - ); - }).toList(), - onChanged: (String? newStatus) { - if (newStatus != null) { - _showConfirmationDialog(user, - newStatus); // Show dialog for confirmation - } - }, - ), - ), - ], - ); - }).toList(), + ), + ), + ] + : filteredUserData.map((user) { + return DataRow( + cells: [ + DataCell(Text(user.userName)), + DataCell(Text(user.emailId)), + DataCell(Text(user.registrationDate)), + DataCell( + DropdownButton( + value: user.status, + items: statusOptions.map((status) { + return DropdownMenuItem( + value: status, + child: Text( + status, + style: TextStyle( + color: getStatusColor(status)), + ), + ); + }).toList(), + onChanged: (newStatus) { + print(user); + if (newStatus != null) { + _showConfirmationDialog( + user, newStatus); + } + }, + ), + ), + ], + ); + }).toList(), + ), ), ), ], @@ -242,12 +336,14 @@ class _ManageUserRouterState extends State { // User data model class User { + final String id; final String userName; final String emailId; final String registrationDate; String status; User({ + required this.id, required this.userName, required this.emailId, required this.registrationDate, diff --git a/lib/presentation/routes/drawer_routes/custom_drawer_routes.dart b/lib/presentation/routes/drawer_routes/custom_drawer_routes.dart index 24383844..ba1d5bd7 100644 --- a/lib/presentation/routes/drawer_routes/custom_drawer_routes.dart +++ b/lib/presentation/routes/drawer_routes/custom_drawer_routes.dart @@ -23,9 +23,8 @@ class BaseScaffold extends StatelessWidget { double myheight = MediaQuery.of(context).size.height; double mywidth = MediaQuery.of(context).size.width; return Scaffold( - appBar: AppBar( - title: title, - actions: [IconButton(onPressed: (){}, icon: Icon(Icons.toggle_off_outlined)), + appBar: AppBar(title: title, actions: [ + IconButton(onPressed: () {}, icon: Icon(Icons.toggle_off_outlined)), ]), drawer: Drawer( child: ListView( @@ -45,7 +44,7 @@ class BaseScaffold extends StatelessWidget { ), Divider(), InkWell( - onTap: () => context.go('/profile'), + onTap: () => context.go('/editProfile'), child: Row( children: [ SizedBox( @@ -69,7 +68,6 @@ class BaseScaffold extends StatelessWidget { ], ), ), - ListTile( leading: SizedBox( height: myheight / 15, @@ -79,7 +77,6 @@ class BaseScaffold extends StatelessWidget { title: Text('Feedback'), onTap: () => context.go('/feedback'), ), - ListTile( leading: SizedBox( height: myheight / 15, @@ -98,18 +95,35 @@ class BaseScaffold extends StatelessWidget { type: BottomNavigationBarType.fixed, items: [ BottomNavigationBarItem( - icon: SizedBox(height: myheight/15,width : mywidth/15,child: Image(image: AssetImage('assets/icons/bottom_bar/home.png'))), + icon: SizedBox( + height: myheight / 15, + width: mywidth / 15, + child: Image( + image: AssetImage('assets/icons/bottom_bar/home.png'))), label: 'Home', ), BottomNavigationBarItem( - icon: SizedBox(height: myheight/15,width : mywidth/15,child: Image(image: AssetImage('assets/icons/bottom_bar/uae_map.png'))), + icon: SizedBox( + height: myheight / 15, + width: mywidth / 15, + child: Image( + image: AssetImage('assets/icons/bottom_bar/uae_map.png'))), label: 'UAE Numbers', ), BottomNavigationBarItem( - icon: SizedBox(height: myheight/15,width : mywidth/15,child: Image(image: AssetImage('assets/icons/bottom_bar/ranking.png'))), + icon: SizedBox( + height: myheight / 15, + width: mywidth / 15, + child: Image( + image: + AssetImage('assets/icons/bottom_bar/ranking.png'))), label: 'Competitiveness'), BottomNavigationBarItem( - icon: SizedBox(height: myheight/15,width : mywidth/15,child: Image(image: AssetImage('assets/icons/bottom_bar/globe.png'))), + icon: SizedBox( + height: myheight / 15, + width: mywidth / 15, + child: Image( + image: AssetImage('assets/icons/bottom_bar/globe.png'))), label: 'Country Profile'), ], selectedItemColor: Colors.black,