FIX_Tracker task

This commit is contained in:
sanjeev.p 2026-03-13 10:26:57 +05:30
parent df4d01ee6e
commit bf729885a3
17 changed files with 859 additions and 613 deletions

View File

@ -90,7 +90,7 @@ class _SplashScreenState extends State<SplashScreen> {
Future<String?> tokenRedirectLogic(
BuildContext context, GoRouterState state) async {
print('ABCDEFGH');
const allowedWithoutToken = [
const guestRoutes = [
'/login',
'/verify',
'/mailVerify',
@ -98,17 +98,17 @@ Future<String?> 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

View File

@ -67,6 +67,12 @@ class _changesPasswordState extends State<changesPassword> {
final confirmPassword = confirmPasswordController.text.trim();
try {
if (_formKey.currentState!.validate()) {
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;
@ -196,7 +202,7 @@ class _changesPasswordState extends State<changesPassword> {
body: SingleChildScrollView(
keyboardDismissBehavior: ScrollViewKeyboardDismissBehavior.onDrag,
child: Container(
height: _size.height,
constraints: BoxConstraints(minHeight: _size.height),
color: Colors.white,
child: Stack(
children: [
@ -283,7 +289,7 @@ class _changesPasswordState extends State<changesPassword> {
// ),
Container(
margin: marginInsets,
alignment: Alignment.bottomCenter,
alignment: Alignment.topCenter,
child: SingleChildScrollView(
child: Form(
key: _formKey,
@ -301,62 +307,13 @@ class _changesPasswordState extends State<changesPassword> {
mainAxisAlignment:
MainAxisAlignment.center,
children: [
if (!Responsive.isMobile(context) &&
!Responsive.isTablet(context))
Row(
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,
),
),
),
],
),
// Removed desktop-only back icon and logo to eliminate extra top spacing
SizedBox(
height: Responsive.isDesktop(context)
? _size.height * 0.1
? null
: 10,
),
SizedBox(height: 10),
// SizedBox(height: 10),
Container(
margin: Responsive.isDesktop(context)
? EdgeInsets.symmetric(
@ -508,6 +465,25 @@ class _changesPasswordState extends State<changesPassword> {
},
),
),
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),
@ -651,14 +627,14 @@ class _changesPasswordState extends State<changesPassword> {
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(

View File

@ -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<String, dynamic>;
// 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<String, dynamic>;
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<String, dynamic>?; // 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<String, dynamic>?;
return planclaimsform(details: details);
},
),
GoRoute(
path: '/retailClaimForm',
builder: (context, state) {
final details = state.extra as Map<String, dynamic>?;
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 {

View File

@ -474,7 +474,8 @@ class _MyEmailVerifyState extends State<MyEmailVerify> {
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');
@ -649,10 +650,12 @@ class _MyEmailVerifyState extends State<MyEmailVerify> {
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');
}
@ -703,10 +706,12 @@ class _MyEmailVerifyState extends State<MyEmailVerify> {
if (response.statusCode == 200) {
Map<String, dynamic> 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,
@ -929,7 +934,7 @@ class _MyEmailVerifyState extends State<MyEmailVerify> {
),
Container(
margin: marginInsets,
alignment: Alignment.bottomCenter,
alignment: Alignment.center,
child: SingleChildScrollView(
child: Form(
key: _formKey,
@ -1269,14 +1274,14 @@ class _MyEmailVerifyState extends State<MyEmailVerify> {
// ),
SizedBox(
height: Responsive.isDesktop(context)
? _size.height * 0.4
: _size.height * 0.3,
? _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(

View File

@ -1428,15 +1428,14 @@ class _addOnsDetailsState extends State<addOnsDetails> {
Future<void> 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<Map<String, dynamic>> siData = [
@ -4520,8 +4519,7 @@ class _addOnsDetailsState extends State<addOnsDetails> {
checkDependentTopUp();
sendAddonsGmcDependentToAPI();
}
context.go('/empReviewDetails');
// Navigator.pushNamed(context,
context.push('/empReviewDetails'); // Navigator.pushNamed(context,
// 'empReviewDetails');
}
: null,

View File

@ -976,7 +976,7 @@ class _empDetailsState extends State<empDetails> {
alignment: Alignment.centerRight,
child: ElevatedButton(
onPressed: () {
context.go('/addOnsDetails');
context.push('/addOnsDetails');
// Navigator.pushNamed(
// context, 'addOnsDetails');
},
@ -2240,6 +2240,8 @@ class _empDetailsState extends State<empDetails> {
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,47 +2637,134 @@ class _empDetailsState extends State<empDetails> {
Column(
children: familyFloaterContainers,
),
// Add Family Member Button HERE
// if (gmcOpenForEnrollment != 0 && gmcECardDownload == null)...[
if(gmcOpenForEnrollment != 0 && gmcECardDownload == null && getFalseObjects.isNotEmpty)...[
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') ...<Widget>[
if (gmcOpenForEnrollment != 0 &&
gmcECardDownload == null &&
getFalseObjects.isNotEmpty &&
gmcRelationShip.any((rel) =>
(rel is Map
? rel['relationship']?.toString().toLowerCase()
: rel?.toString().toLowerCase() ?? '') != 'self')) ...<Widget>[
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 ...<Widget>[
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 ...<Widget>[
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(

View File

@ -252,7 +252,7 @@ class _loginState extends State<login> {
print('isEmailFieldVisible $isEmailFieldVisible');
// prefs.setString('empEmail', emailController.text);
print('${emailMobileController.text}');
context.push(
context.go(
'/mailVerify',
extra: {
'type': 'email',
@ -261,7 +261,7 @@ class _loginState extends State<login> {
);
} else {
// _verifyPhoneNumber();
context.push(
context.go(
'/mailVerify',
extra: {
'type': 'mobile',
@ -1097,7 +1097,7 @@ class _loginState extends State<login> {
child: Image.asset(
'assets/nhance_app_logo.png',
width: 150,
height: 100,
height: 150,
)),
),
if (!Responsive.isMobile(context) &&
@ -1148,7 +1148,7 @@ class _loginState extends State<login> {
),
Container(
margin: marginInsets,
alignment: Alignment.bottomCenter,
alignment: Alignment.center,
child: SingleChildScrollView(
child: Form(
key: _formKey,
@ -1159,9 +1159,7 @@ class _loginState extends State<login> {
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,
@ -1677,119 +1675,114 @@ class _loginState extends State<login> {
},
),
),
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(
'Didnt 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 09
],
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(
'Didnt Receive Code?',
style: GoogleFonts.poppins(
color: Colors.blue,
fontSize: 14,
),
),
),
),
],
)
: const SizedBox
.shrink(), // Empty widget when false
),
],
),
],
SizedBox(height: 20),

View File

@ -127,13 +127,15 @@ class _AddPolicyScreenState extends State<AddPolicyScreen> {
policyNoController.clear();
expDateController.clear();
setState(() => isLoading = false);
context.go('/home');
}
}
} catch (e) {
print('Submit error: $e');
} finally {
if (mounted) {
setState(() => isLoading = false);
}
}
}

View File

@ -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:

View File

@ -311,11 +311,18 @@ class _claimsState extends State<claims> {
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<claims> {
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(

View File

@ -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 ?? '',

View File

@ -336,31 +336,31 @@ class _helpState extends State<help> {
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: [

View File

@ -1880,10 +1880,44 @@ class _HomeState extends State<Home> {
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<Widget> cards) {
final int totalCards = cards.length;
final bool hasAddCard = isActive; // because you add AddCard only when isActive

View File

@ -132,6 +132,7 @@ class _planclaimsformState extends State<planclaimsform> {
bool isIntimationDateValid = true;
bool isAdmitDateValid = true;
bool isDischargeDateValid = true;
bool showFileError = false;
final session = SessionManager();
@ -541,55 +542,70 @@ class _planclaimsformState extends State<planclaimsform> {
}
Future<void> 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);
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 &&
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;
});
@ -658,7 +674,8 @@ class _planclaimsformState extends State<planclaimsform> {
_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() ?? ''));
@ -674,7 +691,10 @@ class _planclaimsformState extends State<planclaimsform> {
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;
}
}
@ -715,33 +735,20 @@ class _planclaimsformState extends State<planclaimsform> {
// 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));
}
}
}
@ -762,31 +769,25 @@ class _planclaimsformState extends State<planclaimsform> {
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
}
}
@ -878,8 +879,7 @@ class _planclaimsformState extends State<planclaimsform> {
canPop: false,
onPopInvokedWithResult: (didPop, result) {
if (didPop) return;
final route = fromClaimsPage == 0 ? 'claims' : 'help';
context.go('/$route');
context.pop();
},
child: Scaffold(
backgroundColor: Colors.white,
@ -956,8 +956,7 @@ class _planclaimsformState extends State<planclaimsform> {
flex: 1,
child: InkWell(
onTap: () {
final route = fromClaimsPage == 0 ? 'claims' : 'help';
context.go('/$route');
context.pop();
},
child: Icon(
Icons
@ -1025,8 +1024,8 @@ class _planclaimsformState extends State<planclaimsform> {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
buildDropdownField(
'Service',
(value) {
'Service',isRequired: true,
(value) {
setState(() {
serviceId = value;
selectedServiceName = departmentList.firstWhere(
@ -1084,8 +1083,8 @@ class _planclaimsformState extends State<planclaimsform> {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
buildDropdownField(
'Select Policy',
(value) {
'Select Policy',isRequired: true,
(value) {
setState(() {
policyNumberId = value;
selectedMemberId = null;
@ -1149,8 +1148,8 @@ class _planclaimsformState extends State<planclaimsform> {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
buildDropdownField(
'Member Name',
(value) {
'Member Name',isRequired: true,
(value) {
setState(() {
selectedMemberId = value;
selectedMemberName = employeePolicyList.firstWhere(
@ -1212,37 +1211,44 @@ SizedBox(height: 15),
// accidentDetailsController),
SizedBox(height: 15),
if (policyTypeCondition == 1 || policyTypeCondition == 72) ...[
buildTextField('Hospital Name', hospitalNameController),
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),
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),
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),
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,keyboardType: TextInputType.number,
inputFormatters: [
FilteringTextInputFormatter.digitsOnly,
LengthLimitingTextInputFormatter(6), // optional
]),
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,keyboardType: TextInputType.number,
inputFormatters: [
FilteringTextInputFormatter.digitsOnly,
LengthLimitingTextInputFormatter(10), // optional
]),
buildTextField('Hospital Phone No', hospitalPhoneNoController,
isRequired: true,
keyboardType: TextInputType.number,
inputFormatters: [
FilteringTextInputFormatter.digitsOnly,
LengthLimitingTextInputFormatter(10),
],minLength: 10,),
if (!isHospitalPhoneNoValid)
Text('Please enter the Hospital Phone No', style: TextStyle(color: Colors.red)),
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),
),
],
SizedBox(height: 15),
if (serviceId == 2 ||
@ -1265,6 +1271,7 @@ SizedBox(height: 15),
SizedBox(height: 15),
buildDatePickerField(
label: 'Accident Date',
isRequired: true,
selectedDate: accidentDate,
allowFuture: false,
minDate: parsedPolicyStartDate,
@ -1285,6 +1292,7 @@ SizedBox(height: 15),
'Date of Death',
selectedDate:
deathDate,
isRequired: true,
allowFuture: false,
minDate: accidentDate ?? parsedPolicyStartDate,
maxDate: parsedPolicyEndDate,
@ -1299,6 +1307,7 @@ SizedBox(height: 15),
SizedBox(height: 15),
buildDatePickerField(
label: 'Date of Intimation',
isRequired: true,
selectedDate: intimationDate,
allowFuture: false,
minDate: accidentDate ?? deathDate ?? parsedPolicyStartDate,
@ -1321,6 +1330,7 @@ SizedBox(height: 15),
// 🟡 Admit Date no future allowed
buildDatePickerField(
label: 'Admit Date',
isRequired: true,
selectedDate: admitDate,
allowFuture: false,
onDateSelected: (selectedDate) {
@ -1337,6 +1347,7 @@ SizedBox(height: 15),
// 🟢 Discharge Date must be after Admit Date
buildDatePickerField(
label: 'Discharge Date',
isRequired: true,
selectedDate: dischargeDate,
allowFuture: true,
minDate: admitDate != null
@ -1356,7 +1367,7 @@ SizedBox(height: 15),
SizedBox(height: 15),
if (serviceId == 1) ...[
buildTextField('Claims Amount', claimAmountController, keyboardType: TextInputType.number,
buildTextField('Claims Amount', claimAmountController, isRequired: true, keyboardType: TextInputType.number,
inputFormatters: [
FilteringTextInputFormatter.digitsOnly,
]),
@ -1371,6 +1382,7 @@ SizedBox(height: 15),
buildTextField(
'Sum Insured',
sumInsuredController,
isRequired: true,
keyboardType: TextInputType.number,
inputFormatters: [
FilteringTextInputFormatter.digitsOnly,
@ -1396,7 +1408,7 @@ SizedBox(height: 15),
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
const MultiFileUploadWidget(),
MultiFileUploadWidget(isRequired: true,showError: showFileError),
],
),
),
@ -1532,32 +1544,38 @@ SizedBox(height: 15),
);
}
Widget buildTextField(
String label,
TextEditingController controller, {
bool isRequired = false, // 👈 ADD THIS
TextInputType keyboardType = TextInputType.text,
List<TextInputFormatter>? 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) {
@ -1573,6 +1591,7 @@ SizedBox(height: 15),
required DateTime? selectedDate,
required bool allowFuture,
required ValueChanged<DateTime?> onDateSelected,
bool isRequired = false,
DateTime? minDate,
DateTime? maxDate,
}) {
@ -1623,7 +1642,7 @@ SizedBox(height: 15),
},
child: InputDecorator(
decoration: InputDecoration(
labelText: label,
labelText: isRequired ? '$label *' : label,
border: const OutlineInputBorder(),
),
child: Text(
@ -1643,11 +1662,12 @@ SizedBox(height: 15),
bool readOnly,
List<Map<String, dynamic>> itemsList,
String displayField,
int? selectedValue // Added selectedValue parameter
int? selectedValue, // Added selectedValue parameter
{bool isRequired = false,}
) {
return DropdownButtonFormField<int>(
value: selectedValue,
decoration: InputDecoration(labelText: label),
decoration: InputDecoration(labelText: isRequired ? '$label *' : label,),
items: itemsList.map<DropdownMenuItem<int>>((item) {
return DropdownMenuItem<int>(
value: item['id'], // Ensure 'id' is correctly referenced

View File

@ -322,12 +322,8 @@ class ApiService {
}
Future<void> _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<Map<String, dynamic>> getBotDetails(
@ -407,6 +403,7 @@ class ApiService {
await _clearLocalStorageAndRedirect();
return {};
} else if (response.statusCode == 429) {
if (!context.mounted) return {};
final body = jsonDecode(response.body);
final message = body['message'];
ToastHelper.showWarningToast(context, message);

View File

@ -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<MultiFileUploadWidget> createState() => _MultiFileUploadWidgetState();
@ -18,27 +25,12 @@ class _MultiFileUploadWidgetState extends State<MultiFileUploadWidget> {
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<MultiFileUploadWidget> {
});
}
// 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<MultiFileUploadWidget> {
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<MultiFileUploadWidget> {
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<MultiFileUploadWidget> {
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<MultiFileUploadWidget> {
),
],
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<MultiFileUploadWidget> {
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<MultiFileUploadWidget> {
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),

View File

@ -66,6 +66,14 @@ class _setPasswordState extends State<setPassword> {
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;
@ -182,15 +190,20 @@ class _setPasswordState extends State<setPassword> {
}
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: [
@ -277,7 +290,7 @@ class _setPasswordState extends State<setPassword> {
// ),
Container(
margin: marginInsets,
alignment: Alignment.bottomCenter,
alignment: Alignment.center,
child: SingleChildScrollView(
child: Form(
key: _formKey,
@ -322,8 +335,7 @@ class _setPasswordState extends State<setPassword> {
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',
@ -347,10 +359,9 @@ class _setPasswordState extends State<setPassword> {
),
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(
@ -433,6 +444,25 @@ class _setPasswordState extends State<setPassword> {
},
),
),
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),
@ -573,10 +603,10 @@ class _setPasswordState extends State<setPassword> {
),
),
),
// _size.height * 0.3
SizedBox(
height: Responsive.isDesktop(context)
? _size.height * 0.3
? null
: _size.height * 0.2,
),
// SizedBox(