diff --git a/lib/main.dart b/lib/main.dart index ca49a9f..5da1123 100755 --- a/lib/main.dart +++ b/lib/main.dart @@ -90,7 +90,7 @@ class _SplashScreenState extends State { Future tokenRedirectLogic( BuildContext context, GoRouterState state) async { print('ABCDEFGH'); - const allowedWithoutToken = [ + const guestRoutes = [ '/login', '/verify', '/mailVerify', @@ -98,17 +98,17 @@ Future tokenRedirectLogic( '/splash', ]; - final location = state.uri.toString(); - - // ✅ Allow login, verify, splash without token - if (allowedWithoutToken.contains(location)) return null; - - // ✅ Check if token exists - // final hasToken = await TokenService.hasValidToken(); - // if (!hasToken) return '/login'; final hasToken = await TokenService.hasValidToken(); print('hasToken : $hasToken'); - if (!hasToken) { + final location = state.matchedLocation; + + // ✅ If user is already logged in, never allow returning to OTP/login screens. + if (hasToken && guestRoutes.contains(location)) { + return '/home'; + } + + // ✅ If not logged in and trying to access a protected route, force login. + if (!hasToken && !guestRoutes.contains(location)) { print('!!!!hasToken : $hasToken'); await SessionManager().clear(); // Show toast once diff --git a/lib/pages/changePassword.dart b/lib/pages/changePassword.dart index 3b0439a..e416647 100755 --- a/lib/pages/changePassword.dart +++ b/lib/pages/changePassword.dart @@ -28,72 +28,84 @@ class changesPassword extends StatefulWidget { } class _changesPasswordState extends State { - final TextEditingController oldPasswordController = TextEditingController(); - final TextEditingController newPasswordController = TextEditingController(); - final TextEditingController confirmPasswordController = TextEditingController(); - final _formKey = GlobalKey(); - dynamic _preToken; - dynamic _postToken; - dynamic clientName; - dynamic clientLogo; + final TextEditingController oldPasswordController = TextEditingController(); + final TextEditingController newPasswordController = TextEditingController(); + final TextEditingController confirmPasswordController = TextEditingController(); + final _formKey = GlobalKey(); + dynamic _preToken; + dynamic _postToken; + dynamic clientName; + dynamic clientLogo; bool _isLoading = false; bool _obscureOldPassword = true; bool _obscureNewPassword = true; bool _obscureConfirmPassword = true; late SessionManager session; - bool hasMinLength = false; - bool hasUpperLower = false; - bool hasNumber = false; - bool hasSpecialChar = false; - bool get isPasswordValid => hasMinLength && hasUpperLower && hasNumber && hasSpecialChar; + bool hasMinLength = false; + bool hasUpperLower = false; + bool hasNumber = false; + bool hasSpecialChar = false; + bool get isPasswordValid => hasMinLength && hasUpperLower && hasNumber && hasSpecialChar; @override void initState() { super.initState(); } - void validatePassword(String password) { - setState(() { - hasMinLength = password.length >= 8; - hasUpperLower = RegExp(r'(?=.*[A-Za-z])').hasMatch(password); - hasNumber = RegExp(r'(?=.*\d)').hasMatch(password); - hasSpecialChar = RegExp(r'(?=.*[@$!%*#?&])').hasMatch(password); - }); - } + void validatePassword(String password) { + setState(() { + hasMinLength = password.length >= 8; + hasUpperLower = RegExp(r'(?=.*[A-Za-z])').hasMatch(password); + hasNumber = RegExp(r'(?=.*\d)').hasMatch(password); + hasSpecialChar = RegExp(r'(?=.*[@$!%*#?&])').hasMatch(password); + }); + } Future resetYourPassword() async { - final oldpassword = oldPasswordController.text.trim(); - final newPassword = newPasswordController.text.trim(); - final confirmPassword = confirmPasswordController.text.trim(); - try { - if (_formKey.currentState!.validate()) { - if (confirmPassword != newPassword) { - ToastHelper.showErrorToast(context, 'Passwords do not match'); - return; - } - setState(() { - _isLoading = true; - }); + final oldpassword = oldPasswordController.text.trim(); + final newPassword = newPasswordController.text.trim(); + final confirmPassword = confirmPasswordController.text.trim(); + try { + if (_formKey.currentState!.validate()) { + // Extra safety checks before calling the API + if (oldpassword == newPassword) { + ToastHelper.showErrorToast( + context, 'New password cannot be same as old password'); + return; + } + if (!isPasswordValid) { + ToastHelper.showErrorToast( + context, + 'Password must be 8+ characters with 1 letter, 1 number, and 1 special character'); + return; + } + if (confirmPassword != newPassword) { + ToastHelper.showErrorToast(context, 'Passwords do not match'); + return; + } + setState(() { + _isLoading = true; + }); - // Determine the API and the payload based on the visible field - String apiEndpoint = Environment.apiUrlEnrollment + 'changePassword'; - Map payload = { - 'email_id': widget.email, - 'client_id': widget.client_id, - 'old_password': oldpassword, - 'new_password': newPassword, - 'confirm_password': confirmPassword - }; + // Determine the API and the payload based on the visible field + String apiEndpoint = Environment.apiUrlEnrollment + 'changePassword'; + Map payload = { + 'email_id': widget.email, + 'client_id': widget.client_id, + 'old_password': oldpassword, + 'new_password': newPassword, + 'confirm_password': confirmPassword + }; - // var enteredMobileNumber = mobileController.text; - final response = await http.post( - Uri.parse(apiEndpoint), - body: json.encode(payload), - headers: { - HttpHeaders.contentTypeHeader: 'application/json', - 'APP-SIGNATURE': 'nhance-2025-signature-T8f6gV9k5fK2wf5V5c4vId3b0J8z6cAm2x8Y', - }, - ); + // var enteredMobileNumber = mobileController.text; + final response = await http.post( + Uri.parse(apiEndpoint), + body: json.encode(payload), + headers: { + HttpHeaders.contentTypeHeader: 'application/json', + 'APP-SIGNATURE': 'nhance-2025-signature-T8f6gV9k5fK2wf5V5c4vId3b0J8z6cAm2x8Y', + }, + ); if (response.statusCode == 200) { Map data = json.decode(response.body); @@ -163,26 +175,26 @@ class _changesPasswordState extends State { print('Error: $e'); } - // 🔹 Password validation - // if (password.isEmpty) { - // ToastHelper.showErrorToast(context, 'Please enter your password'); - // return; - // } - // if (password.length < 6) { - // ToastHelper.showErrorToast(context, 'Password must be at least 6 characters'); - // return; - // } - // if (!RegExp(r'^(?=.*[A-Z])(?=.*[0-9]).{6,}$').hasMatch(password)) { - // ToastHelper.showErrorToast(context, 'Include at least 1 uppercase letter and 1 number'); - // return; - // } - // - // // 🔹 Confirm password validation - // if (confirmPassword.isEmpty) { - // ToastHelper.showErrorToast(context, 'Please confirm your password'); - // return; - // } - } + // 🔹 Password validation + // if (password.isEmpty) { + // ToastHelper.showErrorToast(context, 'Please enter your password'); + // return; + // } + // if (password.length < 6) { + // ToastHelper.showErrorToast(context, 'Password must be at least 6 characters'); + // return; + // } + // if (!RegExp(r'^(?=.*[A-Z])(?=.*[0-9]).{6,}$').hasMatch(password)) { + // ToastHelper.showErrorToast(context, 'Include at least 1 uppercase letter and 1 number'); + // return; + // } + // + // // 🔹 Confirm password validation + // if (confirmPassword.isEmpty) { + // ToastHelper.showErrorToast(context, 'Please confirm your password'); + // return; + // } + } //Ends Login with UserName and Password @@ -227,7 +239,7 @@ class _changesPasswordState extends State { body: SingleChildScrollView( keyboardDismissBehavior: ScrollViewKeyboardDismissBehavior.onDrag, child: Container( - height: _size.height, + constraints: BoxConstraints(minHeight: _size.height), color: Colors.white, child: Stack( children: [ @@ -314,7 +326,7 @@ class _changesPasswordState extends State { // ), Container( margin: marginInsets, - alignment: Alignment.bottomCenter, + alignment: Alignment.topCenter, child: SingleChildScrollView( child: Form( key: _formKey, @@ -332,62 +344,37 @@ class _changesPasswordState extends State { mainAxisAlignment: MainAxisAlignment.center, children: [ - if (!Responsive.isMobile(context) && - !Responsive.isTablet(context)) + // Desktop-only: center "<" and logo in one row + if (Responsive.isDesktop(context)) Row( + mainAxisAlignment: + MainAxisAlignment.center, children: [ InkWell( - onTap: () { - context.go('/home'); - }, - child: Row( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - // if (!Responsive.isDesktop(context)) - Icon( - Icons.chevron_left, - color: Color(0xFF000000), - size: 30, - ), - SizedBox( - width: Responsive.isDesktop(context) - ? 0 - : 5), - ], - ), - ), - Expanded( - flex: 12, - child: Align( - alignment: Alignment - .topLeft, // ✅ Always top-left - child: _size.width <= 1100 - ? Image.asset( - 'assets/nhance_app_logo.png', - width: 150, - height: 150, - ) - : _size.width > 1100 - ? Image.asset( - 'assets/nhance_app_logo.png', - width: 150, - height: 150, - ) - : Image.asset( - 'assets/nhance_app_logo.png', - width: 150, - height: 150, - ), + onTap: () { + context.go('/home'); + }, + child: const Icon( + Icons.chevron_left, + color: Color(0xFF000000), + size: 30, ), ), + const SizedBox(width: 8), + Image.asset( + 'assets/nhance_app_logo.png', + width: 150, + height: 150, + // color: Colors.green, + ), ], ), SizedBox( height: Responsive.isDesktop(context) - ? _size.height * 0.1 + ? null : 10, ), - SizedBox(height: 10), + // SizedBox(height: 10), Container( margin: Responsive.isDesktop(context) ? EdgeInsets.symmetric( @@ -437,259 +424,278 @@ class _changesPasswordState extends State { height: 20, ), - Column( - children: [ - // 🔹 Password Field - Container( - height: 55, - margin: Responsive.isDesktop( - context) - ? const EdgeInsets + Column( + children: [ + // 🔹 Password Field + Container( + height: 55, + margin: Responsive.isDesktop( + context) + ? const EdgeInsets + .symmetric( + horizontal: 150) + : const EdgeInsets + .symmetric( + horizontal: 0), + decoration: BoxDecoration( + border: Border.all( + width: 1, + color: Colors.grey), + borderRadius: + BorderRadius.circular( + 10), + ), + child: TextFormField( + controller: + oldPasswordController, + obscureText: + _obscureOldPassword, + textAlignVertical: + TextAlignVertical + .center, + decoration: InputDecoration( + border: InputBorder.none, + hintText: + "Old Password", + contentPadding: + const EdgeInsets .symmetric( - horizontal: 150) - : const EdgeInsets - .symmetric( - horizontal: 0), - decoration: BoxDecoration( - border: Border.all( - width: 1, - color: Colors.grey), - borderRadius: - BorderRadius.circular( - 10), - ), - child: TextFormField( - controller: - oldPasswordController, - obscureText: - _obscureOldPassword, - textAlignVertical: - TextAlignVertical - .center, - decoration: InputDecoration( - border: InputBorder.none, - hintText: - "Old Password", - contentPadding: - const EdgeInsets - .symmetric( - horizontal: 10), - suffixIcon: IconButton( - icon: Icon( - _obscureOldPassword - ? Icons - .visibility_off - : Icons - .visibility, - color: Colors.grey, - ), - onPressed: () { - setState(() { - _obscureOldPassword = - !_obscureOldPassword; - }); - }, - ), + horizontal: 10), + suffixIcon: IconButton( + icon: Icon( + _obscureOldPassword + ? Icons + .visibility_off + : Icons + .visibility, + color: Colors.grey, ), - validator: (value) { - if (value == null || - value.isEmpty) { - return 'Please enter your old password'; - } - if (value.length < 6) { - return 'Password must be at least 6 characters'; - } - if (!RegExp( - r'^(?=.*[A-Z])(?=.*[0-9]).{6,}$') - .hasMatch(value)) { - return 'Include at least 1 uppercase letter and 1 number'; - } - return null; + onPressed: () { + setState(() { + _obscureOldPassword = + !_obscureOldPassword; + }); }, ), ), - const SizedBox(height: 10), - Container( - height: 55, - margin: Responsive.isDesktop(context) - ? const EdgeInsets.symmetric(horizontal: 150) - : const EdgeInsets.symmetric(horizontal: 0), - decoration: BoxDecoration( - border: Border.all(width: 1, color: Colors.grey), - borderRadius: BorderRadius.circular(10), - ), - child: TextFormField( - controller: newPasswordController, - obscureText: _obscureNewPassword, - onChanged: validatePassword, - textAlignVertical: TextAlignVertical.center, - decoration: InputDecoration( - border: InputBorder.none, - hintText: "New Password", - contentPadding: const EdgeInsets.symmetric(horizontal: 10), - suffixIcon: IconButton( - icon: Icon( - _obscureNewPassword ? Icons.visibility_off : Icons.visibility, - color: Colors.grey, - ), - onPressed: () { - setState(() { - _obscureNewPassword = !_obscureNewPassword; - }); - }, - ), + validator: (value) { + if (value == null || + value.isEmpty) { + return 'Please enter your old password'; + } + if (value.length < 8) { + return 'Password must be at least 8 characters'; + } + if (!RegExp( + r'^(?=.*[A-Z])(?=.*[0-9]).{6,}$') + .hasMatch(value)) { + return 'Include at least 1 uppercase letter and 1 number'; + } + return null; + }, + ), + ), + const SizedBox(height: 10), + Container( + height: 55, + margin: Responsive.isDesktop(context) + ? const EdgeInsets.symmetric(horizontal: 150) + : const EdgeInsets.symmetric(horizontal: 0), + decoration: BoxDecoration( + border: Border.all(width: 1, color: Colors.grey), + borderRadius: BorderRadius.circular(10), + ), + child: TextFormField( + controller: newPasswordController, + obscureText: _obscureNewPassword, + onChanged: validatePassword, + textAlignVertical: TextAlignVertical.center, + decoration: InputDecoration( + border: InputBorder.none, + hintText: "New Password", + contentPadding: const EdgeInsets.symmetric(horizontal: 10), + suffixIcon: IconButton( + icon: Icon( + _obscureNewPassword ? Icons.visibility_off : Icons.visibility, + color: Colors.grey, ), + onPressed: () { + setState(() { + _obscureNewPassword = !_obscureNewPassword; + }); + }, ), ), - const SizedBox(height: 8), - // 🔹 VALIDATION LIST - Padding( - padding: Responsive.isDesktop(context) - ? const EdgeInsets.symmetric(horizontal: 150) - : const EdgeInsets.symmetric(horizontal: 5), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, + validator: (value) { + final password = value ?? ''; + if (password.isEmpty) { + return 'Please enter your new password'; + } + if (password.length < 8) { + return 'Password must be at least 8 characters'; + } + if (!RegExp(r'[A-Za-z]').hasMatch(password)) { + return 'Password must include at least 1 letter'; + } + if (!RegExp(r'\d').hasMatch(password)) { + return 'Password must include at least 1 number'; + } + if (!RegExp(r'[@$!%*#?&]').hasMatch(password)) { + return 'Password must include at least 1 special character'; + } + return null; + }, + ), + ), + const SizedBox(height: 8), + // 🔹 VALIDATION LIST + Padding( + padding: Responsive.isDesktop(context) + ? const EdgeInsets.symmetric(horizontal: 150) + : const EdgeInsets.symmetric(horizontal: 5), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( children: [ - Row( - children: [ - Expanded(child: _buildCheckItem(hasMinLength, "Minimum 8 characters")), - Expanded(child: _buildCheckItem(hasSpecialChar, "1 special character")), - ], - ), - Row( - children: [ - Expanded(child: _buildCheckItem(hasUpperLower, "1 UPPER or lower case")), - Expanded(child: _buildCheckItem(hasNumber, "1 numerical")), - ], - ), + Expanded(child: _buildCheckItem(hasMinLength, "Minimum 8 characters")), + Expanded(child: _buildCheckItem(hasSpecialChar, "1 special character")), ], ), - ), - const SizedBox(height: 10), - Container( - height: 55, - margin: Responsive.isDesktop( - context) - ? const EdgeInsets - .symmetric( - horizontal: 150) - : const EdgeInsets - .symmetric( - horizontal: 0), - decoration: BoxDecoration( - border: Border.all( - width: 1, - color: Colors.grey), - borderRadius: - BorderRadius.circular( - 10), + Row( + children: [ + Expanded(child: _buildCheckItem(hasUpperLower, "1 UPPER or lower case")), + Expanded(child: _buildCheckItem(hasNumber, "1 numerical")), + ], ), - child: TextFormField( - controller: - confirmPasswordController, - obscureText: - _obscureConfirmPassword, - textAlignVertical: - TextAlignVertical - .center, - decoration: InputDecoration( - border: InputBorder.none, - hintText: - "Confirm Password", - contentPadding: - const EdgeInsets - .symmetric( - horizontal: 10), - suffixIcon: IconButton( - icon: Icon( - _obscureConfirmPassword - ? Icons - .visibility_off - : Icons - .visibility, - color: Colors.grey, - ), - onPressed: () { - setState(() { - _obscureConfirmPassword = - !_obscureConfirmPassword; - }); - }, - ), + ], + ), + ), + const SizedBox(height: 10), + Container( + height: 55, + margin: Responsive.isDesktop( + context) + ? const EdgeInsets + .symmetric( + horizontal: 150) + : const EdgeInsets + .symmetric( + horizontal: 0), + decoration: BoxDecoration( + border: Border.all( + width: 1, + color: Colors.grey), + borderRadius: + BorderRadius.circular( + 10), + ), + child: TextFormField( + controller: + confirmPasswordController, + obscureText: + _obscureConfirmPassword, + textAlignVertical: + TextAlignVertical + .center, + decoration: InputDecoration( + border: InputBorder.none, + hintText: + "Confirm Password", + contentPadding: + const EdgeInsets + .symmetric( + horizontal: 10), + suffixIcon: IconButton( + icon: Icon( + _obscureConfirmPassword + ? Icons + .visibility_off + : Icons + .visibility, + color: Colors.grey, ), - validator: (value) { - if (value == null || - value.isEmpty) { - return 'Please confirm your password'; - } - if (value != - newPasswordController - .text) { - return 'Passwords do not match'; - } - return null; + onPressed: () { + setState(() { + _obscureConfirmPassword = + !_obscureConfirmPassword; + }); }, ), ), - ], + validator: (value) { + if (value == null || + value.isEmpty) { + return 'Please confirm your password'; + } + if (value != + newPasswordController + .text) { + return 'Passwords do not match'; + } + return null; + }, + ), ), - SizedBox(height: 15), - Container( - margin: - Responsive.isDesktop(context) - ? EdgeInsets.symmetric( - horizontal: 150) - : EdgeInsets.symmetric( - horizontal: 0), - child: SizedBox( - width: double.infinity, - height: 40, - child: ElevatedButton( - style: - ElevatedButton.styleFrom( - backgroundColor: - Color(0xFF00989E), - shape: - RoundedRectangleBorder( - borderRadius: - BorderRadius.circular( - 10), - ), - ), - onPressed: _isLoading - ? null - : resetYourPassword, + ], + ), + SizedBox(height: 15), + Container( + margin: + Responsive.isDesktop(context) + ? EdgeInsets.symmetric( + horizontal: 150) + : EdgeInsets.symmetric( + horizontal: 0), + child: SizedBox( + width: double.infinity, + height: 40, + child: ElevatedButton( + style: + ElevatedButton.styleFrom( + backgroundColor: + Color(0xFF00989E), + shape: + RoundedRectangleBorder( + borderRadius: + BorderRadius.circular( + 10), + ), + ), + onPressed: _isLoading + ? null + : resetYourPassword, - child: _isLoading - ? CircularProgressIndicator( - valueColor: - AlwaysStoppedAnimation< - Color>( - Color(0xFF00989E), - ), - ) - : Text( - "Reset Password", - style: GoogleFonts - .poppins( - color: Color( - 0xFFFFFFFF), - ), - ), + child: _isLoading + ? CircularProgressIndicator( + valueColor: + AlwaysStoppedAnimation< + Color>( + Color(0xFF00989E), + ), + ) + : Text( + "Reset Password", + style: GoogleFonts + .poppins( + color: Color( + 0xFFFFFFFF), ), ), ), + ), + ), SizedBox( height: Responsive.isDesktop(context) - ? _size.height * 0.3 + ? null : _size.height * 0.2, ), // SizedBox( // height: _size.height * 0.1, // ), Container( - alignment: Alignment.bottomCenter, + alignment: Alignment.center, padding: EdgeInsets.symmetric(vertical: 8), child: RichText( @@ -801,27 +807,27 @@ class _changesPasswordState extends State { ))); } - Widget _buildCheckItem(bool status, String text) { - return Padding( - padding: const EdgeInsets.symmetric(vertical: 2), - child: Row( - children: [ - Icon( - status ? Icons.check : Icons.close, - color: status ? Colors.green : Colors.red, - size: 18, - ), - const SizedBox(width: 6), - Text( - text, - style: TextStyle( - color: status ? Colors.green : Colors.red, - fontSize: 14, - ), - ), - ], - ), - ); - } + Widget _buildCheckItem(bool status, String text) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 2), + child: Row( + children: [ + Icon( + status ? Icons.check : Icons.close, + color: status ? Colors.green : Colors.red, + size: 18, + ), + const SizedBox(width: 6), + Text( + text, + style: TextStyle( + color: status ? Colors.green : Colors.red, + fontSize: 14, + ), + ), + ], + ), + ); + } } diff --git a/lib/pages/config/gorouter.dart b/lib/pages/config/gorouter.dart index b04fc61..6decf68 100755 --- a/lib/pages/config/gorouter.dart +++ b/lib/pages/config/gorouter.dart @@ -32,171 +32,216 @@ // import '../session/settingUpPinAndBiometric.dart'; // import '../verify.dart'; // -// class AppRouter { -// // static final SessionNotifier sessionNotifier = SessionNotifier(); -// static GoRouter createRouter() { -// return GoRouter( -// initialLocation: kIsWeb ? '/login' : '/splash', -// debugLogDiagnostics: true, -// // refreshListenable: sessionNotifier, -// redirect: (BuildContext context, GoRouterState state) { -// const allowedWithoutToken = [ -// '/login', -// '/verify', -// '/mailVerify', -// '/pinPage', -// '/splash', -// ]; -// -// final location = state.uri.toString(); -// if (allowedWithoutToken.contains(location)) return null; -// -// // final hasToken = TokenService.hasValidTokenSync(); -// // if (!hasToken) return '/login'; -// -// // if (!kIsWeb && -// // SessionManager().prefs?.getString('is_mpin_skipped') == '0' && -// // location != '/pinPage') { -// // return '/pinPage'; -// // } -// -// return null; -// }, -// -// -// -// routes: [ -// // if (!kIsWeb) -// // GoRoute( -// // path: '/splash', -// // builder: (context, state) => SplashScreen(), -// // ), -// GoRoute( -// path: '/login', -// builder: (context, state) => login(), -// ), -// GoRoute( -// path: '/mailVerify', -// builder: (context, state) { -// final email = state.extra as String; -// return MyEmailVerify(email: email); -// }, -// ), -// GoRoute( -// path: '/verify', -// builder: (context, state) { -// final args = state.extra as Map; -// return MyVerify( -// verificationId: args['verificationId'] as String, -// mobileNumber: args['mobileNumber'] as String, -// resendToken: args['resendToken'], -// onResendCode: args['onResendCode'] as Function(String, int?), -// ); -// }, -// ), -// GoRoute( -// path: '/mailVerify', -// builder: (context, state) => MyVerify( -// verificationId: '', -// mobileNumber: '', -// resendToken: null, -// onResendCode: (String, int) {}, -// ), -// ), -// GoRoute( -// path: '/home', -// builder: (context, state) => Home(), -// ), -// GoRoute( -// path: '/pinSettingPage', -// builder: (context, state) => pinSettingPage(), -// ), -// GoRoute( -// path: '/pinPage', -// builder: (context, state) => pinPage(), -// ), -// GoRoute( -// path: '/changePin', -// builder: (context, state) => changePin(), -// ), -// GoRoute( -// path: '/claimprocess', -// builder: (context, state) => claimprocess(), -// ), -// GoRoute( -// path: '/policies', -// builder: (context, state) => policies(), -// ), -// GoRoute( -// path: '/claims', -// builder: (context, state) => claims(), -// ), -// GoRoute( -// path: '/profile', -// builder: (context, state) => profile(), -// ), -// GoRoute( -// path: '/help', -// builder: (context, state) => help(), -// ), -// GoRoute( -// path: '/wellness', -// builder: (context, state) => wellness(), -// ), -// GoRoute( -// path: '/planclaimsform', -// builder: (context, state) => planclaimsform(), -// ), -// GoRoute( -// path: '/privacypolicy', -// builder: (context, state) => privacypolicy(), -// ), -// GoRoute( -// path: '/termsofuse', -// builder: (context, state) => termsofuse(), -// ), -// GoRoute( -// path: '/generalExclusionsDeductibles', -// builder: (context, state) => generalExclusionsDeductibles(), -// ), -// GoRoute( -// path: '/chatbot()', -// builder: (context, state) => chatbot(), -// ), -// GoRoute( -// path: '/chatbot()', -// builder: (context, state) => chatbot(), -// ), -// GoRoute( -// path: '/tickettracklist', -// builder: (context, state) => tickettracklist( -// ticketID: "", -// ), -// ), -// GoRoute( -// path: '/empDetails()', -// builder: (context, state) => empDetails(), -// ), -// GoRoute( -// path: '/addOnsDetails()', -// builder: (context, state) => addOnsDetails(), -// ), -// GoRoute( -// path: '/empReviewDetails()', -// builder: (context, state) => empReviewDetails(), -// ), -// GoRoute( -// path: '/tickets()', -// builder: (context, state) => tickets(), -// ), -// GoRoute( -// path: '/raisedTicketHistory()', -// builder: (context, state) => raisedTicketHistory(), -// ), -// ], -// ); -// } -// } +// import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:flutter/foundation.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import '../../main.dart'; +import '../email_verify.dart'; +import '../enrollment/addons.dart'; +import '../enrollment/empDetails.dart'; +import '../enrollment/empReview.dart'; +import '../login.dart'; +import '../postEnrollment/chatbot.dart'; +import '../postEnrollment/claimprocess.dart'; +import '../postEnrollment/claims.dart'; +import '../postEnrollment/faqs.dart'; +import '../postEnrollment/generalexclusionsdeductibles.dart'; +import '../postEnrollment/help.dart'; +import '../postEnrollment/home.dart'; +import '../postEnrollment/planclaimsform.dart'; +import '../postEnrollment/policies.dart'; +import '../postEnrollment/privacypolicy.dart'; +import '../postEnrollment/profile.dart'; +import '../postEnrollment/raisedTicketList.dart'; +import '../postEnrollment/retailClaimForm.dart'; +import '../postEnrollment/termsofuse.dart'; +import '../postEnrollment/tickets.dart'; +import '../postEnrollment/tickettracklist.dart'; +import '../postEnrollment/wellness.dart'; +import '../postEnrollment/wellness_web_view.dart'; +import '../service/SessionManager.dart'; +import '../service/TokenService.dart'; +import '../session/SetPinBiometric.dart'; +import '../session/changePin.dart'; +import '../session/settingUpPinAndBiometric.dart'; +import '../verify.dart'; + +class AppRouter { + // static final SessionNotifier sessionNotifier = SessionNotifier(); + static GoRouter createRouter() { + return GoRouter( + // On web, respect the browser URL if present; otherwise fall back to login. + initialLocation: kIsWeb ? '/login' : '/splash', + debugLogDiagnostics: true, + redirect: (BuildContext context, GoRouterState state) async { + const allowedWithoutToken = [ + '/login', + '/verify', + '/mailVerify', + '/pinPage', + '/splash', + ]; + + // Always work with the matched path (ignores query params). + final location = state.matchedLocation; + final hasToken = await TokenService.hasValidToken(); + + final isGuestRoute = allowedWithoutToken.contains(location); + + if (hasToken && isGuestRoute) { + // If a logged-in user tries to access a guest route, redirect to home + return '/home'; + } + + if (!hasToken && !isGuestRoute) { + // If a guest user tries to access a protected route, redirect to login + return '/login'; + } + + return null; // No redirect needed + }, + routes: [ + if (!kIsWeb) + GoRoute( + path: '/splash', + builder: (context, state) => const SplashScreen(), + ), + GoRoute( + path: '/login', + builder: (context, state) => const login(), + ), + GoRoute( + path: '/mailVerify', + builder: (context, state) { + final data = state.extra as Map; + final type = data['type'] as String; + final value = data['value'] as String; + return MyEmailVerify(type: type, value: value); + }, + ), + // Legacy Firebase-phone OTP route is commented out in verify.dart, + // so we do not expose /verify from GoRouter anymore. + GoRoute( + path: '/home', + builder: (context, state) => Home(), + ), + GoRoute( + path: '/pinSettingPage', + builder: (context, state) => pinSettingPage(), + ), + GoRoute( + path: '/pinPage', + builder: (context, state) => pinPage(), + ), + GoRoute( + path: '/changePin', + builder: (context, state) => changePin(), + ), + GoRoute( + path: '/claimprocess', + builder: (context, state) => claimprocess(), + ), + GoRoute( + path: '/policies', + builder: (context, state) { + final arguments = + state.extra as Map?; // optional args + return policies(arguments: arguments); + }, + ), + GoRoute( + path: '/claims', + builder: (context, state) { + final int tabIndex = state.extra as int? ?? 0; + return claims(initialTab: tabIndex); + }, + ), + GoRoute( + path: '/profile', + builder: (context, state) => profile(), + ), + GoRoute( + path: '/help', + builder: (context, state) => help(), + ), + GoRoute( + path: '/wellness', + builder: (context, state) => wellness(), + ), + GoRoute( + path: '/privacypolicy', + builder: (context, state) => privacypolicy(), + ), + GoRoute( + path: '/termsofuse', + builder: (context, state) => termsofuse(), + ), + GoRoute( + path: '/generalExclusionsDeductibles', + builder: (context, state) => generalExclusionsDeductibles(), + ), + GoRoute( + path: '/planclaimsform', + builder: (context, state) { + final details = state.extra as Map?; + return planclaimsform(details: details); + }, + ), + GoRoute( + path: '/retailClaimForm', + builder: (context, state) { + final details = state.extra as Map?; + return retailClaimForm(details: details); + }, + ), + GoRoute( + path: '/raisedTicketHistory', + builder: (context, state) => raisedTicketHistory(), + ), + GoRoute( + path: '/tickettracklist/:ticketID', + builder: (context, state) { + final ticketID = state.pathParameters['ticketID']!; + return tickettracklist(ticketID: ticketID); + }, + ), + GoRoute( + path: '/empDetails', + builder: (context, state) => empDetails(), + ), + GoRoute( + path: '/faqs', + builder: (context, state) => faqs(), + ), + GoRoute( + path: '/addOnsDetails', + builder: (context, state) => addOnsDetails(), + ), + GoRoute( + path: '/empReviewDetails', + builder: (context, state) => empReviewDetails(), + ), + GoRoute( + path: '/tickets', + builder: (context, state) => tickets(), + ), + GoRoute( + path: '/chatbot', + builder: (context, state) => chatbot(), + ), + GoRoute( + path: '/wellnessWebView', + builder: (context, state) { + final url = state.extra as String; + return WellnessWebView(url: url); + }, + ), + ], + ); + } +} // // // // // class AppRouter { diff --git a/lib/pages/email_verify.dart b/lib/pages/email_verify.dart index 6a54549..15840d3 100755 --- a/lib/pages/email_verify.dart +++ b/lib/pages/email_verify.dart @@ -511,7 +511,8 @@ class _MyEmailVerifyState extends State { session.empClientBranchId == null || session.empClientBranchId!.isEmpty) { prefs.setBool('isRetailLoggedIn', true); } - context.go('/home'); + // context.go('/home'); + context.replace('/home'); } // if (emp_status == 'enrolled' || emp_status == 'active') { // context.go('/home'); @@ -686,10 +687,12 @@ class _MyEmailVerifyState extends State { if (_postToken != null && _postToken.isNotEmpty) { ToastHelper.showSuccessToast(context, 'Successfully Logged In'); - context.go('/home'); + context.replace('/home'); + // context.go('/home'); // Navigator.pushReplacementNamed(context, 'home'); } else { - context.go('/empDetails'); + context.replace('/empDetails'); + // context.go('/empDetails'); // Navigator.pushReplacementNamed(context, 'empDetails'); } @@ -740,10 +743,12 @@ class _MyEmailVerifyState extends State { if (response.statusCode == 200) { Map data = json.decode(response.body); if(data['status'] == 'success') { - context.go('/${route}'); + context.replace('/${route}'); + // context.go('/${route}'); } else if(data['status'] == 'failed'){ print('setPassword'); - context.goNamed( + // goNamed + context.replaceNamed( 'setPassword', queryParameters: { 'email': email_id, @@ -930,7 +935,7 @@ class _MyEmailVerifyState extends State { ), Container( margin: marginInsets, - alignment: Alignment.bottomCenter, + alignment: Alignment.center, child: SingleChildScrollView( child: Form( key: _formKey, @@ -1272,14 +1277,14 @@ class _MyEmailVerifyState extends State { // ), SizedBox( height: Responsive.isDesktop(context) - ? _size.height * 0.3 - : _size.height * 0.2, + ? _size.height * 0.2 + : _size.height * 0.15, ), // SizedBox( // height: _size.height * 0.1, // ), Container( - alignment: Alignment.bottomCenter, + alignment: Alignment.center, padding: EdgeInsets.symmetric(vertical: 8), child: RichText( diff --git a/lib/pages/enrollment/addons.dart b/lib/pages/enrollment/addons.dart index 0805024..434b6fb 100755 --- a/lib/pages/enrollment/addons.dart +++ b/lib/pages/enrollment/addons.dart @@ -1428,15 +1428,14 @@ class _addOnsDetailsState extends State { Future backFunction() async { final SharedPreferences prefs = await SharedPreferences.getInstance(); - if (prefs.containsKey('hrtoken')) { - String? mobileNo = prefs.getString('fromHrLoginMobileNo'); - Navigator.pushNamed(context, 'empDetails', - arguments: {'mobile': mobileNo}); - } else { - context.go('/empDetails'); - // Navigator.pushNamed(context, 'empDetails'); - } - } + if (prefs.containsKey('hrtoken')) { + String? mobileNo = prefs.getString('fromHrLoginMobileNo'); + Navigator.pushNamed(context, 'empDetails', + arguments: {'mobile': mobileNo}); + } else { + context.pop(); + // Navigator.pushNamed(context, 'empDetails'); + } } void checkSiTopUp() async { List> siData = [ @@ -1936,6 +1935,14 @@ class _addOnsDetailsState extends State { Image.network( clientLogo ?? '', // Nhance logo height: 40, + errorBuilder: (BuildContext context, Object error, + StackTrace? stackTrace) { + return Image.asset( + 'assets/Solid_gray.png', + height: 40, + fit: BoxFit.contain, + ); + }, ), // Image.network( // 'https://i.imgur.com/qOihOvk.png', // Prodapt logo @@ -4520,8 +4527,7 @@ class _addOnsDetailsState extends State { checkDependentTopUp(); sendAddonsGmcDependentToAPI(); } - context.go('/empReviewDetails'); - // Navigator.pushNamed(context, + context.push('/empReviewDetails'); // Navigator.pushNamed(context, // 'empReviewDetails'); } : null, diff --git a/lib/pages/enrollment/empDetails.dart b/lib/pages/enrollment/empDetails.dart index a3f42db..86da523 100755 --- a/lib/pages/enrollment/empDetails.dart +++ b/lib/pages/enrollment/empDetails.dart @@ -760,15 +760,15 @@ class _empDetailsState extends State { ), ); }, - // errorBuilder: (BuildContext context, - // Object error, StackTrace? stackTrace) { - // return Image.asset( - // 'assets/Solid_gray.png', // Replace 'default_image.png' with your default image asset path - // width: 80, - // height: 80, - // fit: BoxFit.cover, - // ); - // }, + errorBuilder: (BuildContext context, + Object error, StackTrace? stackTrace) { + return Image.asset( + 'assets/Solid_gray.png', + width: 80, + height: 80, + fit: BoxFit.cover, + ); + }, ), ), ), @@ -976,7 +976,7 @@ class _empDetailsState extends State { alignment: Alignment.centerRight, child: ElevatedButton( onPressed: () { - context.go('/addOnsDetails'); + context.push('/addOnsDetails'); // Navigator.pushNamed( // context, 'addOnsDetails'); }, @@ -2240,6 +2240,8 @@ class _empDetailsState extends State { print('gmcFloaterTextDescription $gmcFloaterTextDescription'); String gmcNotes = item['notes']; print('gmcNotes $gmcNotes'); + String cleanedNotes = gmcNotes?.toString().toLowerCase().replaceAll(' ', '') ?? ''; + print('gmcNotes cleaned: $cleanedNotes'); dynamic gmcECardDownload = item['eCardDownload']; print('gmcECardDownload $gmcECardDownload'); bool gmcCopyDependenceDataEnable = item['copy_dependence_data_enable']; @@ -2635,46 +2637,134 @@ class _empDetailsState extends State { Column( children: familyFloaterContainers, ), - // Add Family Member Button HERE - if (gmcOpenForEnrollment != 0 && gmcECardDownload == null)...[ - GestureDetector( - onTap: () { - if (getFalseObjects.length == 0) { - ToastHelper.showWarningToast(context, "No family member to add"); - return; - } - openAddFamilyMemberPopup( - "Add", - null, - gmcClientPolicyId, - gmcRelationShip, - gmcSumInsured, - ); - }, - child: Container( - margin: EdgeInsets.only(top: 10), - padding: EdgeInsets.all(15), - decoration: BoxDecoration( - border: Border.all(color: Colors.black, width: 1), - borderRadius: BorderRadius.circular(8), - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon(Icons.person_add, color: Colors.black), - SizedBox(width: 10), - Text( - "Add Family Member", - style: GoogleFonts.poppins( - fontSize: Responsive.isDesktop(context) ? 20 : 16, - fontWeight: FontWeight.w500, + // Add Family Member Button: + // Show only when: + // - enrollment is open + // - e-card not generated + // - there are still dependants left to add + // - and the policy allows relationships beyond just "Self" + // if (gmcOpenForEnrollment != 0 && + // gmcECardDownload == null && + // getFalseObjects.isNotEmpty && + // gmcRelationShip.any((rel) => + // (rel?.toString().toLowerCase() ?? '') != 'self')) ...[ + // Builder(builder: (context) { + // print("*** Condition is TRUE"); + // GestureDetector( + // onTap: () { + // if (getFalseObjects.length == 0) { + // ToastHelper.showWarningToast(context, "No family member to add"); + // return; + // } + // openAddFamilyMemberPopup( + // "Add", + // null, + // gmcClientPolicyId, + // gmcRelationShip, + // gmcSumInsured, + // ); + // }, + // child: Container( + // margin: EdgeInsets.only(top: 10), + // padding: EdgeInsets.all(15), + // decoration: BoxDecoration( + // border: Border.all(color: Colors.black, width: 1), + // borderRadius: BorderRadius.circular(8), + // ), + // child: Row( + // mainAxisAlignment: MainAxisAlignment.center, + // children: [ + // Icon(Icons.person_add, color: Colors.black), + // SizedBox(width: 10), + // Text( + // "Add Family Member", + // style: GoogleFonts.poppins( + // fontSize: Responsive.isDesktop(context) ? 20 : 16, + // fontWeight: FontWeight.w500, + // ), + // ), + // ], + // ), + // ), + // ), + // ], + if (cleanedNotes != 'allowedmembersself') ...[ + if (gmcOpenForEnrollment != 0 && + gmcECardDownload == null && + getFalseObjects.isNotEmpty && + gmcRelationShip.any((rel) => + (rel is Map + ? rel['relationship']?.toString().toLowerCase() + : rel?.toString().toLowerCase() ?? '') != 'self')) ...[ + Builder(builder: (context) { + print("*** IF - Condition is TRUE"); + print("*** gmcOpenForEnrollment: $gmcOpenForEnrollment"); + print("*** gmcECardDownload: $gmcECardDownload"); + print("*** getFalseObjects length: ${getFalseObjects.length}"); + print("*** gmcRelationShip full: $gmcRelationShip"); + print("*** gmcNotes original: $gmcNotes"); + print("*** gmcNotes cleaned: $cleanedNotes"); + + + return GestureDetector( + onTap: () { + if (getFalseObjects.length == 0) { + ToastHelper.showWarningToast(context, "No family member to add"); + return; + } + openAddFamilyMemberPopup( + "Add", + null, + gmcClientPolicyId, + gmcRelationShip, + gmcSumInsured, + ); + }, + child: Container( + margin: EdgeInsets.only(top: 10), + padding: EdgeInsets.all(15), + decoration: BoxDecoration( + border: Border.all(color: Colors.black, width: 1), + borderRadius: BorderRadius.circular(8), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.person_add, color: Colors.black), + SizedBox(width: 10), + Text( + "Add Family Member", + style: GoogleFonts.poppins( + fontSize: Responsive.isDesktop(context) ? 20 : 16, + fontWeight: FontWeight.w500, + ), ), - ), - ], + ], + ), ), - ), - ), + ); + }), + ] else ...[ + Builder(builder: (context) { + print("*** ELSE - Condition is FALSE"); + print("*** gmcOpenForEnrollment: $gmcOpenForEnrollment → pass: ${gmcOpenForEnrollment != 0}"); + print("*** gmcECardDownload: $gmcECardDownload → pass: ${gmcECardDownload == null}"); + print("*** getFalseObjects length: ${getFalseObjects.length} → pass: ${getFalseObjects.isNotEmpty}"); + print("*** gmcRelationShip full list: $gmcRelationShip"); + print("*** gmcNotes original: $gmcNotes"); + print("*** gmcNotes cleaned: $cleanedNotes"); + return SizedBox.shrink(); // no widget shown in else + }), ], + ]else ...[ + Builder(builder: (context) { + print("*** OUTER ELSE - Condition is FALSE"); + print("*** gmcNotes original: $gmcNotes"); + print("*** gmcNotes cleaned: $cleanedNotes"); + return SizedBox.shrink(); + }), + + ], SizedBox(height: 15), if (Responsive.isDesktop(context) && gmcIsValueValid) Row( diff --git a/lib/pages/enrollment/empReview.dart b/lib/pages/enrollment/empReview.dart index 292f477..1a1748f 100755 --- a/lib/pages/enrollment/empReview.dart +++ b/lib/pages/enrollment/empReview.dart @@ -1413,6 +1413,14 @@ setState(() { Image.network( clientLogo ?? '', // Nhance logo height: 40, + errorBuilder: (BuildContext context, Object error, + StackTrace? stackTrace) { + return Image.asset( + 'assets/Solid_gray.png', + height: 40, + fit: BoxFit.contain, + ); + }, ), // Image.network( // 'https://i.imgur.com/qOihOvk.png', // Prodapt logo diff --git a/lib/pages/login.dart b/lib/pages/login.dart index 3949ed1..80d6dc5 100755 --- a/lib/pages/login.dart +++ b/lib/pages/login.dart @@ -278,7 +278,7 @@ class _loginState extends State { print('isEmailFieldVisible $isEmailFieldVisible'); // prefs.setString('empEmail', emailController.text); print('${emailMobileController.text}'); - context.push( + context.go( '/mailVerify', extra: { 'type': 'email', @@ -287,7 +287,7 @@ class _loginState extends State { ); } else { // _verifyPhoneNumber(); - context.push( + context.go( '/mailVerify', extra: { 'type': 'mobile', @@ -690,6 +690,14 @@ class _loginState extends State { clickedForgotPassword = true; passwordController.text = ''; passwordController.clear(); + + // ✅ Clear everything so the flow starts fresh + emailController.clear(); + _otpController.clear(); + resetPasswordController.clear(); + confirmPasswordController.clear(); + + _formKey.currentState?.reset(); }); } @@ -800,8 +808,17 @@ class _loginState extends State { otpValueStatus = false; otpFieldShow = false; clickedForgotPassword = false; - resetPasswordEnable = true; + resetPasswordEnable = true; // This shows the reset fields _isLoading = false; + hasMinLength = false; + hasUpperLower = false; + hasNumber = false; + hasSpecialChar = false; + + // ✅ ADD THESE LINES TO CLEAR CACHED DATA + resetPasswordController.clear(); + confirmPasswordController.clear(); + _formKey.currentState?.reset(); }); // ToastHelper.showSuccessToast(context, message); } else { @@ -871,6 +888,17 @@ class _loginState extends State { resetPasswordEnable = false; clickedForgotPassword = false; otpFieldShow = false; + + // ✅ CLEAR THE CONTROLLERS HERE + resetPasswordController.clear(); + confirmPasswordController.clear(); + _otpController.clear(); + // emailController.clear(); // Uncomment if you want the email cleared too + + // Reset the form state to clear validation error messages + _formKey.currentState?.reset(); + emailController.text = ''; + emailController.clear(); }); // ToastHelper.showSuccessToast(context, message); } else { @@ -1095,7 +1123,7 @@ class _loginState extends State { child: Image.asset( 'assets/nhance_app_logo.png', width: 150, - height: 100, + height: 150, )), ), ], @@ -1109,7 +1137,7 @@ class _loginState extends State { ), Container( margin: marginInsets, - alignment: Alignment.bottomCenter, + alignment: Alignment.center, child: SingleChildScrollView( child: Form( key: _formKey, @@ -1120,9 +1148,7 @@ class _loginState extends State { Expanded( flex: _size.width < 1100 ? 6 : 12, child: Container( - margin: _size.width > 1100 - ? EdgeInsets.only(left: 20, right: 20) - : EdgeInsets.only(left: 0, right: 0), + margin: EdgeInsets.symmetric(horizontal: 20), child: Column( mainAxisAlignment: MainAxisAlignment.center, @@ -1638,152 +1664,114 @@ class _loginState extends State { }, ), ), - if (otpFieldShow) ...[ - SizedBox(height: 10), - AnimatedSwitcher( - duration: const Duration( - milliseconds: 500, - ), // animation speed - switchInCurve: - Curves.easeInOutCirc, - switchOutCurve: - Curves.easeOutCirc, - child: - clickedForgotPassword - ? Column( - key: const ValueKey( - 'otp_block'), - crossAxisAlignment: - CrossAxisAlignment - .start, - children: [ - Container( - child: Center( - child: - Text( - 'OTP', - style: - TextStyle( - fontSize: Responsive.isMobile(context) - ? 14 - : 18, - fontWeight: - FontWeight.w600, - color: + if (otpFieldShow && + clickedForgotPassword) ...[ + const SizedBox(height: 10), + Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Center( + child: Text( + 'OTP', + style: TextStyle( + fontSize: Responsive + .isMobile( + context) + ? 14 + : 18, + fontWeight: + FontWeight + .w600, + color: Colors.black, - ), + ), + ), + ), + const SizedBox( + height: 10), + Container( + alignment: + Alignment.center, + margin: Responsive + .isDesktop( + context) + ? const EdgeInsets + .symmetric( + horizontal: + 150) + : const EdgeInsets + .symmetric( + horizontal: + 0), + child: Pinput( + length: 6, + inputFormatters: [ + FilteringTextInputFormatter + .digitsOnly, + ], + keyboardType: + TextInputType + .number, + showCursor: true, + controller: + _otpController, + validator: + (value) { + if (value == + null || + value + .isEmpty) { + return 'Please enter OTP'; + } + if (value + .length < + 6) { + return 'OTP must be 6 digits'; + } + return null; + }, + ), + ), + const SizedBox( + height: 10), + Container( + margin: Responsive + .isDesktop( + context) + ? const EdgeInsets + .symmetric( + horizontal: + 150) + : const EdgeInsets + .symmetric( + horizontal: + 0), + alignment: Alignment + .centerRight, + child: InkWell( + onTap: () { + setState(() { + otpFieldShow = + false; + }); + resendOTP(); + }, + mouseCursor: + SystemMouseCursors + .click, + child: Text( + 'Didn’t Receive Code?', + style: GoogleFonts + .poppins( + color: Colors + .blue, + fontSize: 14, ), ), ), - const SizedBox( - height: - 10), - Container( - alignment: Alignment.center, - margin: Responsive.isDesktop(context) - ? EdgeInsets.symmetric( - horizontal: 150) - : EdgeInsets.symmetric( - horizontal: 0), - child: Pinput( - length: 6, - // defaultPinTheme: defaultPinTheme, - // focusedPinTheme: focusedPinTheme, - // submittedPinTheme: submittedPinTheme, - inputFormatters: [ - FilteringTextInputFormatter - .digitsOnly, // ✅ allows only 0–9 - ], - keyboardType: TextInputType.number, - showCursor: true, - controller: _otpController, - validator: - (value) { - if (value == null || - value.isEmpty) { - return 'Please enter OTP'; - } - if (value.length < - 6) { - return 'OTP must be 6 digits'; - } - // if (otpValueStatus) { - // // example - // return 'Invalid OTP'; - // } - return null; - }, - ), - ), - const SizedBox( - height: - 10), - Container( - margin: Responsive.isDesktop(context) - ? EdgeInsets.symmetric( - horizontal: - 150) - : EdgeInsets.symmetric( - horizontal: - 0), - alignment: - Alignment - .centerRight, // center the text - child:InkWell( - onTap: (){ - setState(() { - otpFieldShow = false; - }); - print('ABCDEF'); - resendOTP(); - }, - mouseCursor: SystemMouseCursors.click, - child: Text( - 'Didn’t Receive Code?', - style: GoogleFonts.poppins( - color: Colors.blue, - fontSize: 14, - ), - ), - ), - // RichText( - // textAlign: - // TextAlign.right, - // text: - // TextSpan( - // text: - // 'Didn’t Receive Code? ', // normal text - // style: - // TextStyle( - // fontSize: Responsive.isMobile(context) - // ? 12 - // : 14, - // fontWeight: - // FontWeight.normal, - // color: - // Colors.black, - // ), - // children: [ - // TextSpan( - // text: 'Resend', // bold clickable part - // style: TextStyle( - // fontWeight: FontWeight.bold, - // color: Colors.white, - // decoration: TextDecoration.underline, // optional - // ), - // // recognizer: TapGestureRecognizer() - // // ..onTap = () { - // // - // // }, - // ), - // ], - // ), - // ), - ), - ], - ) - : const SizedBox - .shrink(), // Empty widget when false + ), + ], ), ], SizedBox(height: 20), @@ -1856,6 +1844,7 @@ class _loginState extends State { borderRadius: BorderRadius.circular(10), ), child: TextFormField( + key: const ValueKey('new_password_field'), controller: resetPasswordController, obscureText: _resetObscurePassword, onChanged: validatePassword, @@ -1915,6 +1904,7 @@ class _loginState extends State { borderRadius: BorderRadius.circular(10), ), child: TextFormField( + key: const ValueKey('confirm_password_field'), controller: confirmPasswordController, obscureText: _obscureConfirmPassword, textAlignVertical: TextAlignVertical.center, diff --git a/lib/pages/postEnrollment/AddPolicyScreen.dart b/lib/pages/postEnrollment/AddPolicyScreen.dart index e5569b1..05a19c9 100755 --- a/lib/pages/postEnrollment/AddPolicyScreen.dart +++ b/lib/pages/postEnrollment/AddPolicyScreen.dart @@ -127,13 +127,15 @@ class _AddPolicyScreenState extends State { policyNoController.clear(); expDateController.clear(); - setState(() => isLoading = false); - context.go('/home'); } } } catch (e) { print('Submit error: $e'); + } finally { + if (mounted) { + setState(() => isLoading = false); + } } } diff --git a/lib/pages/postEnrollment/claimprocess.dart b/lib/pages/postEnrollment/claimprocess.dart index 8491ead..3c8860e 100755 --- a/lib/pages/postEnrollment/claimprocess.dart +++ b/lib/pages/postEnrollment/claimprocess.dart @@ -177,7 +177,8 @@ import '../service/popup_helper.dart'; canPop: false, onPopInvokedWithResult: (didPop, result) { if (didPop) return; - context.go('/claims'); + // context.go('/claims'); + context.pop(); }, child: Scaffold( backgroundColor: Colors.white, @@ -209,7 +210,7 @@ import '../service/popup_helper.dart'; flex: 12, child: InkWell( onTap: () { - context.go('/claims'); + context.pop(); // context.go('/claims'); }, child: Row( mainAxisAlignment: diff --git a/lib/pages/postEnrollment/claims.dart b/lib/pages/postEnrollment/claims.dart index 1383301..7a3bcee 100755 --- a/lib/pages/postEnrollment/claims.dart +++ b/lib/pages/postEnrollment/claims.dart @@ -311,11 +311,18 @@ class _claimsState extends State { if (widget.initialTab == 2) { context.go('/home'); } - if (widget.initialTab == 0) { - context.go('/home'); - } else { - context.pop(); - } + + if (Navigator.canPop(context)) { + context.pop(); + } else { + context.go('/home'); + } + + // if (widget.initialTab == 0) { + // context.pop(); + // } else { + // context.pop(); + // } }, child: Row( mainAxisAlignment: MainAxisAlignment.start, @@ -993,14 +1000,14 @@ class _claimsState extends State { var details = { "retailDetails": item, }; - context.go('/retailClaimForm', extra: details); + context.push('/retailClaimForm', extra: details); } else { // Normal claim policy click var details = { "claimsDetails": item, "fromClaimPage": 0, }; - context.go('/planclaimsform', extra: details); + context.push('/planclaimsform', extra: details); } }, child: MouseRegion( diff --git a/lib/pages/postEnrollment/generalexclusionsdeductibles.dart b/lib/pages/postEnrollment/generalexclusionsdeductibles.dart index 536d15f..58bbf3f 100755 --- a/lib/pages/postEnrollment/generalexclusionsdeductibles.dart +++ b/lib/pages/postEnrollment/generalexclusionsdeductibles.dart @@ -192,11 +192,55 @@ class _generalExclusionsDeductiblesState child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - 'General Exclusions & Deductibles', - style: GoogleFonts.poppins( - fontSize: 18, fontWeight: FontWeight.w600), + Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Expanded( + flex: 12, + child: InkWell( + onTap: () { + context.go('/home'); + }, + child: Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Icon( + Icons.chevron_left, + color: Color(0xFF000000), + size: 30, + ), + SizedBox( + width: Responsive.isDesktop(context) + ? 0 + : 5), + Column( + mainAxisAlignment: + MainAxisAlignment.center, + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + 'General Exclusions & Deductibles', + textAlign: TextAlign.start, + style: GoogleFonts.poppins( + fontSize: 16, + fontWeight: FontWeight.w600, + color: Color(0xFF000000), + ), + ), + ], + ), + ], + ), + ), + ), + ], ), + // Text( + // 'General Exclusions & Deductibles', + // style: GoogleFonts.poppins( + // fontSize: 18, fontWeight: FontWeight.w600), + // ), const SizedBox(height: 20), if (type3Content.isNotEmpty) ...[ Text(type3SectionName ?? '', diff --git a/lib/pages/postEnrollment/help.dart b/lib/pages/postEnrollment/help.dart index b5a8d45..cacbd2f 100755 --- a/lib/pages/postEnrollment/help.dart +++ b/lib/pages/postEnrollment/help.dart @@ -336,31 +336,31 @@ class _helpState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.center, children: [ - Row( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - Expanded( - flex: 12, - child: InkWell( - onTap: () { - context.push('/claims'); - // Navigator.pushNamed(context, 'home'); - }, - child: Row( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - Icon( - Icons - .chevron_left, // Replace with your desired icon - color: Color(0xFF000000), - size: 30, - ), - ], - ), - ), - ), - ], - ), + // Row( + // mainAxisAlignment: MainAxisAlignment.start, + // children: [ + // Expanded( + // flex: 12, + // child: InkWell( + // onTap: () { + // context.push('/claims'); + // // Navigator.pushNamed(context, 'home'); + // }, + // child: Row( + // mainAxisAlignment: MainAxisAlignment.start, + // children: [ + // Icon( + // Icons + // .chevron_left, // Replace with your desired icon + // color: Color(0xFF000000), + // size: 30, + // ), + // ], + // ), + // ), + // ), + // ], + // ), Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ diff --git a/lib/pages/postEnrollment/home.dart b/lib/pages/postEnrollment/home.dart index 1951e76..0260a03 100755 --- a/lib/pages/postEnrollment/home.dart +++ b/lib/pages/postEnrollment/home.dart @@ -1880,10 +1880,44 @@ class _HomeState extends State { cards.add(_buildAddCard()); // <-- always add this card } + // ✅ NEW: Show empty state when inactive tab has no policies + if (!isActive && cards.isEmpty) { + return _buildEmptyState("No inactive policies available"); + } return _buildCarouselSlider(cards); } + Widget _buildEmptyState(String message) { + return Container( + width: double.infinity, + margin: const EdgeInsets.symmetric(vertical: 8), + padding: const EdgeInsets.symmetric(vertical: 40, horizontal: 20), + decoration: BoxDecoration( + color: const Color(0xFFF7F7F7), + borderRadius: BorderRadius.circular(20), + border: Border.all(color: const Color(0xFFE0E0E0), width: 1.5), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.folder_off_outlined, size: 48, color: Colors.grey[400]), + const SizedBox(height: 12), + Text( + message, + textAlign: TextAlign.center, + style: GoogleFonts.poppins( + fontSize: 14, + fontWeight: FontWeight.w500, + color: Colors.grey[600], + ), + ), + ], + ), + ); + } + Widget _buildCarouselSlider(List cards) { final int totalCards = cards.length; final bool hasAddCard = isActive; // because you add AddCard only when isActive diff --git a/lib/pages/postEnrollment/planclaimsform.dart b/lib/pages/postEnrollment/planclaimsform.dart index 8440823..f9f2077 100755 --- a/lib/pages/postEnrollment/planclaimsform.dart +++ b/lib/pages/postEnrollment/planclaimsform.dart @@ -137,6 +137,7 @@ class _planclaimsformState extends State { bool isIntimationDateValid = true; bool isAdmitDateValid = true; bool isDischargeDateValid = true; + bool showFileError = false; final session = SessionManager(); @@ -591,57 +592,70 @@ class _planclaimsformState extends State { } Future sendFormDataToApi() async { - setState(() => isSubmitting = true); // 🔥 start loader - setState(() { - isServiceValid = serviceId != null; - isPolicyValid = policyNumberId != null; - isMemberValid = selectedMemberId != null; - isSubjectValid = subjectController.text.trim().isNotEmpty; - isHospitalNameValid = (policyTypeCondition != 1 && policyTypeCondition != 72) || hospitalNameController.text.trim().isNotEmpty; - isHospitalAddressValid = (policyTypeCondition != 1 && policyTypeCondition != 72) || hospitalAddressController.text.trim().isNotEmpty; - isHospitalStateValid = (policyTypeCondition != 1 && policyTypeCondition != 72) || hospitalStateController.text.trim().isNotEmpty; - isHospitalCityValid = (policyTypeCondition != 1 && policyTypeCondition != 72) || hospitalCityController.text.trim().isNotEmpty; - isHospitalPincodeValid = (policyTypeCondition != 1 && policyTypeCondition != 72) || hospitalPinCodeController.text.trim().isNotEmpty; - isHospitalPhoneNoValid = (policyTypeCondition != 1 && policyTypeCondition != 72) || hospitalPhoneNoController.text.trim().isNotEmpty; - // isAdmitDischargeValid = policyTypeCondition != 1 || (admitDate != null && dischargeDate != null); - isClaimTypeValid = (policyTypeCondition != 1 && policyTypeCondition != 72) || claimTypeId != null; - isAdmitDateValid = (policyTypeCondition != 1 && policyTypeCondition != 72) || admitDate != null; - isDischargeDateValid = (policyTypeCondition != 1 && policyTypeCondition != 72) || dischargeDate != null; - isClaimAmountValid = serviceId != 1 || claimAmountController.text.trim().isNotEmpty; - isAccidentDateValid = (serviceId == 2 || serviceId == 3 || serviceId == 4) ? accidentDate != null : true; - isIntimationDateValid = (serviceId == 2 || serviceId == 3 || serviceId == 4) ? intimationDate != null : true; - }); + final isServiceValid = serviceId != null; + final isPolicyValid = policyNumberId != null; + final isMemberValid = selectedMemberId != null; + final isSubjectValid = subjectController.text.trim().isNotEmpty; + final isHospitalNameValid = (policyTypeCondition != 1 && policyTypeCondition != 72) || hospitalNameController.text.trim().isNotEmpty; + final isHospitalAddressValid = (policyTypeCondition != 1 && policyTypeCondition != 72) || hospitalAddressController.text.trim().isNotEmpty; + final isHospitalStateValid = (policyTypeCondition != 1 && policyTypeCondition != 72) || hospitalStateController.text.trim().isNotEmpty; + final isHospitalCityValid = (policyTypeCondition != 1 && policyTypeCondition != 72) || hospitalCityController.text.trim().isNotEmpty; + final isHospitalPincodeValid = (policyTypeCondition != 1 && policyTypeCondition != 72) || hospitalPinCodeController.text.trim().isNotEmpty; + final isHospitalPhoneNoValid = (policyTypeCondition != 1 && policyTypeCondition != 72) || (hospitalPhoneNoController.text.trim().length == 10); + final isAdmitDateValid = (policyTypeCondition != 1 && policyTypeCondition != 72) || admitDate != null; + final isDischargeDateValid = (policyTypeCondition != 1 && policyTypeCondition != 72) || dischargeDate != null; + final isClaimAmountValid = serviceId != 1 || claimAmountController.text.trim().isNotEmpty; + final isAccidentDateValid = (serviceId == 2 || serviceId == 3 || serviceId == 4) ? accidentDate != null : true; + final isIntimationDateValid = (serviceId == 2 || serviceId == 3 || serviceId == 4) ? intimationDate != null : true; + final areFilesUploaded = FileUploadService().files.isNotEmpty; - if (isServiceValid && - isClaimTypeValid && - isPolicyValid && - isMemberValid && - isSubjectValid && - isHospitalNameValid && - isHospitalAddressValid && - isHospitalStateValid && - isHospitalCityValid && - isHospitalPincodeValid && - isHospitalPhoneNoValid && - isAdmitDateValid && - isDischargeDateValid && - isClaimAmountValid && - isAccidentDateValid && - isIntimationDateValid) { - if (FileUploadService().files.isEmpty) { - setState(() { - MultiFileUploadWidget.hasFiles = false; - }); - ToastHelper.showErrorToast(context, 'Please upload at least one document'); - return; - } - // Proceed to submit - } else { + if (!isServiceValid || + !isPolicyValid || + !isMemberValid || + !isSubjectValid || + !isHospitalNameValid || + !isHospitalAddressValid || + !isHospitalStateValid || + !isHospitalCityValid || + !isHospitalPincodeValid || + !isHospitalPhoneNoValid || + !isAdmitDateValid || + !isDischargeDateValid || + !isClaimAmountValid || + !isAccidentDateValid || + !isIntimationDateValid) { + setState(() { + this.isServiceValid = isServiceValid; + this.isPolicyValid = isPolicyValid; + this.isMemberValid = isMemberValid; + this.isSubjectValid = isSubjectValid; + this.isHospitalNameValid = isHospitalNameValid; + this.isHospitalAddressValid = isHospitalAddressValid; + this.isHospitalStateValid = isHospitalStateValid; + this.isHospitalCityValid = isHospitalCityValid; + this.isHospitalPincodeValid = isHospitalPincodeValid; + this.isHospitalPhoneNoValid = isHospitalPhoneNoValid; + this.isAdmitDateValid = isAdmitDateValid; + this.isDischargeDateValid = isDischargeDateValid; + this.isClaimAmountValid = isClaimAmountValid; + this.isAccidentDateValid = isAccidentDateValid; + this.isIntimationDateValid = isIntimationDateValid; + showFileError = true; // Also show file error if other fields are invalid + }); ToastHelper.showErrorToast(context, 'Please Fill Required Fields'); return; } + if (!areFilesUploaded) { + setState(() { + showFileError = true; + }); + ToastHelper.showErrorToast(context, 'Please upload the document'); + return; + } + setState(() { + isSubmitting = true; isLoading = true; }); @@ -711,7 +725,8 @@ class _planclaimsformState extends State { _token = await TokenService.getPostToken(); - final request = http.MultipartRequest('POST', Uri.parse('${Environment.apiUrl}initiateClaim')); + final request = http.MultipartRequest( + 'POST', Uri.parse('${Environment.apiUrl}initiateClaim')); request.headers['Authorization'] = 'Bearer $_token'; request.headers['APP-SIGNATURE'] = 'nhance-2025-signature-T8f6gV9k5fK2wf5V5c4vId3b0J8z6cAm2x8Y'; final stringFields = fields.map((key, value) => MapEntry(key, value?.toString() ?? '')); @@ -728,7 +743,10 @@ class _planclaimsformState extends State { print("📝 Checking label for file: ${uf.file.name}, current label: '${uf.label}'"); if ((uf.label ?? '').trim().isEmpty) { ToastHelper.showErrorToast(context, 'Please enter name for all uploaded documents'); - setState(() => isLoading = false); + setState(() { + isLoading = false; + isSubmitting = false; // ✅ reset loader + }); return; } } @@ -769,33 +787,20 @@ class _planclaimsformState extends State { // ✅ Convert image → PDF final pdf = pw.Document(); final image = pw.MemoryImage(fileBytes); - - pdf.addPage( - pw.Page( - build: (pw.Context context) => pw.Center( - child: pw.Image(image, fit: pw.BoxFit.contain), - ), - ), - ); - - fileBytes = await pdf.save(); // converted PDF bytes - - // replace file name with .pdf extension + pdf.addPage(pw.Page( + build: (pw.Context context) => + pw.Center(child: pw.Image(image, fit: pw.BoxFit.contain)), + )); + fileBytes = await pdf.save(); final pdfFileName = pf.name.replaceAll(RegExp(r'\.\w+$'), '.pdf'); print('📄 Converted image ${pf.name} → PDF ($pdfFileName)'); request.files.add(http.MultipartFile.fromBytes( - 'claim_docs[]', - fileBytes, - filename: pdfFileName, - )); + 'claim_docs[]', fileBytes, filename: pdfFileName)); } else { // ✅ Already a PDF request.files.add(http.MultipartFile.fromBytes( - 'claim_docs[]', - fileBytes, - filename: pf.name, - )); + 'claim_docs[]', fileBytes, filename: pf.name)); } } } @@ -816,31 +821,25 @@ class _planclaimsformState extends State { final responseBody = await response.stream.bytesToString(); final decoded = jsonDecode(responseBody); + if (decoded['status'] == true) { ToastHelper.showSuccessToast(context, decoded['message']); serviceId = null; departmentList.clear(); - setState(() { - isLoading = false; - }); + setState(() => isLoading = false); context.go('/claims', extra: 2); print('Form data submitted successfully'); fileService.clearAll(); } else { - setState(() { - isLoading = false; - }); + setState(() => isLoading = false); ToastHelper.showErrorToast(context, "Failed: ${decoded['message']}"); } - } catch (e) { - setState(() { - isLoading = false; - }); + setState(() => isLoading = false); print('Error submitting form data: $e'); } finally { - setState(() => isSubmitting = false); // 🔥 stop loader + setState(() => isSubmitting = false); // 🔥 always stop loader } } @@ -932,60 +931,92 @@ class _planclaimsformState extends State { canPop: false, onPopInvokedWithResult: (didPop, result) { if (didPop) return; - final route = fromClaimsPage == 0 ? 'claims' : 'help'; - context.go('/$route'); + context.pop(); }, child: Scaffold( - backgroundColor: Colors.white, - appBar: CustomAppBar(), - body: Stack(children: [ - SingleChildScrollView( - child: Container( - padding: Responsive.isDesktop(context) - ? EdgeInsets.symmetric( - horizontal: MediaQuery.of(context).size.width * - 0.2, // 30% of screen width as horizontal padding - vertical: MediaQuery.of(context).size.height * - 0.05, // 5% of screen height as vertical padding - ) - : EdgeInsets.all(10), - color: Colors.white, - child: Column(children: [ - Card( - elevation: 5, - shape: RoundedRectangleBorder( - borderRadius: - BorderRadius.circular(15.0), // Set border radius here - ), - child: Container( - decoration: BoxDecoration( - color: Colors.white, // Set background color to white - borderRadius: BorderRadius.circular( - 15.0), // Set border radius for Container - ), - padding: Responsive.isDesktop(context) - ? EdgeInsets.only(top: 15, bottom: 15, left: 15, right: 15) - : EdgeInsets.only( - top: 10, - bottom: 10, - left: 10, - right: 10), // Add padding to the container - child: Row( - children: [ - Expanded( - flex: 12, - child: Container( - alignment: Alignment.centerLeft, - child: Column( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Container( - decoration: BoxDecoration( - color: Color( - 0xFFFFFCE5), // Set background color for the container - borderRadius: BorderRadius.circular( - 10), // Set border radius for the container + backgroundColor: Colors.white, + appBar: CustomAppBar(), + body: Stack(children: [ + SingleChildScrollView( + child: Container( + padding: Responsive.isDesktop(context) + ? EdgeInsets.symmetric( + horizontal: MediaQuery.of(context).size.width * + 0.2, // 30% of screen width as horizontal padding + vertical: MediaQuery.of(context).size.height * + 0.05, // 5% of screen height as vertical padding + ) + : EdgeInsets.all(10), + color: Colors.white, + child: Column(children: [ + Card( + elevation: 5, + shape: RoundedRectangleBorder( + borderRadius: + BorderRadius.circular(15.0), // Set border radius here + ), + child: Container( + decoration: BoxDecoration( + color: Colors.white, // Set background color to white + borderRadius: BorderRadius.circular( + 15.0), // Set border radius for Container + ), + padding: Responsive.isDesktop(context) + ? EdgeInsets.only(top: 15, bottom: 15, left: 15, right: 15) + : EdgeInsets.only( + top: 10, + bottom: 10, + left: 10, + right: 10), // Add padding to the container + child: Row( + children: [ + Expanded( + flex: 12, + child: Container( + alignment: Alignment.centerLeft, + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + decoration: BoxDecoration( + color: Color( + 0xFFFFFCE5), // Set background color for the container + borderRadius: BorderRadius.circular( + 10), // Set border radius for the container + ), + padding: Responsive.isDesktop(context) + ? EdgeInsets.only( + top: 20, + bottom: 20, + left: 0, + right: 0) + : EdgeInsets.only( + top: 10, + bottom: 10, + left: 10, + right: 10), + child: Column( + crossAxisAlignment: + CrossAxisAlignment.center, + children: [ + Row( + mainAxisAlignment: + MainAxisAlignment.start, + children: [ + Expanded( + flex: 1, + child: InkWell( + onTap: () { + context.pop(); + }, + child: Icon( + Icons + .chevron_left, // Replace with your desired icon + color: Color(0xFF000000), + size: 30, + ), + ), ), padding: Responsive.isDesktop(context) ? EdgeInsets.only( @@ -1039,19 +1070,439 @@ class _planclaimsformState extends State { fontSize: Responsive.isDesktop( context) - ? 20 - : 16, - fontWeight: FontWeight.w600, - color: Color(0xFF000000), + ? 20 + : 16, + fontWeight: FontWeight.w600, + color: Color(0xFF000000), + ), + ) + ], + ), + ) + ], + ), // Space between rows + // Add more rows as needed + ], + ), + ), + SizedBox(height: 15), + Container( + child: Column( + children: [ + Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Container( + width: double.infinity, + alignment: Alignment.centerLeft, + child: Column( + mainAxisAlignment: + MainAxisAlignment.start, + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Form( + key: formKey, + child: Column( + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + buildDropdownField( + 'Service',isRequired: true, + (value) { + setState(() { + serviceId = value; + selectedServiceName = departmentList.firstWhere( + (serList) => serList['id'] == value, + orElse: () => {'name': ''}, + )['name']; + policyNumberId = null; + if (fromClaimsPage == 1) { + fetchPoliciesBasedOnService(serviceId); + } + isServiceValid = true; + }); + }, + fromClaimsPage == 0, + departmentList, + 'name', + serviceId, + ), + if (!isServiceValid) + Text('Please select a service', style: TextStyle(color: Colors.red)), + ], + ), + + // buildDropdownField( + // 'Service', + // (value) { + // setState(() { + // serviceId = value; + // selectedServiceName = + // departmentList + // .firstWhere( + // (serList) => + // serList[ + // 'id'] == + // value, + // orElse: () => + // {'name': ''}, + // )['name']; + // policyNumberId = + // null; + // if (fromClaimsPage == + // 1) { + // fetchPoliciesBasedOnService( + // serviceId); + // } + // }); + // }, + // fromClaimsPage == 0, + // departmentList, + // 'name', + // serviceId, // Pass current serviceId as selectedValue + // ), + SizedBox(height: 15), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + buildDropdownField( + 'Select Policy',isRequired: true, + (value) { + setState(() { + policyNumberId = value; + selectedMemberId = null; + String? selectedPolicyNo = policyNumberList.firstWhere( + (item) => item['id'] == value, + orElse: () => {'policy_no': ''}, + )['policy_no']; + if (selectedPolicyNo != null && fromClaimsPage == 1) { + fetchMemberBasedOnPolicy(selectedPolicyNo); + } + isPolicyValid = true; + }); + }, + fromClaimsPage == 0, + policyNumberList, + 'policy_no', + policyNumberId, + ), + if (!isPolicyValid) + Text('Please select a policy', style: TextStyle(color: Colors.red)), + ], + ), + + // // if (fromClaimsPage == 1) + // buildDropdownField( + // 'Select Policy', + // (value) { + // setState(() { + // policyNumberId = + // value; + // selectedMemberId = + // null; + // String? + // selectedPolicyNo = + // policyNumberList + // .firstWhere( + // (item) => + // item['id'] == + // value, + // orElse: () => + // { + // 'policy_no': '' + // })['policy_no']; + // + // if (selectedPolicyNo != + // null && + // fromClaimsPage == + // 1) { + // fetchMemberBasedOnPolicy( + // selectedPolicyNo); + // } + // }); + // }, + // fromClaimsPage == 0, + // policyNumberList, + // 'policy_no', + // policyNumberId, // Pass current policyNumberId as selectedValue + // ), + SizedBox(height: 15), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + buildDropdownField( + 'Member Name',isRequired: true, + (value) { + setState(() { + selectedMemberId = value; + selectedMemberName = employeePolicyList.firstWhere( + (member) => member['id'] == value, + orElse: () => {'name': ''}, + )['name']; + isMemberValid = true; + }); + }, + false, + employeePolicyList, + 'name', + selectedMemberId, + ), + if (!isMemberValid) + Text('Please select a member', style: TextStyle(color: Colors.red)), + ], + ), +SizedBox(height: 15), + // buildDropdownField( + // 'Member Name', + // (value) { + // setState(() { + // selectedMemberId = + // value; + // selectedMemberName = + // employeePolicyList + // .firstWhere( + // (member) => + // member[ + // 'id'] == + // value, + // orElse: () => + // {'name': ''}, + // )['name']; + // }); + // }, + // false, + // employeePolicyList, + // 'name', + // selectedMemberId, // Pass current selectedMemberId as selectedValue + // ), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // buildTextField('Subject', + // subjectController), + // if (!isSubjectValid) + // Text('Please enter the Subject', style: TextStyle(color: Colors.red)), + SizedBox(height: 15), + buildTextAreaField( + 'Message', + messageController), + SizedBox(height: 15), + // if (policyTypeCondition == + // 3) + // buildTextField( + // 'Accident Details', + // accidentDetailsController), + SizedBox(height: 15), + if (policyTypeCondition == 1 || policyTypeCondition == 72) ...[ + buildTextField('Hospital Name', hospitalNameController, isRequired: true), + if (!isHospitalNameValid) + Text('Please enter the Hospital Name', style: TextStyle(color: Colors.red)), + SizedBox(height: 15), + buildTextAreaField('Hospital Address', hospitalAddressController, isRequired: true), + if (!isHospitalAddressValid) + Text('Please enter the Hospital Address', style: TextStyle(color: Colors.red)), + SizedBox(height: 15), + buildTextField('Hospital City', hospitalCityController,isRequired: true,inputFormatters: [FilteringTextInputFormatter.allow(RegExp(r'[a-zA-Z ]'))]), + if (!isHospitalCityValid) + Text('Please enter the Hospital City', style: TextStyle(color: Colors.red)), + SizedBox(height: 15), + buildTextField('Hospital State', hospitalStateController,isRequired: true,inputFormatters: [FilteringTextInputFormatter.allow(RegExp(r'[a-zA-Z ]'))]), + if (!isHospitalStateValid) + Text('Please enter the Hospital State', style: TextStyle(color: Colors.red)), + SizedBox(height: 15), + buildTextField('Hospital Pincode', hospitalPinCodeController,isRequired: true,keyboardType: TextInputType.number, + inputFormatters: [ + FilteringTextInputFormatter.digitsOnly, + LengthLimitingTextInputFormatter(6), + ]), + if (!isHospitalPincodeValid) + Text('Please enter the Hospital Pincode', style: TextStyle(color: Colors.red)), + SizedBox(height: 15), + buildTextField('Hospital Phone No', hospitalPhoneNoController, + isRequired: true, + keyboardType: TextInputType.number, + inputFormatters: [ + FilteringTextInputFormatter.digitsOnly, + LengthLimitingTextInputFormatter(10), + ],minLength: 10,), + if (!isHospitalPhoneNoValid) + Text( + hospitalPhoneNoController.text.trim().isEmpty + ? 'Please enter the Hospital Phone No' + : 'Hospital Phone No must be 10 digits', // ✅ specific message + style: TextStyle(color: Colors.red), ), - ) ], - ), - ) - ], - ), // Space between rows - // Add more rows as needed - ], + SizedBox(height: 15), + if (serviceId == 2 || + serviceId == 3 || + serviceId == 4) ...[ + buildDatePickerField( + label: + 'Date of Birth', + selectedDate: + birthDate, + allowFuture: false, + onDateSelected: + (selectedDate) { + setState(() { + birthDate = + selectedDate; + }); + }, + ), + SizedBox(height: 15), + buildDatePickerField( + label: 'Accident Date', + isRequired: true, + selectedDate: accidentDate, + allowFuture: false, + minDate: parsedPolicyStartDate, + maxDate: parsedPolicyEndDate, + onDateSelected: (selectedDate) { + setState(() { + if (deathDate != null) deathDate = null; + if (intimationDate != null) intimationDate = null; + accidentDate = selectedDate; + }); + }, + ), + if (!isAccidentDateValid) + Text('Please select the Accident Date', style: TextStyle(color: Colors.red)), + SizedBox(height: 15), + buildDatePickerField( + label: + 'Date of Death', + selectedDate: + deathDate, + isRequired: true, + allowFuture: false, + minDate: accidentDate ?? parsedPolicyStartDate, + maxDate: parsedPolicyEndDate, + onDateSelected: + (selectedDate) { + setState(() { + deathDate = + selectedDate; + }); + }, + ), + SizedBox(height: 15), + buildDatePickerField( + label: 'Date of Intimation', + isRequired: true, + selectedDate: intimationDate, + allowFuture: false, + minDate: accidentDate ?? deathDate ?? parsedPolicyStartDate, + maxDate: parsedPolicyEndDate, + onDateSelected: (selectedDate) { + setState(() { + intimationDate = selectedDate; + }); + }, + ), + if (!isIntimationDateValid) + Text('Please select the Intimation Date', style: TextStyle(color: Colors.red)), + ], + SizedBox(height: 15), + // buildTextAreaField( + // 'Accident Details', + // accidentDetailsController), + // SizedBox(height: 15), + if (policyTypeCondition == 1 || policyTypeCondition == 72) ...[ + // 🟡 Admit Date — no future allowed + buildDatePickerField( + label: 'Admit Date', + isRequired: true, + selectedDate: admitDate, + allowFuture: false, + onDateSelected: (selectedDate) { + setState(() { + admitDate = selectedDate; + dischargeDate = null; // reset discharge when admit changes + }); + }, + ), + if (!isAdmitDateValid) + const Text('Please select the Admit Date', style: TextStyle(color: Colors.red)), + const SizedBox(height: 20), + + // 🟢 Discharge Date — must be after Admit Date + buildDatePickerField( + label: 'Discharge Date', + isRequired: true, + selectedDate: dischargeDate, + allowFuture: true, + minDate: admitDate != null + ? admitDate!.add(const Duration(days: 1)) + : DateTime.now().add(const Duration(days: 1)), + maxDate: null, + onDateSelected: (selectedDate) { + setState(() { + dischargeDate = selectedDate; + }); + }, + ), + if (!isDischargeDateValid) + const Text('Please select the Discharge Date', style: TextStyle(color: Colors.red)), + ], + + + SizedBox(height: 15), + if (serviceId == 1) ...[ + buildTextField('Claims Amount', claimAmountController, isRequired: true, keyboardType: TextInputType.number, + inputFormatters: [ + FilteringTextInputFormatter.digitsOnly, + ]), + if (!isClaimAmountValid) + Text('Please enter the Claims Amount', style: TextStyle(color: Colors.red)), + ], + + SizedBox(height: 15), + if (serviceId == 2 || + serviceId == 3 || + serviceId == 4) + buildTextField( + 'Sum Insured', + sumInsuredController, + isRequired: true, + keyboardType: TextInputType.number, + inputFormatters: [ + FilteringTextInputFormatter.digitsOnly, + ], + ), + ] + ), + SizedBox(height: 15), + // ThemedUploadField( + // hintText: "Upload Documents", + // txtwidth: MediaQuery.of(context).size.width < 600 + // ? MediaQuery.of(context).size.width // Full width on mobile + // : MediaQuery.of(context).size.width * 0.26, // 26% on desktop + // txtheight: 45, + // onFilesSelected: (files) { + // print("Picked files: ${files.map((f) => f.name).toList()}"); + // setState(() { + // uploadedFiles = files; // store all selected files + // }); + // }, + // ), + Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + children: [ + MultiFileUploadWidget(isRequired: true,showError: showFileError), + ], + ), + ), + ], + ), + ), + ], + ), ), ), SizedBox(height: 15), @@ -1615,32 +2066,38 @@ class _planclaimsformState extends State { ); } - Widget buildTextField( String label, TextEditingController controller, { + bool isRequired = false, // 👈 ADD THIS TextInputType keyboardType = TextInputType.text, List? inputFormatters, + int? minLength, }) { return TextFormField( controller: controller, keyboardType: keyboardType, inputFormatters: inputFormatters, - decoration: InputDecoration(labelText: label), + decoration: InputDecoration( + labelText: isRequired ? '$label *' : label, // 👈 ADD THIS + ), validator: (value) { if (value == null || value.isEmpty) { return 'Please enter the $label'; } + if (minLength != null && value.trim().length < minLength) { // 👈 ADD this + return '$label must be at least $minLength digits'; + } return null; }, ); } - Widget buildTextAreaField(String label, TextEditingController controller) { + Widget buildTextAreaField(String label, TextEditingController controller ,{ bool isRequired = false}) { return TextFormField( controller: controller, - decoration: InputDecoration(labelText: label), + decoration: InputDecoration(labelText: isRequired ? '$label *' : label), maxLines: 5, validator: (value) { if (value == null || value.isEmpty) { @@ -1656,6 +2113,7 @@ class _planclaimsformState extends State { required DateTime? selectedDate, required bool allowFuture, required ValueChanged onDateSelected, + bool isRequired = false, DateTime? minDate, DateTime? maxDate, }) { @@ -1706,7 +2164,7 @@ class _planclaimsformState extends State { }, child: InputDecorator( decoration: InputDecoration( - labelText: label, + labelText: isRequired ? '$label *' : label, border: const OutlineInputBorder(), ), child: Text( @@ -1726,11 +2184,12 @@ class _planclaimsformState extends State { bool readOnly, List> itemsList, String displayField, - int? selectedValue // Added selectedValue parameter + int? selectedValue, // Added selectedValue parameter + {bool isRequired = false,} ) { return DropdownButtonFormField( value: selectedValue, - decoration: InputDecoration(labelText: label), + decoration: InputDecoration(labelText: isRequired ? '$label *' : label,), items: itemsList.map>((item) { return DropdownMenuItem( value: item['id'], // Ensure 'id' is correctly referenced diff --git a/lib/pages/postEnrollment/service/api_service.dart b/lib/pages/postEnrollment/service/api_service.dart index 037457f..ccb4317 100755 --- a/lib/pages/postEnrollment/service/api_service.dart +++ b/lib/pages/postEnrollment/service/api_service.dart @@ -330,12 +330,8 @@ class ApiService { } Future _clearLocalStorageAndRedirect() async { - final prefs = await SharedPreferences.getInstance(); - await prefs.clear(); - // Assuming you have access to the context - ToastHelper.showErrorToast(context, 'Session Out'); - context.go('/login'); - // Navigator.pushNamed(context, 'login'); + if (!context.mounted) return; + await TokenService().logout(context); } Future> getBotDetails( @@ -425,7 +421,12 @@ class ApiService { ToastHelper.showWarningToast(context, message); return {}; } else if (response.statusCode == 429) { +<<<<<<< HEAD final body = jsonDecode(response.body); +======= + if (!context.mounted) return {}; + final body = jsonDecode(response.body); +>>>>>>> 24f532f750daddbb5d414d9056a4e1c4c760a2c4 final message = body['message']; ToastHelper.showWarningToast(context, message); return {}; diff --git a/lib/pages/postEnrollment/service/multi_file_upload_widget.dart b/lib/pages/postEnrollment/service/multi_file_upload_widget.dart index 018d1ed..b89720f 100755 --- a/lib/pages/postEnrollment/service/multi_file_upload_widget.dart +++ b/lib/pages/postEnrollment/service/multi_file_upload_widget.dart @@ -4,8 +4,15 @@ import 'file_upload_service.dart'; class MultiFileUploadWidget extends StatefulWidget { final bool forceMobile; + final bool isRequired; + final bool showError; // ✅ ADD THIS - const MultiFileUploadWidget({super.key, this.forceMobile = false}); + const MultiFileUploadWidget({ + super.key, + this.forceMobile = false, + this.isRequired = false, + this.showError = false, // ✅ ADD THIS + }); @override State createState() => _MultiFileUploadWidgetState(); @@ -18,27 +25,12 @@ class _MultiFileUploadWidgetState extends State { String? errorMessage; void _pickFiles() async { - final error = await fileService.pickFiles(maxFileSizeInMB: 10); // 5 MB limit + final error = await fileService.pickFiles(maxFileSizeInMB: 10); if (error != null) { if (mounted) { setState(() { errorMessage = error; }); - - // also show alert dialog for big error messages - // showDialog( - // context: context, - // builder: (ctx) => AlertDialog( - // title: const Text("File Upload Error"), - // content: Text(error), - // actions: [ - // TextButton( - // onPressed: () => Navigator.pop(ctx), - // child: const Text("OK"), - // ), - // ], - // ), - // ); } } else { setState(() { @@ -55,6 +47,31 @@ class _MultiFileUploadWidgetState extends State { }); } + // ✅ Build the button label with RichText to show * in red + Widget _buildButtonLabel() { + if (!widget.isRequired) { + return const Text( + "Upload Documents", + style: TextStyle(fontSize: 14, color: Colors.black), + overflow: TextOverflow.ellipsis, + ); + } + return RichText( + text: const TextSpan( + children: [ + TextSpan( + text: "Upload Documents ", + style: TextStyle(fontSize: 14, color: Colors.black), + ), + TextSpan( + text: "*", // ✅ red * like other fields + style: TextStyle(fontSize: 14, color: Colors.red), + ), + ], + ), + ); + } + @override Widget build(BuildContext context) { final files = fileService.files; @@ -67,21 +84,14 @@ class _MultiFileUploadWidgetState extends State { onPressed: _pickFiles, icon: const Icon(Icons.file_upload_outlined, color: Color(0xFFE26728), size: 24), - label: const Text( - "Upload Documents", - style: TextStyle( - fontSize: 14, - color: Colors.black, - ), - overflow: TextOverflow.ellipsis, - ), + label: _buildButtonLabel(), // ✅ use rich text label style: OutlinedButton.styleFrom( side: const BorderSide(color: Color(0xFFE26728)), ), ), const SizedBox(height: 6), const Text( - "Supports only PDF, PNG, JPG, JPEG,HEIC formats (max 10 MB each)", + "Supports only PDF, PNG, JPG, JPEG, HEIC formats (max 10 MB each)", style: TextStyle(fontSize: 12, color: Colors.grey), ), ] else ...[ @@ -91,14 +101,7 @@ class _MultiFileUploadWidgetState extends State { onPressed: _pickFiles, icon: const Icon(Icons.file_upload_outlined, color: Color(0xFFE26728), size: 24), - label: const Text( - "Upload Documents", - style: TextStyle( - fontSize: 14, - color: Colors.black, - ), - overflow: TextOverflow.ellipsis, - ), + label: _buildButtonLabel(), // ✅ use rich text label style: OutlinedButton.styleFrom( side: const BorderSide(color: Color(0xFFE26728)), ), @@ -106,7 +109,7 @@ class _MultiFileUploadWidgetState extends State { const SizedBox(width: 12), const Expanded( child: Text( - "Supports only PDF, PNG, JPG, JPEG,HEIC formats (max 10 MB each)", + "Supports only PDF, PNG, JPG, JPEG, HEIC formats (max 10 MB each)", style: TextStyle(fontSize: 12, color: Colors.grey), overflow: TextOverflow.ellipsis, ), @@ -115,10 +118,11 @@ class _MultiFileUploadWidgetState extends State { ), ], - if (fileService.files.isEmpty && errorMessage == null) ...[ + // ✅ Show error only when required AND no files + if (widget.isRequired && widget.showError && files.isEmpty && errorMessage == null) ...[ const SizedBox(height: 4), const Text( - "Required", + 'Please upload the document', style: TextStyle(color: Colors.red, fontSize: 12), ), ], @@ -134,7 +138,7 @@ class _MultiFileUploadWidgetState extends State { const SizedBox(height: 8), ...fileService.files.asMap().entries.map((entry) { final index = entry.key; - final uploaded = entry.value; // UploadedFile + final uploaded = entry.value; return Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -142,7 +146,8 @@ class _MultiFileUploadWidgetState extends State { ListTile( dense: true, contentPadding: EdgeInsets.zero, - title: Text(uploaded.file.name, style: const TextStyle(fontSize: 14)), + title: Text(uploaded.file.name, + style: const TextStyle(fontSize: 14)), trailing: IconButton( icon: const Icon(Icons.close, color: Colors.red), onPressed: () => _removeFile(index), diff --git a/lib/pages/setPassword.dart b/lib/pages/setPassword.dart index 1bc30e7..bfef7a2 100755 --- a/lib/pages/setPassword.dart +++ b/lib/pages/setPassword.dart @@ -66,6 +66,14 @@ class _setPasswordState extends State { final confirmPassword = confirmPasswordController.text.trim(); try { if (_formKey.currentState!.validate()) { + final strongPasswordRegex = + RegExp(r'^(?=.*[A-Za-z])(?=.*\d)(?=.*[@$!%*#?&]).{8,}$'); + if (!strongPasswordRegex.hasMatch(newPassword)) { + ToastHelper.showErrorToast( + context, + 'Enter a valid password (8+ chars with letter, number, special char)'); + return; + } if (confirmPassword != newPassword) { ToastHelper.showErrorToast(context, 'Passwords do not match'); return; @@ -213,15 +221,20 @@ class _setPasswordState extends State { } return WillPopScope( onWillPop: () async { - // Close the app on mobile back button press - exit(0); // This will exit the app - return false; // Return false to prevent any other actions + // 🖥 Desktop / Web → always go back to login + if (kIsWeb || Responsive.isDesktop(context)) { + context.go('/login'); + return false; + } + // 📱 Mobile → keep existing behavior (exit app) + exit(0); + return false; }, child: Scaffold( body: SingleChildScrollView( keyboardDismissBehavior: ScrollViewKeyboardDismissBehavior.onDrag, child: Container( - height: _size.height, + constraints: BoxConstraints(minHeight: _size.height), color: Colors.white, child: Stack( children: [ @@ -308,7 +321,7 @@ class _setPasswordState extends State { // ), Container( margin: marginInsets, - alignment: Alignment.bottomCenter, + alignment: Alignment.center, child: SingleChildScrollView( child: Form( key: _formKey, @@ -353,8 +366,7 @@ class _setPasswordState extends State { Expanded( flex: 12, child: Align( - alignment: Alignment - .topLeft, // ✅ Always top-left + alignment: Alignment.center, child: _size.width <= 1100 ? Image.asset( 'assets/nhance_app_logo.png', @@ -378,10 +390,9 @@ class _setPasswordState extends State { ), SizedBox( height: Responsive.isDesktop(context) - ? _size.height * 0.1 - : 10, + ? null + : _size.height * 0.2, ), - SizedBox(height: 10), Container( margin: Responsive.isDesktop(context) ? EdgeInsets.symmetric( @@ -464,6 +475,25 @@ class _setPasswordState extends State { }, ), ), + validator: (value) { + final password = value ?? ''; + if (password.isEmpty) { + return 'Please enter your new password'; + } + if (password.length < 8) { + return 'Password must be at least 8 characters'; + } + if (!RegExp(r'[A-Za-z]').hasMatch(password)) { + return 'Password must include at least 1 letter'; + } + if (!RegExp(r'\d').hasMatch(password)) { + return 'Password must include at least 1 number'; + } + if (!RegExp(r'[@$!%*#?&]').hasMatch(password)) { + return 'Password must include at least 1 special character'; + } + return null; + }, ), ), const SizedBox(height: 8), @@ -604,10 +634,10 @@ class _setPasswordState extends State { ), ), ), - + // _size.height * 0.3 SizedBox( height: Responsive.isDesktop(context) - ? _size.height * 0.3 + ? null : _size.height * 0.2, ), // SizedBox(