diff --git a/devtools_options.yaml b/devtools_options.yaml new file mode 100644 index 00000000..fa0b357c --- /dev/null +++ b/devtools_options.yaml @@ -0,0 +1,3 @@ +description: This file stores settings for Dart & Flutter DevTools. +documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states +extensions: diff --git a/lib/presentation/Screens/profilepage.dart b/lib/presentation/Screens/profilepage.dart index 767f8253..9b0f4822 100644 --- a/lib/presentation/Screens/profilepage.dart +++ b/lib/presentation/Screens/profilepage.dart @@ -1,6 +1,12 @@ +import 'dart:convert'; +import 'dart:io'; + import 'package:flutter/gestures.dart'; import 'package:flutter/material.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'; class ProfileScreen extends StatefulWidget { @override @@ -8,9 +14,13 @@ class ProfileScreen extends StatefulWidget { } class _ProfileScreenState extends State { + final _pb = PocketBase('https://pb.venbait.in'); + // final _pb = PocketBase('http://127.0.0.1:8090'); 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; @@ -24,15 +34,72 @@ class _ProfileScreenState extends State { ]; String? _selectedCountry; bool _termsAccepted = false; + final _picker = ImagePicker(); + File? _profileImage; // Regular expression to validate Full Name (no special characters) final RegExp _nameRegExp = RegExp(r"^[a-zA-Z\s]+$"); + @override + void initState() { + super.initState(); + _fetchUserData(); // Call the function to fetch user data + } + + @override + void dispose() { + _fullNameController.dispose(); + _dateController.dispose(); + super.dispose(); + } + + void _fetchUserData() async { + try { + final userId = 'tsgehyvgy6owypj'; + final adminAuth = await _pb.admins + .authWithPassword('pb@venbainfotech.com', 'pb@venbainfotech.com'); + final adminToken = adminAuth.token; + final response = await http.get( + Uri.parse( + 'https://pb.venbait.in/api/collections/users/records/$userId'), + headers: { + 'Authorization': 'Bearer $adminToken', // Add token to header + 'Content-Type': 'application/json', + }, + ); + + if (response.statusCode == 200) { + final data = jsonDecode(response.body); + setState(() { + _usernameController.text = data['username']; + _emailController.text = data['email']; + }); + } else { + print('Failed to fetch user data: ${response.body}'); + } + } catch (error) { + print('Failed to fetch user data: $error'); + } + } + + 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 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 @@ -82,6 +149,97 @@ class _ProfileScreenState extends State { }); } + void showConfirmationDialog(BuildContext context) async { + final result = await showDialog( + context: context, + builder: (context) => const ConfirmationDialog(), + ); + if (result == true) { + if (_formKey.currentState?.validate() ?? false) { + try { + String userID = 'tsgehyvgy6owypj'; + // Retrieve data from text fields and other inputs + String fullName = _fullNameController.text; + // String username = 'users55538'; + // String email = 'surendarsuri30@gmail.com'; + String dateOfBirth = _dateController.text; // in DD/MM/YYYY format + String countryRegion = _selectedCountry ?? ''; + bool termsAccepted = _termsAccepted; + + // Parse the date from dd/MM/yyyy format and convert to yyyy-MM-dd + // Parse the date from dd/MM/yyyy format and format to yyyy-MM-dd + DateTime dob = DateFormat('dd/MM/yyyy').parse(dateOfBirth); + String formattedDob = DateFormat('yyyy-MM-dd').format(dob); + + // Prepare form data + final request = http.MultipartRequest( + 'PATCH', + Uri.parse( + 'https://pb.venbait.in/api/collections/users/records/$userID'), // replace with actual user ID + ); + + // Set the fields for the user profile + request.fields['full_name'] = fullName; + // request.fields['username'] = username; + // request.fields['email'] = email; + request.fields['dob'] = formattedDob; // ensure correct format + request.fields['country_region'] = countryRegion; + request.fields['terms_accepted'] = termsAccepted.toString(); + + // Add image file if selected + if (_profileImage != null) { + request.files.add(await http.MultipartFile.fromPath( + 'avatar', _profileImage!.path)); + } + final adminAuth = await _pb.admins + .authWithPassword('pb@venbainfotech.com', 'pb@venbainfotech.com'); + final adminToken = adminAuth.token; + // Add PocketBase auth headers if needed (for authenticated requests) + request.headers['Authorization'] = 'Bearer ${adminToken}'; + + print(request); + + // Send the request + final response = await request.send(); + + if (response.statusCode == 200) { + print(response.statusCode); + _resetFormFields(); + Navigator.pushNamed(context, 'home'); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text("Profile updated successfully!"))); + } else { + // Log the response body for better debugging + final responseBody = await response.stream.bytesToString(); + ScaffoldMessenger.of(context).showSnackBar(SnackBar( + content: Text( + "Failed to update profile: ${response.reasonPhrase}, Body: $responseBody"))); + } + } catch (error) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text("Failed to update profile: $error"))); + } + } + } + } + + void _resetFormFields() { + print('reset'); + setState(() { + // Reset all text controllers + _fullNameController.clear(); + _dateController.clear(); + _selectedCountry = null; + _termsAccepted = false; + + // Reset profile image + _profileImage = null; + + // Reset form validation state + _formKey.currentState?.reset(); + }); + } + @override Widget build(BuildContext context) { return Scaffold( @@ -121,18 +279,22 @@ class _ProfileScreenState extends State { children: [ CircleAvatar( radius: 50, - backgroundImage: - AssetImage("assets/edit_profile/profile.png"), - // Replace with actual image URL + backgroundImage: _profileImage != null + ? FileImage(_profileImage!) + : AssetImage("assets/edit_profile/profile.png") + as ImageProvider, child: Align( alignment: Alignment.bottomRight, - child: CircleAvatar( - radius: 15, - backgroundColor: Colors.white, - child: Icon( - Icons.camera_alt, - size: 15, - color: Colors.grey, + 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, + ), ), ), ), @@ -141,46 +303,56 @@ class _ProfileScreenState extends State { Row( mainAxisAlignment: MainAxisAlignment.start, children: [ - Text("User Name", - style: TextStyle(fontSize: 16, fontWeight: FontWeight.w500), + Text( + "User Name", + style: + TextStyle(fontSize: 16, fontWeight: FontWeight.w500), ), ], ), SizedBox(height: 10), TextFormField( + controller: _usernameController, decoration: InputDecoration( hintText: 'Mohammad Hassan', hintStyle: TextStyle(color: Colors.grey), border: OutlineInputBorder( borderRadius: BorderRadius.circular(8), - ),enabled: false, + ), + enabled: false, ), ), SizedBox(height: 10), Row( mainAxisAlignment: MainAxisAlignment.start, children: [ - Text("E-mail", - style: TextStyle(fontSize: 16, fontWeight: FontWeight.w500), + Text( + "E-mail", + style: + TextStyle(fontSize: 16, fontWeight: FontWeight.w500), ), ], ), SizedBox(height: 10), TextFormField( + controller: _emailController, decoration: InputDecoration( hintText: 'mohammad.hassan@fcsc.gov.ae', hintStyle: TextStyle(color: Colors.grey), border: OutlineInputBorder( borderRadius: BorderRadius.circular(8), - ),enabled: false, + ), + enabled: false, ), ), SizedBox(height: 10), Row( mainAxisAlignment: MainAxisAlignment.start, children: [ - Text("Full Name", - style: TextStyle(fontSize: 16, fontWeight: FontWeight.w500), + Text( + "Full Name", + style: + TextStyle(fontSize: 16, fontWeight: FontWeight.w500), ), ], ), @@ -210,13 +382,14 @@ class _ProfileScreenState extends State { Row( mainAxisAlignment: MainAxisAlignment.start, children: [ - Text("Date of Birth*", - style: TextStyle(fontSize: 16, fontWeight: FontWeight.w500), + Text( + "Date of Birth*", + style: + TextStyle(fontSize: 16, fontWeight: FontWeight.w500), ), ], ), SizedBox(height: 10), - TextFormField( controller: _dateController, decoration: InputDecoration( @@ -226,18 +399,18 @@ class _ProfileScreenState extends State { hintText: 'Select your Date of Birth', suffixIcon: const Icon(Icons.arrow_drop_down_sharp), ), - readOnly: true, onTap: _pickDate, validator: _validateDob, ), - SizedBox(height: 10), Row( mainAxisAlignment: MainAxisAlignment.start, children: [ - Text("Country/Region*", - style: TextStyle(fontSize: 16, fontWeight: FontWeight.w500), + Text( + "Country/Region*", + style: + TextStyle(fontSize: 16, fontWeight: FontWeight.w500), ), ], ), @@ -252,9 +425,9 @@ class _ProfileScreenState extends State { ), items: _countries .map((item) => DropdownMenuItem( - value: item, - child: Text(item), - )) + value: item, + child: Text(item), + )) .toList(), onChanged: (String? newValue) { setState(() { @@ -275,7 +448,9 @@ class _ProfileScreenState extends State { mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: [ - SizedBox(height: 10,), + SizedBox( + height: 10, + ), Text.rich( TextSpan( text: 'I agree to the ', @@ -324,14 +499,22 @@ class _ProfileScreenState extends State { ], ), SizedBox(height: 20), - - Center(child: Text("Change Password",style: TextStyle(color: Colors.blue,decoration: TextDecoration.underline),),), + Center( + child: Text( + "Change Password", + style: TextStyle( + color: Colors.blue, + decoration: TextDecoration.underline), + ), + ), SizedBox(height: 20), ElevatedButton.icon( onPressed: () { if (_formKey.currentState?.validate() ?? false) { + _formKey.currentState?.save(); if (_termsAccepted) { _formKey.currentState?.save(); + // _confirmSaveProfile(); showConfirmationDialog(context); } else { ScaffoldMessenger.of(context).showSnackBar( @@ -366,33 +549,55 @@ class _ProfileScreenState extends State { } } -//Alert Dialog box - class ConfirmationDialog extends StatelessWidget { const ConfirmationDialog({Key? key}) : super(key: key); - @override Widget build(BuildContext context) { return AlertDialog( content: Column( mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: const [ + children: [ Text( - 'Are you sure you want to save this page?', - style: TextStyle( + "Are you sure you want to save this page?", + style: const TextStyle( fontSize: 14, - color: Colors.black54, + color: Color(0xFF898C81), + fontFamily: 'Roboto', + fontWeight: FontWeight.w400, ), + textAlign: TextAlign.center, ), SizedBox(height: 8), - Text( - 'Once saved, you will not be able to change your Name and Date of birth.', - style: TextStyle( - fontSize: 14, - color: Colors.black54, + 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: "name", + style: + TextStyle(fontWeight: FontWeight.w700), // Bold for "name" + ), + TextSpan( + text: " or ", + ), + TextSpan( + text: "date of birth", + style: TextStyle( + fontWeight: FontWeight.w700), // Bold for "date of birth" + ), + TextSpan( + text: ".", + ), + ], ), - ), + textAlign: TextAlign.center, + ) ], ), shape: RoundedRectangleBorder( @@ -408,15 +613,18 @@ class ConfirmationDialog extends StatelessWidget { children: [ OutlinedButton( onPressed: () { - Navigator.of(context).pop(true); + 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 + ), // Set the border color here + ), + child: Text( + "Cancel", + style: TextStyle(color: Color(0xFF92722A)), ), - child: Text("Cancel",style: TextStyle(color: Color(0xFF92722A)),), ), ElevatedButton( onPressed: () => Navigator.of(context).pop(true), @@ -437,31 +645,10 @@ class ConfirmationDialog extends StatelessWidget { ], ), ), - SizedBox(height: 5,) + SizedBox( + height: 5, + ) ], ); } } - -// Usage example: -void showConfirmationDialog(BuildContext context) async { - final result = await showDialog( - context: context, - builder: (context) => const ConfirmationDialog(), - ); - - if (result == true) { - // Add your save logic here - } -} - - - - - - - - - - - diff --git a/lib/presentation/routes/drawer_routes/feedback_route.dart b/lib/presentation/routes/drawer_routes/feedback_route.dart index f60bd38b..ac97ff5b 100644 --- a/lib/presentation/routes/drawer_routes/feedback_route.dart +++ b/lib/presentation/routes/drawer_routes/feedback_route.dart @@ -1,98 +1,22 @@ -// import 'package:flutter/material.dart'; -// import 'package:flutter_hooks/flutter_hooks.dart'; -// import 'package:hooks_riverpod/hooks_riverpod.dart'; -// import 'package:webview_flutter/webview_flutter.dart'; - -// import 'package:uae_stat/domain/use_cases/language.dart'; -// import 'package:uae_stat/presentation/components/themed_app_bar.dart'; - -// class FeedbackRoute extends HookConsumerWidget { -// const FeedbackRoute({super.key}); - -// static const enUrl = -// 'https://fcsc.gov.ae/en-us/Pages/e-Participation/Contact-Us.aspx'; -// static const arUrl = -// 'https://fcsc.gov.ae/ar-ae/Pages/e-Participation/Contact-Us.aspx'; - -// @override -// Widget build(BuildContext context, WidgetRef ref) { -// final didLoadSuccessfully = useState(true); -// final url = context.translate(enUrl, arUrl); -// final ctl = WebViewController() -// ..setJavaScriptMode(JavaScriptMode.unrestricted) -// ..setBackgroundColor(Colors.transparent) -// ..setNavigationDelegate( -// NavigationDelegate( -// // onProgress: (int progress) {}, -// onPageStarted: (String url) => didLoadSuccessfully.value = null, -// onPageFinished: (String url) => didLoadSuccessfully.value = true, -// onWebResourceError: (WebResourceError error) => -// didLoadSuccessfully.value = null, -// onNavigationRequest: (NavigationRequest request) { -// if (!request.url.startsWith('https://fcsc.gov.ae/')) { -// return NavigationDecision.prevent; -// } -// return NavigationDecision.navigate; -// }, -// ), -// ) -// ..loadRequest( -// Uri.parse( -// url, -// ), -// ); -// final bodyContent = Stack( -// children: [ -// Center( -// child: didLoadSuccessfully.value == null -// ? const CircularProgressIndicator() -// : didLoadSuccessfully.value! -// ? const SizedBox.shrink() -// : const Text('An error occurred.'), -// ), -// WebViewWidget(controller: ctl), -// ], -// ); -// final body = Column( -// children: [ -// ThemedAppBar( -// titleText: context.translate( -// 'Feedback', -// 'تعليق', -// ), -// ), -// Expanded( -// child: bodyContent, -// ), -// ], -// ); -// return ColoredBox( -// color: Colors.white, -// child: SafeArea( -// bottom: false, -// child: body, -// ), -// ); -// } -// } - -import 'package:excel/excel.dart'; +import 'package:external_repos/external_repos.dart'; import 'package:flutter/material.dart'; import 'package:flutter_rating_bar/flutter_rating_bar.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; +import 'package:intl/intl.dart'; +import 'package:pocketbase/pocketbase.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:uae_stat/domain/use_cases/language.dart'; +import 'package:uae_stat/infrastructure/services/packages/go_router.dart'; import 'package:uae_stat/presentation/components/my_drawer.dart'; - -// class FeedbackPage extends StatelessWidget { -// const FeedbackPage({super.key}); - -// @override -// Widget build(BuildContext context) { -// return const Scaffold( -// body: Center( -// child: Text('Feedback'), -// ), -// ); -// } -// } +import 'package:mailer/mailer.dart'; +import 'package:mailer/smtp_server.dart'; +import 'package:uae_stat/presentation/components/space.dart'; +import '../../../config/my_theme.dart'; +import '../../../domain/use_cases/preferences_use_case.dart'; +import '../../components/lang_toggle.dart'; +import '../../components/my_bottom_nav_bar.dart'; +import '../../components/my_toggle.dart'; class FeedbackForm extends StatefulWidget { const FeedbackForm({super.key}); @@ -102,111 +26,438 @@ class FeedbackForm extends StatefulWidget { } class _FeedbackFormState extends State { + final _pb = + PocketBase('https://pb.venbait.in'); // Initialize PocketBase client + // final _pb = PocketBase('http://127.0.0.1:8090'); // Initialize PocketBase client + + final TextEditingController _feedbackController = TextEditingController(); + double _easeOfUseRating = 0; double _qualityRating = 0; double _designRating = 0; double _redundancyRating = 0; + int? _selectedEmojiIndex; + bool _isFeedbackSubmitted = false; // Track if feedback was submitted + bool _isFeedbackFailed = false; // New flag for failed submission + bool _isSmileySelected = true; // Track if smiley is selected + dynamic configEmail; + + 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, + }, + ]; + + static const int _characterLimit = 1200; + final RegExp _allowedCharacters = RegExp(r'^[a-zA-Z0-9 .,!?-]*$'); + bool _hasError = false; + String _errorMessage = ''; + + @override + void initState() { + super.initState(); + _loadFeedbackText(); + _feedbackController.addListener(_handleTextChange); + fetchEmailConfiguration(); + } + + Future _saveFeedbackText( + int emojiIndex, String ratingKey, double ratingValue) async { + final prefs = await SharedPreferences.getInstance(); + await prefs.setString('feedbackText', _feedbackController.text); + await prefs.setInt('selected_emoji_index', emojiIndex); + await prefs.setDouble(ratingKey, ratingValue); + } + + Future _saveAllRatings() async { + final prefs = await SharedPreferences.getInstance(); + await prefs.setDouble('ease_of_use_rating', _easeOfUseRating); + await prefs.setDouble('quality_rating', _qualityRating); + await prefs.setDouble('design_rating', _designRating); + await prefs.setDouble('redundancy_rating', _redundancyRating); + } + + Future _loadFeedbackText() async { + final prefs = await SharedPreferences.getInstance(); + final feedbackText = prefs.getString('feedbackText') ?? ''; + setState(() { + _feedbackController.text = feedbackText; + }); + + final savedIndex = prefs.getInt('selected_emoji_index'); + if (savedIndex != null) { + setState(() { + _selectedEmojiIndex = savedIndex; + _isSmileySelected = true; // Indicating the user selected an emoji + }); + } + + setState(() { + _easeOfUseRating = prefs.getDouble('ease_of_use_rating') ?? 0; + _qualityRating = prefs.getDouble('quality_rating') ?? 0; + _designRating = prefs.getDouble('design_rating') ?? 0; + _redundancyRating = prefs.getDouble('redundancy_rating') ?? 0; + }); + } + + // Remove feedback text from local storage + Future _removeFeedbackText() async { + final prefs = await SharedPreferences.getInstance(); + await prefs.remove('feedbackText'); + await prefs.remove('selected_emoji_index'); + await prefs.remove('ease_of_use_rating'); + await prefs.remove('quality_rating'); + await prefs.remove('design_rating'); + await prefs.remove('redundancy_rating'); + } + + void _handleTextChange() { + String currentText = _feedbackController.text; + + // Enforce character limit and truncate excess on paste + if (currentText.length > _characterLimit) { + _feedbackController.text = currentText.substring(0, _characterLimit); + _feedbackController.selection = TextSelection.fromPosition( + TextPosition(offset: _feedbackController.text.length), + ); + } + + // Validate characters + if (!_allowedCharacters.hasMatch(currentText)) { + setState(() { + _hasError = true; + _errorMessage = "Invalid Characters"; + }); + } else { + setState(() { + _hasError = false; + _errorMessage = ''; + }); + } + } @override Widget build(BuildContext context) { return Scaffold( drawer: const MyDrawer(), appBar: AppBar( - title: const Text('Feedback Form'), + backgroundColor: const Color(0xFFf8f9ff), + title: Text(context.translate('Feedback Form', 'نموذج الملاحظات')), + actions: [ + Align( + alignment: AlignmentDirectional.topEnd, + child: LangToggle( + onSaveFeedback: _saveFeedbackText, + onLoadFeedback: _loadFeedbackText, + selectedEmojiIndex: _selectedEmojiIndex ?? 0, + easeOfUseRating: _easeOfUseRating, + qualityRating: _qualityRating, + designRating: _designRating, + redundancyRating: _redundancyRating, + ), + ), + ], ), body: Padding( padding: const EdgeInsets.all(16.0), child: SingleChildScrollView( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Text( - 'Do you have a suggestion or had any problem? Let us know.', - style: TextStyle(fontSize: 18), - ), - const SizedBox(height: 20), - const Text( - 'How was your experience with us today?', - style: TextStyle(fontSize: 16), - ), - const SizedBox(height: 10), - Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - _buildEmojiButton( - Icons.sentiment_very_dissatisfied, - 'Terrible', - ), - _buildEmojiButton(Icons.sentiment_dissatisfied, 'Bad'), - _buildEmojiButton(Icons.sentiment_neutral, 'Okay'), - _buildEmojiButton(Icons.sentiment_satisfied, 'Good'), - _buildEmojiButton(Icons.sentiment_very_satisfied, 'Amazing'), - ], - ), - const SizedBox(height: 30), - const Text( - 'How good did we do in these aspects?', - style: TextStyle(fontSize: 16), - ), - const SizedBox(height: 20), - _buildRatingRow('Ease of use', _easeOfUseRating, (rating) { - setState(() { - _easeOfUseRating = rating; - }); - }), - _buildRatingRow('Quality', _qualityRating, (rating) { - setState(() { - _qualityRating = rating; - }); - }), - _buildRatingRow('Design', _designRating, (rating) { - setState(() { - _designRating = rating; - }); - }), - _buildRatingRow('Redundant', _redundancyRating, (rating) { - setState(() { - _redundancyRating = rating; - }); - }), - const SizedBox(height: 20), - const TextField( - decoration: InputDecoration( - hintText: 'Tell us how we can improve', - border: OutlineInputBorder( - borderSide: BorderSide(color: Colors.blueAccent)), - ), - maxLines: 5, - ), - const SizedBox(height: 20), - Center( - child: ElevatedButton( - onPressed: () { - // Handle the submit action - }, - child: const Text('Submit'), - ), - ), - ], - ), + child: _isFeedbackSubmitted + ? _buildThankYouMessage() + : _isFeedbackFailed + ? _buildFailureMessage() + : _buildFeedbackForm(), ), ), ); } - Widget _buildEmojiButton(IconData icon, String label) { + Widget _buildFeedbackForm() { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + context.translate( + 'Do you have a suggestion or had any problem? Let us know.', + 'هل لديك اقتراح أو واجهت أي مشكلة؟ اسمحوا لنا أن نعرف.', + ), + style: const TextStyle( + fontSize: 18, + color: Color(0xFF898C81), + fontFamily: 'Roboto', + fontWeight: FontWeight.w700, + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 20), + Text( + context.translate('How was your experience with us today?', + 'كيف كانت تجربتك معنا اليوم؟'), + style: const TextStyle( + fontSize: 18, + fontFamily: 'Roboto', + fontWeight: FontWeight.w400, + color: Color(0xFF898C81), + ), + ), + const SizedBox(height: 10), + Container( + decoration: BoxDecoration( + border: Border.all( + color: _isSmileySelected ? Colors.transparent : Colors.red, + width: 1.5, + ), + borderRadius: BorderRadius.circular(8), + ), + padding: const EdgeInsets.all(8), + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: List.generate(_emojiOptions.length, (index) { + return _buildEmojiButton( + icon: _emojiOptions[index]["icon"], + label: _emojiOptions[index]["label"], + index: index, + ); + }), + ), + if (!_isSmileySelected) // Show the error message if no smiley is selected + Padding( + padding: const EdgeInsets.only(top: 8.0), + child: Text( + context.translate( + 'Please rate your experience before submitting your feedback.', + 'يرجى تقييم تجربتك قبل تقديم ملاحظاتك.', + ), + style: TextStyle(color: Colors.red, fontSize: 14), + ), + ), + ], + ), + ), + const SizedBox(height: 30), + Text( + context.translate('How good did we do in these aspects?', + 'ما مدى جودة ما قمنا به في هذه الجوانب؟'), + style: const TextStyle( + fontSize: 18, + fontFamily: 'Roboto', + fontWeight: FontWeight.w400, + color: Color(0xFF898C81), + ), + ), + const SizedBox(height: 20), + _buildRatingRow( + context.translate('Ease of use', 'سهولة الاستخدام'), + _easeOfUseRating, + (rating) { + setState(() { + _easeOfUseRating = rating; + }); + }, + ), + _buildRatingRow( + context.translate('Quality', 'جودة'), + _qualityRating, + (rating) { + setState(() { + _qualityRating = rating; + }); + }, + ), + _buildRatingRow( + context.translate('Design', 'تصميم'), + _designRating, + (rating) { + setState(() { + _designRating = rating; + }); + }, + ), + _buildRatingRow( + context.translate('Redundant', 'متكرر'), + _redundancyRating, + (rating) { + setState(() { + _redundancyRating = rating; + }); + }, + ), + const SizedBox(height: 20), + TextField( + controller: _feedbackController, + minLines: 5, // Start with 5 lines + maxLines: null, // Allows the field to expand automatically + maxLength: _characterLimit, + decoration: InputDecoration( + hintText: context.translate( + 'Tell us how we can improve', 'أخبرنا كيف يمكننا تحسين'), + border: OutlineInputBorder( + borderSide: BorderSide( + color: _hasError ? Colors.red : Color(0xFF7296BE), + ), + ), + errorText: + _hasError ? _errorMessage : null, // Show error below field + ), + onChanged: (text) { + // Trigger re-validation and character limit on each change + _handleTextChange(); + }, + ), + const SizedBox(height: 20), + Center( + child: ElevatedButton( + onPressed: _submitFeedback, + style: ButtonStyle( + shape: WidgetStatePropertyAll( + RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), + ), + padding: const WidgetStatePropertyAll( + EdgeInsets.symmetric(vertical: 5.5), + ), + textStyle: WidgetStatePropertyAll( + TextStyle( + fontFamily: context.translate( + 'Roboto', + 'NotoKufi', + ), + fontSize: 16, + fontWeight: FontWeight.w600, + ), + ), + backgroundColor: WidgetStatePropertyAll( + MyTheme.topicColor(IndicatorTopic.environment), + ), + foregroundColor: const WidgetStatePropertyAll( + Colors.white, + ), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + context.translate( + 'Submit', + 'يُقدِّم', + ), + ), + 6.horizontalSpace, + const Icon(Icons.chevron_right_outlined), + ], + ), + ), + ), + ], + ); + } + + Widget _buildThankYouMessage() { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Icon(Icons.thumb_up, size: 80, color: Color(0xFF7DAFBC)), + const SizedBox(height: 20), + Text( + "Thank You", + style: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold), + ), + const SizedBox(height: 10), + const Text( + "For your valuable feedback, we truly appreciate your input!", + style: TextStyle(fontSize: 16), + textAlign: TextAlign.center, + ), + const SizedBox(height: 30), + ElevatedButton( + onPressed: () => context + .go('/${context.language}/${BottomNavBarItem.home.routePath}'), + child: const Text("Go to Home"), + ), + ], + ), + ); + } + + // New widget for failure message + Widget _buildFailureMessage() { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Icon(Icons.error, size: 80, color: Colors.red), + const SizedBox(height: 20), + const Text("Failed to Share Feedback", + style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold)), + const SizedBox(height: 10), + const Text("Your feedback could not be submitted. Please try again.", + style: TextStyle(fontSize: 16), textAlign: TextAlign.center), + const SizedBox(height: 30), + ElevatedButton( + onPressed: () { + setState(() { + _isFeedbackFailed = false; // Reset failure state + }); + }, + child: const Text("Retry"), + ), + ], + ), + ); + } + + Widget _buildEmojiButton({ + required IconData icon, + required String label, + required int index, + }) { + bool isSelected = _selectedEmojiIndex == index; + return Column( children: [ IconButton( icon: Icon(icon, size: 40), - color: Colors.amber, + color: isSelected ? Colors.green : Colors.amber, onPressed: () { - // Handle the emoji rating + setState(() { + _selectedEmojiIndex = index; + _isSmileySelected = true; // Hide error when a smiley is selected + }); }, ), Text( label, - style: TextStyle(fontWeight: FontWeight.bold), + style: TextStyle( + fontWeight: FontWeight.w400, + color: isSelected ? Colors.green : Color(0xFF000000), + ), ), ], ); @@ -220,7 +471,13 @@ class _FeedbackFormState extends State { return Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Text(label, style: const TextStyle(fontSize: 16)), + Text(label, + style: const TextStyle( + fontSize: 14, + fontFamily: 'Roboto', + fontWeight: FontWeight.w400, + color: Color(0xFF898C81), + )), RatingBar.builder( initialRating: rating, minRating: 1, @@ -233,8 +490,259 @@ class _FeedbackFormState extends State { color: Colors.amber, ), onRatingUpdate: onRatingUpdate, + unratedColor: Color(0xFF8E8E8E), ), ], ); } + + Future _submitFeedback() async { + if (_selectedEmojiIndex == null) { + setState(() { + _isSmileySelected = false; + }); + return; + } else { + setState(() { + _isSmileySelected = true; + }); + } + + if (_hasError) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text("Please correct the errors before submitting.")), + ); + return; + } + + final feedbackData = { + "ease_of_use": _easeOfUseRating, + "quality": _qualityRating, + "design": _designRating, + "redundancy": _redundancyRating, + "emoji_rating": _emojiOptions[_selectedEmojiIndex!] + ["label"], // Emoji rating + "feedback": _feedbackController.text, + }; + + try { + final adminAuth = await _pb.admins + .authWithPassword('pb@venbainfotech.com', 'pb@venbainfotech.com'); + final adminToken = adminAuth.token; + final response = await _pb + .collection('feedback') + .create(body: feedbackData, headers: {'Authorization': adminToken}); + if (response != null) { + final createdTime = response.created; + // Send email + + await sendFeedbackEmail(feedbackData, createdTime); + await _removeFeedbackText(); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text("Feedback submitted successfully!")), + ); + setState(() { + _isFeedbackSubmitted = true; + _isFeedbackFailed = false; + _resetFeedbackForm(); + }); + } + } catch (e) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text("Failed to submit feedback: $e")), + ); + setState(() { + _isFeedbackFailed = true; // Update state to show failure message + }); + } + } + + void _resetFeedbackForm() { + _easeOfUseRating = 0; + _qualityRating = 0; + _designRating = 0; + _redundancyRating = 0; + _selectedEmojiIndex = null; + _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); + + // Assuming the server timezone as UTC, you can append as needed + String submissionTime = "$formattedDateTime "; + + print("Formatted Date (UTC): $formattedDate"); + 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: $submissionTime + + 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() { + _feedbackController.dispose(); + super.dispose(); + } +} + +class LangToggle extends ConsumerWidget { + final Future Function( + int emojiIndex, String ratingKey, double ratingValue) onSaveFeedback; + final Future Function() onLoadFeedback; + final int selectedEmojiIndex; + final double easeOfUseRating; + final double qualityRating; + final double designRating; + final double redundancyRating; + + const LangToggle({ + required this.onSaveFeedback, + required this.onLoadFeedback, + required this.selectedEmojiIndex, + required this.easeOfUseRating, + required this.qualityRating, + required this.designRating, + required this.redundancyRating, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final isOn = context.language == LanguageLocale.enUS; + + return MyToggle( + isOn: isOn, + onTap: () async { + await onSaveFeedback( + selectedEmojiIndex, + 'ease_of_use_rating', + easeOfUseRating, + ); + await onSaveFeedback( + selectedEmojiIndex, + 'quality_rating', + qualityRating, + ); + await onSaveFeedback( + selectedEmojiIndex, + 'design_rating', + designRating, + ); + await onSaveFeedback( + selectedEmojiIndex, + 'redundancy_rating', + redundancyRating, + ); + + final languageAfter = isOn ? LanguageLocale.arAE : LanguageLocale.enUS; + final grs = GoRouterState.of(context); + grs.pathParameters['locale'] = languageAfter.toString(); + context.go(grs.pathWithParameters); + + ref.read(preferencesUseCaseProvider.notifier).updatePreferences( + (prefs) => prefs.copyWith(language: languageAfter), + ); + + await onLoadFeedback(); // Reload feedback text after toggling language + }, + knobTextWhenOn: 'ع', + knobTextWhenOff: 'EN', + pathColorWhenOn: Colors.grey.shade300, + pathColorWhenOff: Colors.grey.shade300, + ); + } } diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 55bcc4ee..89ab99dd 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -5,6 +5,7 @@ import FlutterMacOS import Foundation +import file_selector_macos import flutter_secure_storage_macos import path_provider_foundation import share_plus @@ -13,6 +14,7 @@ import video_player_avfoundation import webview_flutter_wkwebview func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin")) FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin")) diff --git a/pubspec.lock b/pubspec.lock index 2cf3aa30..63d94ab6 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -341,6 +341,38 @@ packages: url: "https://pub.dev" source: hosted version: "7.0.1" + file_selector_linux: + dependency: transitive + description: + name: file_selector_linux + sha256: b2b91daf8a68ecfa4a01b778a6f52edef9b14ecd506e771488ea0f2e0784198b + url: "https://pub.dev" + source: hosted + version: "0.9.3+1" + file_selector_macos: + dependency: transitive + description: + name: file_selector_macos + sha256: "271ab9986df0c135d45c3cdb6bd0faa5db6f4976d3e4b437cf7d0f258d941bfc" + url: "https://pub.dev" + source: hosted + version: "0.9.4+2" + file_selector_platform_interface: + dependency: transitive + description: + name: file_selector_platform_interface + sha256: a3994c26f10378a039faa11de174d7b78eb8f79e4dd0af2a451410c1a5c3f66b + url: "https://pub.dev" + source: hosted + version: "2.6.2" + file_selector_windows: + dependency: transitive + description: + name: file_selector_windows + sha256: "8f5d2f6590d51ecd9179ba39c64f722edc15226cc93dcc8698466ad36a4a85a4" + url: "https://pub.dev" + source: hosted + version: "0.9.3+3" fixnum: dependency: transitive description: @@ -407,6 +439,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.4.2" + flutter_plugin_android_lifecycle: + dependency: transitive + description: + name: flutter_plugin_android_lifecycle + sha256: "9b78450b89f059e96c9ebb355fa6b3df1d6b330436e0b885fb49594c41721398" + url: "https://pub.dev" + source: hosted + version: "2.0.23" flutter_rating_bar: dependency: "direct main" description: @@ -601,6 +641,70 @@ packages: url: "https://pub.dev" source: hosted version: "4.3.0" + image_picker: + dependency: "direct main" + description: + name: image_picker + sha256: "021834d9c0c3de46bf0fe40341fa07168407f694d9b2bb18d532dc1261867f7a" + url: "https://pub.dev" + source: hosted + version: "1.1.2" + image_picker_android: + dependency: transitive + description: + name: image_picker_android + sha256: "8faba09ba361d4b246dc0a17cb4289b3324c2b9f6db7b3d457ee69106a86bd32" + url: "https://pub.dev" + source: hosted + version: "0.8.12+17" + image_picker_for_web: + dependency: transitive + description: + name: image_picker_for_web + sha256: "717eb042ab08c40767684327be06a5d8dbb341fe791d514e4b92c7bbe1b7bb83" + url: "https://pub.dev" + source: hosted + version: "3.0.6" + image_picker_ios: + dependency: transitive + description: + name: image_picker_ios + sha256: "4f0568120c6fcc0aaa04511cb9f9f4d29fc3d0139884b1d06be88dcec7641d6b" + url: "https://pub.dev" + source: hosted + version: "0.8.12+1" + image_picker_linux: + dependency: transitive + description: + name: image_picker_linux + sha256: "4ed1d9bb36f7cd60aa6e6cd479779cc56a4cb4e4de8f49d487b1aaad831300fa" + url: "https://pub.dev" + source: hosted + version: "0.2.1+1" + image_picker_macos: + dependency: transitive + description: + name: image_picker_macos + sha256: "3f5ad1e8112a9a6111c46d0b57a7be2286a9a07fc6e1976fdf5be2bd31d4ff62" + url: "https://pub.dev" + source: hosted + version: "0.2.1+1" + image_picker_platform_interface: + dependency: transitive + description: + name: image_picker_platform_interface + sha256: "9ec26d410ff46f483c5519c29c02ef0e02e13a543f882b152d4bfd2f06802f80" + url: "https://pub.dev" + source: hosted + version: "2.10.0" + image_picker_windows: + dependency: transitive + description: + name: image_picker_windows + sha256: "6ad07afc4eb1bc25f3a01084d28520496c4a3bb0cb13685435838167c9dcedeb" + url: "https://pub.dev" + source: hosted + version: "0.2.1+1" injectable: dependency: "direct main" description: @@ -713,6 +817,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.1.2-main.4" + mailer: + dependency: "direct main" + description: + name: mailer + sha256: "21fde1497c79f402cb5fa7c50abd58927d360139e492546c941ee10767684fac" + url: "https://pub.dev" + source: hosted + version: "6.2.0" marquee: dependency: "direct main" description: @@ -749,10 +861,10 @@ packages: dependency: transitive description: name: mime - sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" + sha256: "801fd0b26f14a4a58ccb09d5892c3fbdeff209594300a542492cf13fba9d247a" url: "https://pub.dev" source: hosted - version: "2.0.0" + version: "1.0.6" nested: dependency: transitive description: @@ -964,10 +1076,10 @@ packages: dependency: "direct main" description: name: share_plus - sha256: "3af2cda1752e5c24f2fc04b6083b40f013ffe84fb90472f30c6499a9213d5442" + sha256: "9c9bafd4060728d7cdb2464c341743adbd79d327cb067ec7afb64583540b47c8" url: "https://pub.dev" source: hosted - version: "10.1.1" + version: "10.1.2" share_plus_platform_interface: dependency: transitive description: @@ -980,10 +1092,10 @@ packages: dependency: "direct main" description: name: shared_preferences - sha256: "746e5369a43170c25816cc472ee016d3a66bc13fcf430c0bc41ad7b4b2922051" + sha256: "95f9997ca1fb9799d494d0cb2a780fd7be075818d59f00c43832ed112b158a82" url: "https://pub.dev" source: hosted - version: "2.3.2" + version: "2.3.3" shared_preferences_android: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 58e853e9..d6542470 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -52,6 +52,8 @@ dependencies: shared_preferences: ^2.2.3 flutter_rating_bar: ^4.0.1 provider: ^6.1.2 + mailer: ^6.2.0 + image_picker: ^1.1.2 dependency_overrides: fading_edge_scrollview: ^4.1.1