diff --git a/lib/customAppBar/side_bar.dart b/lib/customAppBar/side_bar.dart index 504f4b4..1742aa7 100644 --- a/lib/customAppBar/side_bar.dart +++ b/lib/customAppBar/side_bar.dart @@ -32,6 +32,7 @@ class _NhanceSideBarState extends State { List> sideMenuItems = []; List postModules = []; List enrollmentModules = []; + List claimsSubMenu = []; @override void initState() { @@ -59,8 +60,26 @@ class _NhanceSideBarState extends State { ? List.from(jsonDecode(postRaw)) : []; + final claimsSubMenuRaw = await tokenService.readValue('claims_sub_menu'); + if (claimsSubMenuRaw != null && claimsSubMenuRaw.isNotEmpty) { + claimsSubMenu = List.from(jsonDecode(claimsSubMenuRaw)) + .map((e) => e.toString()) + .toList(); + } else { + // Fallback: decoded JWT / selected branch may still have the list. + final decoded = tokenService.getDecodedToken(); + final branch = tokenService.getSelectedBranch(); + final fromDecoded = decoded?['claims_sub_menu']; + final fromBranch = branch?['claims_sub_menu']; + final source = fromBranch is List + ? fromBranch + : (fromDecoded is List ? fromDecoded : const []); + claimsSubMenu = source.map((e) => e.toString()).toList(); + } + logDebug('enrollmentModules $enrollmentModules'); logDebug('postModules $postModules'); + logDebug('claimsSubMenu $claimsSubMenu'); final List> items = []; @@ -82,8 +101,9 @@ class _NhanceSideBarState extends State { }); } - // CLAIMS - if (postModules.contains(4)) { + // CLAIMS — only when module 4 and at least one sub-menu is allowed + if (postModules.contains(4) && + (claimsSubMenu.contains('EB') || claimsSubMenu.contains('Non-EB'))) { items.add({ 'route': 'ClaimsPolicies', 'label': 'Claims', @@ -173,8 +193,10 @@ class _NhanceSideBarState extends State { child: Column( mainAxisSize: MainAxisSize.min, children: [ - _buildClaimsMenuItem('EB', 'ClaimsPolicies'), - _buildClaimsMenuItem('Non-EB', 'nonEBClaimsList'), + if (claimsSubMenu.contains('EB')) + _buildClaimsMenuItem('EB', 'ClaimsPolicies'), + if (claimsSubMenu.contains('Non-EB')) + _buildClaimsMenuItem('Non-EB', 'nonEBClaimsList'), ], ), ), diff --git a/lib/main.dart b/lib/main.dart index 4bfe4da..1cc85a2 100755 --- a/lib/main.dart +++ b/lib/main.dart @@ -181,6 +181,52 @@ Future startApp() async { runApp(MyApp(initialToken: token)); } +/// Routes that require an authenticated HR session. +/// Public auth-flow routes (hrLogin, mailVerify, branchSelection) stay excluded. +const Set _authProtectedRoutes = { + 'hrHome', + 'hrDashboard', + 'policies', + 'claimsOverviewDashboard', + 'ClaimsPolicies', + 'nonEBClaimsList', + 'CdPoliciesList', + 'cdTransactionDetails', + 'hrPolicyDetails', + 'oldPolicy', + 'preFileUpload', + 'postFileUpload', + 'excelErrorScreen', + 'empDetails', + 'addOnsDetails', + 'empReviewDetails', +}; + +String _normalizeRouteName(String routeName) { + var name = routeName.trim(); + if (name.startsWith('#')) { + name = name.substring(1); + } + if (name.startsWith('/')) { + name = name.substring(1); + } + // Flutter web may include query/hash fragments; keep only path segment. + final hashIndex = name.indexOf('#'); + if (hashIndex != -1) { + name = name.substring(hashIndex + 1); + } + final queryIndex = name.indexOf('?'); + if (queryIndex != -1) { + name = name.substring(0, queryIndex); + } + return name; +} + +bool _hasValidSession() { + final token = TokenStorageService().getCurrentToken(); + return token != null && token.trim().isNotEmpty; +} + class MyApp extends StatelessWidget { final String? initialToken; @@ -188,12 +234,33 @@ class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { + final fallbackRoute = + (initialToken == null || initialToken!.isEmpty) ? 'hrLogin' : 'hrHome'; + return MaterialApp( title: 'Nhance HR', debugShowCheckedModeBanner: false, - initialRoute: (initialToken == null || initialToken!.isEmpty) - ? 'hrLogin' - : 'hrHome', + initialRoute: fallbackRoute, + onGenerateInitialRoutes: (String initialRouteName) { + final requested = _normalizeRouteName(initialRouteName); + final resolved = (requested.isEmpty || requested == '/') + ? fallbackRoute + : requested; + + // Deep-link guard: unauthenticated access to protected URLs → HR login + final targetRoute = + (!_hasValidSession() && _authProtectedRoutes.contains(resolved)) + ? 'hrLogin' + : resolved; + + final builder = appRoutes[targetRoute] ?? appRoutes['hrLogin']!; + return [ + MaterialPageRoute( + builder: builder, + settings: RouteSettings(name: targetRoute), + ), + ]; + }, theme: ThemeData( primaryColor: const Color(0xFF00999E), scaffoldBackgroundColor: Colors.white, @@ -225,110 +292,159 @@ final Map appRoutes = { // resendToken: null, // onResendCode: (String, int) {}, // ), - 'hrHome': (context) => MyHrHome(), - 'preFileUpload': (context) => const preFileUpload( - ClientId: '', - policyTypeId: '', - ClientPoliyId: '', - clientBranchId: '', - Token: '', - TokenType: '', - cardType: '', - cardPolicyNo: '', - cardInsurer_name: '', - cardPolicy_name: '', - cardPolicy_ExpDate: '', - total_premium: '', + 'hrHome': (context) => AuthWrapper( + builder: (_) => MyHrHome(), ), - 'postFileUpload': (context) => const postFileUpload( - ClientId: '', - policyTypeId: '', - ClientPoliyId: '', - clientBranchId: '', - Token: '', - TokenType: '', - cardType: '', - cardPolicyNo: '', - cardInsurer_name: '', - cardPolicy_name: '', - cardPolicy_ExpDate: '', - total_premium: '', + 'preFileUpload': (context) => AuthWrapper( + builder: (_) => const preFileUpload( + ClientId: '', + policyTypeId: '', + ClientPoliyId: '', + clientBranchId: '', + Token: '', + TokenType: '', + cardType: '', + cardPolicyNo: '', + cardInsurer_name: '', + cardPolicy_name: '', + cardPolicy_ExpDate: '', + total_premium: '', + ), ), - 'excelErrorScreen': (context) => const excelErrorScreen( - ClientId: '', - policy_no: '', - action: '', - created_at: '', - clientBranchId: '', - Token: '', - TokenType: '', - id: ''), - 'empDetails': (context) => empDetails(), - 'addOnsDetails': (context) => addOnsDetails(), - 'empReviewDetails': (context) => empReviewDetails(), - 'hrDashboard': (context) => policies(), - 'claimsOverviewDashboard': (context) => const ClaimsOverviewDashboard(), - 'hrPolicyDetails': (context) => hrPolicyDetails( - ClientId: '', - policyTypeId: '', - ClientPoliyId: '', - clientBranchId: '', - Token: '', - TokenType: '', - cardType: '', - cardPolicyNo: '', - cardInsurer_name: '', - cardPolicy_name: '', - cardPolicy_ExpDate: '', - total_premium: '', - is_ecard_bulk_download_for_employee: 0, + 'postFileUpload': (context) => AuthWrapper( + builder: (_) => const postFileUpload( + ClientId: '', + policyTypeId: '', + ClientPoliyId: '', + clientBranchId: '', + Token: '', + TokenType: '', + cardType: '', + cardPolicyNo: '', + cardInsurer_name: '', + cardPolicy_name: '', + cardPolicy_ExpDate: '', + total_premium: '', + ), ), - 'oldPolicy': (context) => oldPolicy(), + 'excelErrorScreen': (context) => AuthWrapper( + builder: (_) => const excelErrorScreen( + ClientId: '', + policy_no: '', + action: '', + created_at: '', + clientBranchId: '', + Token: '', + TokenType: '', + id: ''), + ), + 'empDetails': (context) => AuthWrapper( + builder: (_) => empDetails(), + ), + 'addOnsDetails': (context) => AuthWrapper( + builder: (_) => addOnsDetails(), + ), + 'empReviewDetails': (context) => AuthWrapper( + builder: (_) => empReviewDetails(), + ), + 'hrDashboard': (context) => AuthWrapper( + builder: (_) => const policies(), + ), + 'claimsOverviewDashboard': (context) => AuthWrapper( + builder: (_) => const ClaimsOverviewDashboard(), + ), + 'hrPolicyDetails': (context) => AuthWrapper( + builder: (_) => hrPolicyDetails( + ClientId: '', + policyTypeId: '', + ClientPoliyId: '', + clientBranchId: '', + Token: '', + TokenType: '', + cardType: '', + cardPolicyNo: '', + cardInsurer_name: '', + cardPolicy_name: '', + cardPolicy_ExpDate: '', + total_premium: '', + is_ecard_bulk_download_for_employee: 0, + ), + ), + 'oldPolicy': (context) => AuthWrapper( + builder: (_) => oldPolicy(), + ), + // Branch selection happens after login but before a branch token is set. 'branchSelection': (context) => BranchSelectionPage(), - 'policies': (context) => policies(), - 'CdPoliciesList': (context) => CdPoliciesList(), - 'ClaimsPolicies': (context) => ClaimsPolicies( - empCode: '', + 'policies': (context) => AuthWrapper( + builder: (_) => const policies(), ), - 'nonEBClaimsList': (context) => const NonEBClaimsList(empCode: '',), - 'cdTransactionDetails': (context) => cdTransactionDetails( - insurerName: '', - cdMasterAccountNo: '', - insurerId: '', - cd_ac_pk: '', - empClientId: '', + 'CdPoliciesList': (context) => AuthWrapper( + builder: (_) => CdPoliciesList(), + ), + 'ClaimsPolicies': (context) => AuthWrapper( + builder: (_) => const ClaimsPolicies( + empCode: '', + ), + ), + 'nonEBClaimsList': (context) => AuthWrapper( + builder: (_) => const NonEBClaimsList( + empCode: '', + ), + ), + 'cdTransactionDetails': (context) => AuthWrapper( + builder: (_) => cdTransactionDetails( + insurerName: '', + cdMasterAccountNo: '', + insurerId: '', + cd_ac_pk: '', + empClientId: '', + ), ), }; -/// 🔐 Global Auth Wrapper (Protects All Pages) +/// 🔐 Auth Wrapper — blocks protected pages until a valid session exists. +/// Uses a builder so protected widgets are not constructed without a token. class AuthWrapper extends StatefulWidget { - final Widget child; + final WidgetBuilder builder; - const AuthWrapper({super.key, required this.child}); + const AuthWrapper({super.key, required this.builder}); @override State createState() => _AuthWrapperState(); } class _AuthWrapperState extends State { + late final bool _isAuthenticated; + @override void initState() { super.initState(); - _checkLogin(); - } + _isAuthenticated = _hasValidSession(); - void _checkLogin() { - final token = TokenStorageService().getCurrentToken(); - - if (token == null || token.isEmpty) { + if (!_isAuthenticated) { WidgetsBinding.instance.addPostFrameCallback((_) { - Navigator.pushNamedAndRemoveUntil(context, 'hrLogin', (route) => false); + if (!mounted) return; + Navigator.pushNamedAndRemoveUntil( + context, + 'hrLogin', + (route) => false, + ); }); } } @override Widget build(BuildContext context) { - return widget.child; + if (!_isAuthenticated) { + // Do not render protected UI while redirecting to login. + return const Scaffold( + body: Center( + child: CircularProgressIndicator( + color: Color(0xFF00999E), + ), + ), + ); + } + return widget.builder(context); } } diff --git a/lib/presentation/claims.dart b/lib/presentation/claims.dart index 25005cd..45b9043 100755 --- a/lib/presentation/claims.dart +++ b/lib/presentation/claims.dart @@ -15,6 +15,7 @@ import 'package:universal_html/html.dart' as html; import '../service/secure_pop_scope.dart'; import 'claimshistory.dart'; import '../customAppBar/base_layout.dart'; +import '../customAppBar/toastHelper.dart'; import '../service/api_service.dart'; import '../service/token_storage_service.dart'; import 'RaiseClaimForm.dart'; @@ -74,26 +75,14 @@ class _ClaimsPolicieState extends State { int? appliedClaimStatus; Color getStatusColor(String status) { - switch (status.toLowerCase()) { - case 'claim received': - return const Color(0xFF4A90E2); // blue - case 'under process': - return const Color(0xFFF5A623); // orange - case 'information required': - return const Color(0xFFFF8C42); // amber - case 'approved': - return const Color(0xFF2ECC71); // green - case 'settled': - return const Color(0xFF1ABC9C); // teal-green - case 'rejected': - return const Color(0xFFE74C3C); // red - case 'denial review awaite': - return const Color(0xFF9B59B6); // purple - case 'closed': - return const Color(0xFFB0BEC5); // purple - default: - return const Color(0xFFB0BEC5); // grey fallback + final normalized = status.toLowerCase().trim(); + if (normalized == 'rejected' || normalized.contains('rejected')) { + return const Color(0xFFE26728); // Export button orange } + if (normalized == 'settled' || normalized.contains('settled')) { + return const Color(0xFF2ECC71); // Green + } + return const Color(0xFF00A6A6); // List header color } Map claim_Detials() { @@ -148,7 +137,22 @@ class _ClaimsPolicieState extends State { // } // getApiData(); - _loadIds(); + _checkToken(); + } + + Future _checkToken() async { + final token = await tokenService.getCurrentToken(); + if (token == null || token.trim().isEmpty) { + if (!mounted) return; + ToastHelper.showErrorToast(context, 'Session Out'); + Navigator.pushNamedAndRemoveUntil( + context, + 'hrLogin', + (route) => false, + ); + return; + } + await _loadIds(); } Future _loadIds() async { diff --git a/lib/presentation/claims_overview/claims_overview_dashboard.dart b/lib/presentation/claims_overview/claims_overview_dashboard.dart index ba340dc..a17524c 100644 --- a/lib/presentation/claims_overview/claims_overview_dashboard.dart +++ b/lib/presentation/claims_overview/claims_overview_dashboard.dart @@ -41,6 +41,7 @@ class _ClaimsOverviewDashboardState extends State bool _isLoading = true; bool _isRefreshing = false; bool _isExportingPdf = false; + bool _sessionChecked = false; int _replayToken = 0; int _currentTab = 0; @@ -89,7 +90,25 @@ class _ClaimsOverviewDashboardState extends State ); _headerFadeController.forward(); - _loadDashboard(); + _checkToken(); + } + + Future _checkToken() async { + final token = await _tokenService.getCurrentToken(); + if (token == null || token.trim().isEmpty) { + if (!mounted) return; + ToastHelper.showErrorToast(context, 'Session Out'); + Navigator.pushNamedAndRemoveUntil( + context, + 'hrLogin', + (route) => false, + ); + return; + } + + if (!mounted) return; + setState(() => _sessionChecked = true); + await _loadDashboard(); } void _onTabChanged() { @@ -499,6 +518,16 @@ class _ClaimsOverviewDashboardState extends State @override Widget build(BuildContext context) { + if (!_sessionChecked) { + return const Scaffold( + body: Center( + child: CircularProgressIndicator( + color: Color(0xFF00999E), + ), + ), + ); + } + return BaseLayout( child: SecurePopScope( child: Container( diff --git a/lib/presentation/claimshistory.dart b/lib/presentation/claimshistory.dart index 9b3c950..cabd251 100755 --- a/lib/presentation/claimshistory.dart +++ b/lib/presentation/claimshistory.dart @@ -62,6 +62,8 @@ class _ClaimHistoryPopupState extends State bool isSubmitting = false; bool isActionFreeze = false; + bool useStatusKeyedTicketData = false; + bool useListTicketHistory = false; // IR Docs state bool showIRDocs = false; @@ -275,22 +277,28 @@ class _ClaimHistoryPopupState extends State if (response['status'] == 'success') { setState(() { isLoading = false; - final claimsDocs = response['data']['claim_files']; - claimFiles = List>.from(claimsDocs); + final data = response['data'] is Map + ? Map.from(response['data']) + : {}; - getClaimsHistoryList = [ - Map.from(response['data']['ticket_data']) - ]; - stepMap = getClaimsHistoryList[0]; - stepKeys = stepMap.keys.toList(); + final claimsDocs = data['claim_files']; + claimFiles = claimsDocs is List + ? List>.from(claimsDocs) + : >[]; - final isFreeze = - response['data']['required_docs']['is_action_freeze'] ?? false; - setState(() { - isActionFreeze = isFreeze; - }); - final requiredDocs = response['data']['required_docs']['docs']; - requiredDocsList = List>.from(requiredDocs); + _parseTicketData(data); + + final requiredDocsRoot = data['required_docs']; + if (requiredDocsRoot is Map) { + isActionFreeze = requiredDocsRoot['is_action_freeze'] ?? false; + final requiredDocs = requiredDocsRoot['docs']; + requiredDocsList = requiredDocs is List + ? List>.from(requiredDocs) + : >[]; + } else { + isActionFreeze = false; + requiredDocsList = >[]; + } // ⭐ Make a backup copy to restore later requiredDocsListBackup = requiredDocsList @@ -317,6 +325,60 @@ class _ClaimHistoryPopupState extends State } } + void _parseTicketData(Map data) { + final ticketDataRaw = data['ticket_data']; + final ticketHistoryRaw = data['ticket_history']; + + useStatusKeyedTicketData = false; + useListTicketHistory = false; + stepMap = {}; + stepKeys = []; + getClaimsHistoryList = []; + + if (ticketDataRaw is Map && + !ticketDataRaw.containsKey('status') && + ticketDataRaw.values.any((value) => value is Map)) { + useStatusKeyedTicketData = true; + stepMap = Map.from(ticketDataRaw); + stepKeys = stepMap.keys.map((key) => key.toString()).toList(); + getClaimsHistoryList = [stepMap]; + return; + } + + if (ticketDataRaw is List && ticketDataRaw.isNotEmpty) { + useListTicketHistory = true; + getClaimsHistoryList = ticketDataRaw + .whereType() + .map((item) => Map.from(item)) + .toList(); + stepKeys = List.generate(getClaimsHistoryList.length, (index) => '$index'); + return; + } + + if (ticketHistoryRaw is List && ticketHistoryRaw.isNotEmpty) { + useListTicketHistory = true; + getClaimsHistoryList = ticketHistoryRaw + .whereType() + .map((item) => Map.from(item)) + .toList(); + stepKeys = List.generate(getClaimsHistoryList.length, (index) => '$index'); + return; + } + + if (ticketDataRaw is Map && ticketDataRaw.isNotEmpty) { + useListTicketHistory = true; + getClaimsHistoryList = [Map.from(ticketDataRaw)]; + stepKeys = const ['0']; + } + } + + Map _asStringMap(dynamic value) { + if (value is Map) { + return Map.from(value); + } + return {}; + } + Future _launchURL(String url) async { final Uri uri = Uri.parse(url); try { @@ -660,13 +722,23 @@ class _ClaimHistoryPopupState extends State ? Column( crossAxisAlignment: CrossAxisAlignment.start, children: List.generate(stepKeys.length, (index) { - String stepTitleKey = stepKeys[index]; - Map stepData = stepMap[stepTitleKey]; - Widget content = _getStepContentFromApi(stepData); + if (useListTicketHistory) { + final stepData = getClaimsHistoryList[index]; + return _buildStep( + stepNumber: index + 1, + title: _getStepTitleFromHistoryItem(stepData), + content: _getStepContentFromHistoryItem(stepData), + isLast: index == stepKeys.length - 1, + ); + } + + final stepTitleKey = stepKeys[index]; + final stepData = _asStringMap(stepMap[stepTitleKey]); return _buildStep( stepNumber: index + 1, - title: _getStepTitleFromApi(stepTitleKey, stepData), - content: content, + title: _getStepTitleFromApi( + stepTitleKey, stepData), + content: _getStepContentFromApi(stepData), isLast: index == stepKeys.length - 1, ); }), @@ -1459,6 +1531,53 @@ class _ClaimHistoryPopupState extends State ); } + Widget _getStepTitleFromHistoryItem(Map data) { + final title = + (data['display_name'] ?? data['field_name'] ?? 'Update').toString(); + final modifiedBy = (data['modified_by'] ?? '').toString(); + final modifiedAt = + (data['created_at'] ?? data['modified_at'] ?? '').toString(); + final symbol = modifiedBy.isNotEmpty ? ' - ' : ''; + final isDesktop = Responsive.isDesktop(context); + + return RichText( + text: TextSpan( + children: [ + TextSpan( + text: title + (isDesktop ? ' ' : '\n'), + style: GoogleFonts.poppins( + fontSize: Responsive.isDesktop(context) ? 15 : 11, + fontWeight: FontWeight.w500, + color: Color(0xFF212120)), + ), + TextSpan( + text: modifiedAt.isEmpty ? '' : ' ($modifiedBy$symbol$modifiedAt)', + style: GoogleFonts.poppins( + fontSize: Responsive.isDesktop(context) ? 14 : 11, + fontWeight: FontWeight.w400, + color: Color(0xFF565656)), + ), + ], + ), + ); + } + + Widget _getStepContentFromHistoryItem(Map data) { + final oldValue = + (data['old_status_value'] ?? data['old_value'] ?? '-').toString(); + final newValue = + (data['new_status_value'] ?? data['new_value'] ?? '-').toString(); + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildHistoryListData('Previous Value', oldValue), + const SizedBox(height: 1), + _buildHistoryListData('Updated Value', newValue), + ], + ); + } + Widget _getStepTitleFromApi(String status, Map data) { final modifiedBy = data['modified_by'] ?? ''; final modifiedAt = data['modified_at'] ?? ''; @@ -1489,14 +1608,36 @@ class _ClaimHistoryPopupState extends State } Widget _getStepContentFromApi(Map data) { - List rows = []; - data.forEach((key, value) { - if (key == 'modified_by' || key == 'modified_at') return; - String displayName = value['display_name'] ?? key; - String displayValue = value['display_value'] ?? 'N/A'; - rows.add(_buildHistoryListData(displayName, displayValue)); - rows.add(SizedBox(height: 1)); - }); + final rows = []; + + for (final entry in data.entries) { + final key = entry.key.toString(); + final value = entry.value; + + if (key == 'modified_by' || key == 'modified_at') continue; + + if (value is Map) { + final nested = Map.from(value); + final displayName = + (nested['display_name'] ?? key).toString(); + final displayValue = + (nested['display_value'] ?? 'N/A').toString(); + rows + ..add(_buildHistoryListData(displayName, displayValue)) + ..add(const SizedBox(height: 1)); + continue; + } + + if (key == 'reason') { + final reason = value?.toString().trim() ?? ''; + if (reason.isNotEmpty) { + rows + ..add(_buildReasonText(reason)) + ..add(const SizedBox(height: 1)); + } + } + } + return Column(crossAxisAlignment: CrossAxisAlignment.start, children: rows); } @@ -1550,6 +1691,24 @@ class _ClaimHistoryPopupState extends State ); } + Widget _buildReasonText(String value) { + final displayValue = + (value.trim().isEmpty) ? 'N/A' : value.trim(); + + return Align( + alignment: Alignment.centerLeft, + child: Text( + displayValue, + textAlign: TextAlign.left, + style: GoogleFonts.poppins( + color: const Color(0xFF000000), + fontWeight: FontWeight.w400, + fontSize: Responsive.isDesktop(context) ? 14 : 12, + ), + ), + ); + } + Widget _buildHistoryListData(String title, String value) { final displayValue = (value == null || value.trim().isEmpty) ? 'N/A' : value; diff --git a/lib/presentation/nonEBClaimsHistory.dart b/lib/presentation/nonEBClaimsHistory.dart index 46479f1..aeab035 100644 --- a/lib/presentation/nonEBClaimsHistory.dart +++ b/lib/presentation/nonEBClaimsHistory.dart @@ -58,6 +58,7 @@ class _NonEBClaimsHistoryState extends State bool isSubmitting = false; bool isActionFreeze = false; + bool useStatusKeyedTicketData = false; // IR Docs state bool showIRDocs = false; @@ -286,20 +287,34 @@ class _NonEBClaimsHistoryState extends State final ticketDataRaw = root['ticket_data'] ?? nestedData['ticket_data']; if (ticketDataRaw is List) { + useStatusKeyedTicketData = false; getClaimsHistoryList = ticketDataRaw .map>((e) => Map.from(e)) .toList(); + stepMap = getClaimsHistoryList.isNotEmpty + ? getClaimsHistoryList.first + : {}; + stepKeys = List.generate(getClaimsHistoryList.length, (i) => '$i'); } else if (ticketDataRaw is Map) { - getClaimsHistoryList = [Map.from(ticketDataRaw)]; + final map = Map.from(ticketDataRaw); + if (map.containsKey('status')) { + useStatusKeyedTicketData = false; + getClaimsHistoryList = [map]; + stepMap = map; + stepKeys = const ['0']; + } else { + useStatusKeyedTicketData = true; + stepMap = map; + stepKeys = map.keys.map((k) => k.toString()).toList(); + getClaimsHistoryList = [map]; + } } else { + useStatusKeyedTicketData = false; getClaimsHistoryList = []; + stepMap = {}; + stepKeys = []; } - stepMap = getClaimsHistoryList.isNotEmpty - ? getClaimsHistoryList.first - : {}; - stepKeys = List.generate(getClaimsHistoryList.length, (i) => '$i'); - final claimsDocs = root['claims_files'] ?? root['claim_files'] ?? nestedData['claims_files'] ?? @@ -688,13 +703,25 @@ class _NonEBClaimsHistoryState extends State child: getClaimsHistoryList.isNotEmpty ? Column( crossAxisAlignment: CrossAxisAlignment.start, - children: List.generate(getClaimsHistoryList.length, (index) { + children: useStatusKeyedTicketData + ? List.generate(stepKeys.length, (index) { + final stepTitleKey = stepKeys[index]; + final stepData = Map.from( + stepMap[stepTitleKey] ?? {}); + return _buildStep( + stepNumber: index + 1, + title: _getStepTitleFromStatusKey( + stepTitleKey, stepData), + content: _getStepContentFromApi(stepData), + isLast: index == stepKeys.length - 1, + ); + }) + : List.generate(getClaimsHistoryList.length, (index) { final stepData = getClaimsHistoryList[index]; - Widget content = _getStepContentFromApi(stepData); return _buildStep( stepNumber: index + 1, title: _getStepTitleFromApi(stepData), - content: content, + content: _getStepContentFromApi(stepData), isLast: index == getClaimsHistoryList.length - 1, ); }), @@ -1487,6 +1514,35 @@ class _NonEBClaimsHistoryState extends State ); } + Widget _getStepTitleFromStatusKey(String status, Map data) { + final modifiedBy = (data['modified_by'] ?? '').toString(); + final modifiedAt = + (data['modified_at'] ?? data['changed_at'] ?? '').toString(); + final symbol = modifiedBy.isNotEmpty ? ' - ' : ''; + final isDesktop = Responsive.isDesktop(context); + + return RichText( + text: TextSpan( + children: [ + TextSpan( + text: status + (isDesktop ? ' ' : '\n'), + style: GoogleFonts.poppins( + fontSize: Responsive.isDesktop(context) ? 15 : 11, + fontWeight: FontWeight.w500, + color: Color(0xFF212120)), + ), + TextSpan( + text: modifiedAt.isEmpty ? '' : ' ($modifiedBy$symbol$modifiedAt)', + style: GoogleFonts.poppins( + fontSize: Responsive.isDesktop(context) ? 14 : 11, + fontWeight: FontWeight.w400, + color: Color(0xFF565656)), + ), + ], + ), + ); + } + Widget _getStepTitleFromApi(Map data) { final status = (data['status'] ?? '').toString(); final changedAt = @@ -1526,11 +1582,22 @@ class _NonEBClaimsHistoryState extends State key == 'changed_at') { return; } - if (value is! Map) return; - String displayName = value['display_name'] ?? key; - String displayValue = value['display_value'] ?? 'N/A'; - rows.add(_buildHistoryListData(displayName, displayValue)); - rows.add(SizedBox(height: 1)); + + if (value is Map) { + final displayName = value['display_name']?.toString() ?? key; + final displayValue = value['display_value']?.toString() ?? 'N/A'; + rows.add(_buildHistoryListData(displayName, displayValue)); + rows.add(const SizedBox(height: 1)); + return; + } + + if (key == 'reason' && value != null) { + final reason = value.toString().trim(); + if (reason.isNotEmpty) { + rows.add(_buildReasonText(reason)); + rows.add(const SizedBox(height: 1)); + } + } }); return Column(crossAxisAlignment: CrossAxisAlignment.start, children: rows); } @@ -1585,6 +1652,24 @@ class _NonEBClaimsHistoryState extends State ); } + Widget _buildReasonText(String value) { + final displayValue = + (value.trim().isEmpty) ? 'N/A' : value.trim(); + + return Align( + alignment: Alignment.centerLeft, + child: Text( + displayValue, + textAlign: TextAlign.left, + style: GoogleFonts.poppins( + color: const Color(0xFF000000), + fontWeight: FontWeight.w400, + fontSize: Responsive.isDesktop(context) ? 14 : 12, + ), + ), + ); + } + Widget _buildHistoryListData(String title, String value) { final displayValue = (value == null || value.trim().isEmpty) ? 'N/A' : value; diff --git a/lib/presentation/nonEBClaimsList.dart b/lib/presentation/nonEBClaimsList.dart index 2db260f..3ff75a4 100644 --- a/lib/presentation/nonEBClaimsList.dart +++ b/lib/presentation/nonEBClaimsList.dart @@ -16,6 +16,7 @@ import 'package:universal_html/html.dart' as html; import '../service/secure_pop_scope.dart'; import 'claimshistory.dart'; import '../customAppBar/base_layout.dart'; +import '../customAppBar/toastHelper.dart'; import '../service/api_service.dart'; import '../service/token_storage_service.dart'; import 'RaiseClaimForm.dart'; @@ -81,34 +82,18 @@ class _NonEBClaimsListState extends State { int? appliedClaimStatus; Color getStatusColor(String status) { - switch (status.toLowerCase().trim()) { - case 'claim intimation received': - return const Color(0xFF4A90E2); - case 'claim intimated': - return const Color(0xFF5B9BD5); - case 'insurer pending': - return const Color(0xFFF5A623); - case 'surveyor pending': - return const Color(0xFFFF8C42); - case 'insured pending': - return const Color(0xFFE67E22); - case 'insurer / surveyor pending': - return const Color(0xFFD35400); - case 'workshop pending': - return const Color(0xFF9B59B6); - case 'claim approved': - return const Color(0xFF2ECC71); - case 'claim settled': - return const Color(0xFF1ABC9C); - case 'claim closed': - return const Color(0xFF95A5A6); - case 'claim rejected': - return const Color(0xFFE74C3C); - case 'claim withdrawn': - return const Color(0xFF7F8C8D); - default: - return const Color(0xFFB0BEC5); + final normalized = status.toLowerCase().trim(); + if (normalized == 'rejected' || + normalized == 'claim rejected' || + normalized.contains('rejected')) { + return const Color(0xFFE26728); // Export button orange } + if (normalized == 'settled' || + normalized == 'claim settled' || + normalized.contains('settled')) { + return const Color(0xFF2ECC71); // Green + } + return const Color(0xFF00A6A6); // List header color } Map claim_Detials() { @@ -169,7 +154,22 @@ class _NonEBClaimsListState extends State { // } // getApiData(); - _loadIds(); + _checkToken(); + } + + Future _checkToken() async { + final token = await tokenService.getCurrentToken(); + if (token == null || token.trim().isEmpty) { + if (!mounted) return; + ToastHelper.showErrorToast(context, 'Session Out'); + Navigator.pushNamedAndRemoveUntil( + context, + 'hrLogin', + (route) => false, + ); + return; + } + await _loadIds(); } Future _loadIds() async { diff --git a/lib/presentation/postFileUpload.dart b/lib/presentation/postFileUpload.dart index db0a3ef..8c174e7 100755 --- a/lib/presentation/postFileUpload.dart +++ b/lib/presentation/postFileUpload.dart @@ -1,6 +1,5 @@ import 'dart:typed_data'; import 'package:flutter/material.dart'; -import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:google_fonts/google_fonts.dart'; import 'package:jwt_decode/jwt_decode.dart'; import 'package:nhancepolicy/customAppBar/customAppBar.dart'; diff --git a/lib/service/token_storage_service.dart b/lib/service/token_storage_service.dart index d6b6fe3..922a625 100755 --- a/lib/service/token_storage_service.dart +++ b/lib/service/token_storage_service.dart @@ -137,6 +137,15 @@ class TokenStorageService { key: _branchNameKey, value: branch['branch_name']?.toString() ?? '', ); + + // Prefer claims_sub_menu from branch payload when available. + final claimsSubMenu = branch['claims_sub_menu']; + if (claimsSubMenu is List) { + await _secureStorage.write( + key: 'claims_sub_menu', + value: jsonEncode(claimsSubMenu), + ); + } } // 🔓 Decode JWT @@ -225,6 +234,15 @@ class TokenStorageService { // ================= TOKEN ================= await _secureStorage.write(key: 'token', value: token); + + // ================= CLAIMS SUB MENU ================= + if (decodedToken.containsKey('claims_sub_menu')) { + final claimsSubMenu = decodedToken['claims_sub_menu']; + await _secureStorage.write( + key: 'claims_sub_menu', + value: jsonEncode(claimsSubMenu is List ? claimsSubMenu : []), + ); + } } Future readValue(String key) async { @@ -257,6 +275,7 @@ class TokenStorageService { 'enrollmentEmpPrimaryId', 'enrollmentHrId', 'token', + 'claims_sub_menu', ]; for (final key in keysToRemove) { diff --git a/pubspec.yaml b/pubspec.yaml index 1945bed..e6a2903 100755 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -53,7 +53,7 @@ dependencies: flutter_animated_button: ^2.0.3 spreadsheet_decoder: ^2.2.0 url_launcher: ^6.2.6 - font_awesome_flutter: ^10.7.0 + font_awesome_flutter: ^11.0.0 archive: ^3.4.9 dropdown_search: ^6.0.2 flutter_secure_storage: ^10.0.0