changes done

This commit is contained in:
Surendiran 2026-07-28 10:40:36 +05:30
parent 75e5baf0fd
commit 1e97c41545
10 changed files with 610 additions and 177 deletions

View File

@ -32,6 +32,7 @@ class _NhanceSideBarState extends State<NhanceSideBar> {
List<Map<String, dynamic>> sideMenuItems = [];
List<int> postModules = [];
List<int> enrollmentModules = [];
List<String> claimsSubMenu = [];
@override
void initState() {
@ -59,8 +60,26 @@ class _NhanceSideBarState extends State<NhanceSideBar> {
? List<int>.from(jsonDecode(postRaw))
: [];
final claimsSubMenuRaw = await tokenService.readValue('claims_sub_menu');
if (claimsSubMenuRaw != null && claimsSubMenuRaw.isNotEmpty) {
claimsSubMenu = List<dynamic>.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<Map<String, dynamic>> items = [];
@ -82,8 +101,9 @@ class _NhanceSideBarState extends State<NhanceSideBar> {
});
}
// 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<NhanceSideBar> {
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'),
],
),
),

View File

@ -181,6 +181,52 @@ Future<void> startApp() async {
runApp(MyApp(initialToken: token));
}
/// Routes that require an authenticated HR session.
/// Public auth-flow routes (hrLogin, mailVerify, branchSelection) stay excluded.
const Set<String> _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<String, WidgetBuilder> 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<AuthWrapper> createState() => _AuthWrapperState();
}
class _AuthWrapperState extends State<AuthWrapper> {
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);
}
}

View File

@ -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<ClaimsPolicies> {
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<String, dynamic> claim_Detials() {
@ -148,7 +137,22 @@ class _ClaimsPolicieState extends State<ClaimsPolicies> {
// }
// getApiData();
_loadIds();
_checkToken();
}
Future<void> _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<void> _loadIds() async {

View File

@ -41,6 +41,7 @@ class _ClaimsOverviewDashboardState extends State<ClaimsOverviewDashboard>
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<ClaimsOverviewDashboard>
);
_headerFadeController.forward();
_loadDashboard();
_checkToken();
}
Future<void> _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<ClaimsOverviewDashboard>
@override
Widget build(BuildContext context) {
if (!_sessionChecked) {
return const Scaffold(
body: Center(
child: CircularProgressIndicator(
color: Color(0xFF00999E),
),
),
);
}
return BaseLayout(
child: SecurePopScope(
child: Container(

View File

@ -62,6 +62,8 @@ class _ClaimHistoryPopupState extends State<ClaimHistoryPopup>
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<ClaimHistoryPopup>
if (response['status'] == 'success') {
setState(() {
isLoading = false;
final claimsDocs = response['data']['claim_files'];
claimFiles = List<Map<String, dynamic>>.from(claimsDocs);
final data = response['data'] is Map
? Map<String, dynamic>.from(response['data'])
: <String, dynamic>{};
getClaimsHistoryList = [
Map<String, dynamic>.from(response['data']['ticket_data'])
];
stepMap = getClaimsHistoryList[0];
stepKeys = stepMap.keys.toList();
final claimsDocs = data['claim_files'];
claimFiles = claimsDocs is List
? List<Map<String, dynamic>>.from(claimsDocs)
: <Map<String, dynamic>>[];
final isFreeze =
response['data']['required_docs']['is_action_freeze'] ?? false;
setState(() {
isActionFreeze = isFreeze;
});
final requiredDocs = response['data']['required_docs']['docs'];
requiredDocsList = List<Map<String, dynamic>>.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<Map<String, dynamic>>.from(requiredDocs)
: <Map<String, dynamic>>[];
} else {
isActionFreeze = false;
requiredDocsList = <Map<String, dynamic>>[];
}
// Make a backup copy to restore later
requiredDocsListBackup = requiredDocsList
@ -317,6 +325,60 @@ class _ClaimHistoryPopupState extends State<ClaimHistoryPopup>
}
}
void _parseTicketData(Map<String, dynamic> data) {
final ticketDataRaw = data['ticket_data'];
final ticketHistoryRaw = data['ticket_history'];
useStatusKeyedTicketData = false;
useListTicketHistory = false;
stepMap = <String, dynamic>{};
stepKeys = [];
getClaimsHistoryList = [];
if (ticketDataRaw is Map &&
!ticketDataRaw.containsKey('status') &&
ticketDataRaw.values.any((value) => value is Map)) {
useStatusKeyedTicketData = true;
stepMap = Map<String, dynamic>.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>()
.map((item) => Map<String, dynamic>.from(item))
.toList();
stepKeys = List.generate(getClaimsHistoryList.length, (index) => '$index');
return;
}
if (ticketHistoryRaw is List && ticketHistoryRaw.isNotEmpty) {
useListTicketHistory = true;
getClaimsHistoryList = ticketHistoryRaw
.whereType<Map>()
.map((item) => Map<String, dynamic>.from(item))
.toList();
stepKeys = List.generate(getClaimsHistoryList.length, (index) => '$index');
return;
}
if (ticketDataRaw is Map && ticketDataRaw.isNotEmpty) {
useListTicketHistory = true;
getClaimsHistoryList = [Map<String, dynamic>.from(ticketDataRaw)];
stepKeys = const ['0'];
}
}
Map<String, dynamic> _asStringMap(dynamic value) {
if (value is Map) {
return Map<String, dynamic>.from(value);
}
return <String, dynamic>{};
}
Future<void> _launchURL(String url) async {
final Uri uri = Uri.parse(url);
try {
@ -660,13 +722,23 @@ class _ClaimHistoryPopupState extends State<ClaimHistoryPopup>
? Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: List.generate(stepKeys.length, (index) {
String stepTitleKey = stepKeys[index];
Map<String, dynamic> 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<ClaimHistoryPopup>
);
}
Widget _getStepTitleFromHistoryItem(Map<String, dynamic> 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<String, dynamic> 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<String, dynamic> data) {
final modifiedBy = data['modified_by'] ?? '';
final modifiedAt = data['modified_at'] ?? '';
@ -1489,14 +1608,36 @@ class _ClaimHistoryPopupState extends State<ClaimHistoryPopup>
}
Widget _getStepContentFromApi(Map<String, dynamic> data) {
List<Widget> 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 = <Widget>[];
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<String, dynamic>.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<ClaimHistoryPopup>
);
}
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;

View File

@ -58,6 +58,7 @@ class _NonEBClaimsHistoryState extends State<NonEBClaimsHistory>
bool isSubmitting = false;
bool isActionFreeze = false;
bool useStatusKeyedTicketData = false;
// IR Docs state
bool showIRDocs = false;
@ -286,20 +287,34 @@ class _NonEBClaimsHistoryState extends State<NonEBClaimsHistory>
final ticketDataRaw = root['ticket_data'] ?? nestedData['ticket_data'];
if (ticketDataRaw is List) {
useStatusKeyedTicketData = false;
getClaimsHistoryList = ticketDataRaw
.map<Map<String, dynamic>>((e) => Map<String, dynamic>.from(e))
.toList();
stepMap = getClaimsHistoryList.isNotEmpty
? getClaimsHistoryList.first
: <String, dynamic>{};
stepKeys = List.generate(getClaimsHistoryList.length, (i) => '$i');
} else if (ticketDataRaw is Map) {
getClaimsHistoryList = [Map<String, dynamic>.from(ticketDataRaw)];
final map = Map<String, dynamic>.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 = <String, dynamic>{};
stepKeys = [];
}
stepMap = getClaimsHistoryList.isNotEmpty
? getClaimsHistoryList.first
: <String, dynamic>{};
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<NonEBClaimsHistory>
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<String, dynamic>.from(
stepMap[stepTitleKey] ?? <String, dynamic>{});
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<NonEBClaimsHistory>
);
}
Widget _getStepTitleFromStatusKey(String status, Map<String, dynamic> 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<String, dynamic> data) {
final status = (data['status'] ?? '').toString();
final changedAt =
@ -1526,11 +1582,22 @@ class _NonEBClaimsHistoryState extends State<NonEBClaimsHistory>
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<NonEBClaimsHistory>
);
}
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;

View File

@ -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<NonEBClaimsList> {
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<String, dynamic> claim_Detials() {
@ -169,7 +154,22 @@ class _NonEBClaimsListState extends State<NonEBClaimsList> {
// }
// getApiData();
_loadIds();
_checkToken();
}
Future<void> _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<void> _loadIds() async {

View File

@ -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';

View File

@ -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<String?> readValue(String key) async {
@ -257,6 +275,7 @@ class TokenStorageService {
'enrollmentEmpPrimaryId',
'enrollmentHrId',
'token',
'claims_sub_menu',
];
for (final key in keysToRemove) {

View File

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