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<Map<String, dynamic>> sideMenuItems = [];
List<int> postModules = []; List<int> postModules = [];
List<int> enrollmentModules = []; List<int> enrollmentModules = [];
List<String> claimsSubMenu = [];
@override @override
void initState() { void initState() {
@ -59,8 +60,26 @@ class _NhanceSideBarState extends State<NhanceSideBar> {
? List<int>.from(jsonDecode(postRaw)) ? 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('enrollmentModules $enrollmentModules');
logDebug('postModules $postModules'); logDebug('postModules $postModules');
logDebug('claimsSubMenu $claimsSubMenu');
final List<Map<String, dynamic>> items = []; final List<Map<String, dynamic>> items = [];
@ -82,8 +101,9 @@ class _NhanceSideBarState extends State<NhanceSideBar> {
}); });
} }
// CLAIMS // CLAIMS only when module 4 and at least one sub-menu is allowed
if (postModules.contains(4)) { if (postModules.contains(4) &&
(claimsSubMenu.contains('EB') || claimsSubMenu.contains('Non-EB'))) {
items.add({ items.add({
'route': 'ClaimsPolicies', 'route': 'ClaimsPolicies',
'label': 'Claims', 'label': 'Claims',
@ -173,8 +193,10 @@ class _NhanceSideBarState extends State<NhanceSideBar> {
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
_buildClaimsMenuItem('EB', 'ClaimsPolicies'), if (claimsSubMenu.contains('EB'))
_buildClaimsMenuItem('Non-EB', 'nonEBClaimsList'), _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)); 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 { class MyApp extends StatelessWidget {
final String? initialToken; final String? initialToken;
@ -188,12 +234,33 @@ class MyApp extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final fallbackRoute =
(initialToken == null || initialToken!.isEmpty) ? 'hrLogin' : 'hrHome';
return MaterialApp( return MaterialApp(
title: 'Nhance HR', title: 'Nhance HR',
debugShowCheckedModeBanner: false, debugShowCheckedModeBanner: false,
initialRoute: (initialToken == null || initialToken!.isEmpty) initialRoute: fallbackRoute,
? 'hrLogin' onGenerateInitialRoutes: (String initialRouteName) {
: 'hrHome', 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( theme: ThemeData(
primaryColor: const Color(0xFF00999E), primaryColor: const Color(0xFF00999E),
scaffoldBackgroundColor: Colors.white, scaffoldBackgroundColor: Colors.white,
@ -225,110 +292,159 @@ final Map<String, WidgetBuilder> appRoutes = {
// resendToken: null, // resendToken: null,
// onResendCode: (String, int) {}, // onResendCode: (String, int) {},
// ), // ),
'hrHome': (context) => MyHrHome(), 'hrHome': (context) => AuthWrapper(
'preFileUpload': (context) => const preFileUpload( builder: (_) => MyHrHome(),
ClientId: '',
policyTypeId: '',
ClientPoliyId: '',
clientBranchId: '',
Token: '',
TokenType: '',
cardType: '',
cardPolicyNo: '',
cardInsurer_name: '',
cardPolicy_name: '',
cardPolicy_ExpDate: '',
total_premium: '',
), ),
'postFileUpload': (context) => const postFileUpload( 'preFileUpload': (context) => AuthWrapper(
ClientId: '', builder: (_) => const preFileUpload(
policyTypeId: '', ClientId: '',
ClientPoliyId: '', policyTypeId: '',
clientBranchId: '', ClientPoliyId: '',
Token: '', clientBranchId: '',
TokenType: '', Token: '',
cardType: '', TokenType: '',
cardPolicyNo: '', cardType: '',
cardInsurer_name: '', cardPolicyNo: '',
cardPolicy_name: '', cardInsurer_name: '',
cardPolicy_ExpDate: '', cardPolicy_name: '',
total_premium: '', cardPolicy_ExpDate: '',
total_premium: '',
),
), ),
'excelErrorScreen': (context) => const excelErrorScreen( 'postFileUpload': (context) => AuthWrapper(
ClientId: '', builder: (_) => const postFileUpload(
policy_no: '', ClientId: '',
action: '', policyTypeId: '',
created_at: '', ClientPoliyId: '',
clientBranchId: '', clientBranchId: '',
Token: '', Token: '',
TokenType: '', TokenType: '',
id: ''), cardType: '',
'empDetails': (context) => empDetails(), cardPolicyNo: '',
'addOnsDetails': (context) => addOnsDetails(), cardInsurer_name: '',
'empReviewDetails': (context) => empReviewDetails(), cardPolicy_name: '',
'hrDashboard': (context) => policies(), cardPolicy_ExpDate: '',
'claimsOverviewDashboard': (context) => const ClaimsOverviewDashboard(), total_premium: '',
'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,
), ),
'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(), 'branchSelection': (context) => BranchSelectionPage(),
'policies': (context) => policies(), 'policies': (context) => AuthWrapper(
'CdPoliciesList': (context) => CdPoliciesList(), builder: (_) => const policies(),
'ClaimsPolicies': (context) => ClaimsPolicies(
empCode: '',
), ),
'nonEBClaimsList': (context) => const NonEBClaimsList(empCode: '',), 'CdPoliciesList': (context) => AuthWrapper(
'cdTransactionDetails': (context) => cdTransactionDetails( builder: (_) => CdPoliciesList(),
insurerName: '', ),
cdMasterAccountNo: '', 'ClaimsPolicies': (context) => AuthWrapper(
insurerId: '', builder: (_) => const ClaimsPolicies(
cd_ac_pk: '', empCode: '',
empClientId: '', ),
),
'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 { 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 @override
State<AuthWrapper> createState() => _AuthWrapperState(); State<AuthWrapper> createState() => _AuthWrapperState();
} }
class _AuthWrapperState extends State<AuthWrapper> { class _AuthWrapperState extends State<AuthWrapper> {
late final bool _isAuthenticated;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_checkLogin(); _isAuthenticated = _hasValidSession();
}
void _checkLogin() { if (!_isAuthenticated) {
final token = TokenStorageService().getCurrentToken();
if (token == null || token.isEmpty) {
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
Navigator.pushNamedAndRemoveUntil(context, 'hrLogin', (route) => false); if (!mounted) return;
Navigator.pushNamedAndRemoveUntil(
context,
'hrLogin',
(route) => false,
);
}); });
} }
} }
@override @override
Widget build(BuildContext context) { 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 '../service/secure_pop_scope.dart';
import 'claimshistory.dart'; import 'claimshistory.dart';
import '../customAppBar/base_layout.dart'; import '../customAppBar/base_layout.dart';
import '../customAppBar/toastHelper.dart';
import '../service/api_service.dart'; import '../service/api_service.dart';
import '../service/token_storage_service.dart'; import '../service/token_storage_service.dart';
import 'RaiseClaimForm.dart'; import 'RaiseClaimForm.dart';
@ -74,26 +75,14 @@ class _ClaimsPolicieState extends State<ClaimsPolicies> {
int? appliedClaimStatus; int? appliedClaimStatus;
Color getStatusColor(String status) { Color getStatusColor(String status) {
switch (status.toLowerCase()) { final normalized = status.toLowerCase().trim();
case 'claim received': if (normalized == 'rejected' || normalized.contains('rejected')) {
return const Color(0xFF4A90E2); // blue return const Color(0xFFE26728); // Export button orange
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
} }
if (normalized == 'settled' || normalized.contains('settled')) {
return const Color(0xFF2ECC71); // Green
}
return const Color(0xFF00A6A6); // List header color
} }
Map<String, dynamic> claim_Detials() { Map<String, dynamic> claim_Detials() {
@ -148,7 +137,22 @@ class _ClaimsPolicieState extends State<ClaimsPolicies> {
// } // }
// getApiData(); // 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 { Future<void> _loadIds() async {

View File

@ -41,6 +41,7 @@ class _ClaimsOverviewDashboardState extends State<ClaimsOverviewDashboard>
bool _isLoading = true; bool _isLoading = true;
bool _isRefreshing = false; bool _isRefreshing = false;
bool _isExportingPdf = false; bool _isExportingPdf = false;
bool _sessionChecked = false;
int _replayToken = 0; int _replayToken = 0;
int _currentTab = 0; int _currentTab = 0;
@ -89,7 +90,25 @@ class _ClaimsOverviewDashboardState extends State<ClaimsOverviewDashboard>
); );
_headerFadeController.forward(); _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() { void _onTabChanged() {
@ -499,6 +518,16 @@ class _ClaimsOverviewDashboardState extends State<ClaimsOverviewDashboard>
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (!_sessionChecked) {
return const Scaffold(
body: Center(
child: CircularProgressIndicator(
color: Color(0xFF00999E),
),
),
);
}
return BaseLayout( return BaseLayout(
child: SecurePopScope( child: SecurePopScope(
child: Container( child: Container(

View File

@ -62,6 +62,8 @@ class _ClaimHistoryPopupState extends State<ClaimHistoryPopup>
bool isSubmitting = false; bool isSubmitting = false;
bool isActionFreeze = false; bool isActionFreeze = false;
bool useStatusKeyedTicketData = false;
bool useListTicketHistory = false;
// IR Docs state // IR Docs state
bool showIRDocs = false; bool showIRDocs = false;
@ -275,22 +277,28 @@ class _ClaimHistoryPopupState extends State<ClaimHistoryPopup>
if (response['status'] == 'success') { if (response['status'] == 'success') {
setState(() { setState(() {
isLoading = false; isLoading = false;
final claimsDocs = response['data']['claim_files']; final data = response['data'] is Map
claimFiles = List<Map<String, dynamic>>.from(claimsDocs); ? Map<String, dynamic>.from(response['data'])
: <String, dynamic>{};
getClaimsHistoryList = [ final claimsDocs = data['claim_files'];
Map<String, dynamic>.from(response['data']['ticket_data']) claimFiles = claimsDocs is List
]; ? List<Map<String, dynamic>>.from(claimsDocs)
stepMap = getClaimsHistoryList[0]; : <Map<String, dynamic>>[];
stepKeys = stepMap.keys.toList();
final isFreeze = _parseTicketData(data);
response['data']['required_docs']['is_action_freeze'] ?? false;
setState(() { final requiredDocsRoot = data['required_docs'];
isActionFreeze = isFreeze; if (requiredDocsRoot is Map) {
}); isActionFreeze = requiredDocsRoot['is_action_freeze'] ?? false;
final requiredDocs = response['data']['required_docs']['docs']; final requiredDocs = requiredDocsRoot['docs'];
requiredDocsList = List<Map<String, dynamic>>.from(requiredDocs); 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 // Make a backup copy to restore later
requiredDocsListBackup = requiredDocsList 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 { Future<void> _launchURL(String url) async {
final Uri uri = Uri.parse(url); final Uri uri = Uri.parse(url);
try { try {
@ -660,13 +722,23 @@ class _ClaimHistoryPopupState extends State<ClaimHistoryPopup>
? Column( ? Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: List.generate(stepKeys.length, (index) { children: List.generate(stepKeys.length, (index) {
String stepTitleKey = stepKeys[index]; if (useListTicketHistory) {
Map<String, dynamic> stepData = stepMap[stepTitleKey]; final stepData = getClaimsHistoryList[index];
Widget content = _getStepContentFromApi(stepData); 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( return _buildStep(
stepNumber: index + 1, stepNumber: index + 1,
title: _getStepTitleFromApi(stepTitleKey, stepData), title: _getStepTitleFromApi(
content: content, stepTitleKey, stepData),
content: _getStepContentFromApi(stepData),
isLast: index == stepKeys.length - 1, 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) { Widget _getStepTitleFromApi(String status, Map<String, dynamic> data) {
final modifiedBy = data['modified_by'] ?? ''; final modifiedBy = data['modified_by'] ?? '';
final modifiedAt = data['modified_at'] ?? ''; final modifiedAt = data['modified_at'] ?? '';
@ -1489,14 +1608,36 @@ class _ClaimHistoryPopupState extends State<ClaimHistoryPopup>
} }
Widget _getStepContentFromApi(Map<String, dynamic> data) { Widget _getStepContentFromApi(Map<String, dynamic> data) {
List<Widget> rows = []; final rows = <Widget>[];
data.forEach((key, value) {
if (key == 'modified_by' || key == 'modified_at') return; for (final entry in data.entries) {
String displayName = value['display_name'] ?? key; final key = entry.key.toString();
String displayValue = value['display_value'] ?? 'N/A'; final value = entry.value;
rows.add(_buildHistoryListData(displayName, displayValue));
rows.add(SizedBox(height: 1)); 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); 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) { Widget _buildHistoryListData(String title, String value) {
final displayValue = final displayValue =
(value == null || value.trim().isEmpty) ? 'N/A' : value; (value == null || value.trim().isEmpty) ? 'N/A' : value;

View File

@ -58,6 +58,7 @@ class _NonEBClaimsHistoryState extends State<NonEBClaimsHistory>
bool isSubmitting = false; bool isSubmitting = false;
bool isActionFreeze = false; bool isActionFreeze = false;
bool useStatusKeyedTicketData = false;
// IR Docs state // IR Docs state
bool showIRDocs = false; bool showIRDocs = false;
@ -286,20 +287,34 @@ class _NonEBClaimsHistoryState extends State<NonEBClaimsHistory>
final ticketDataRaw = root['ticket_data'] ?? nestedData['ticket_data']; final ticketDataRaw = root['ticket_data'] ?? nestedData['ticket_data'];
if (ticketDataRaw is List) { if (ticketDataRaw is List) {
useStatusKeyedTicketData = false;
getClaimsHistoryList = ticketDataRaw getClaimsHistoryList = ticketDataRaw
.map<Map<String, dynamic>>((e) => Map<String, dynamic>.from(e)) .map<Map<String, dynamic>>((e) => Map<String, dynamic>.from(e))
.toList(); .toList();
stepMap = getClaimsHistoryList.isNotEmpty
? getClaimsHistoryList.first
: <String, dynamic>{};
stepKeys = List.generate(getClaimsHistoryList.length, (i) => '$i');
} else if (ticketDataRaw is Map) { } 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 { } else {
useStatusKeyedTicketData = false;
getClaimsHistoryList = []; getClaimsHistoryList = [];
stepMap = <String, dynamic>{};
stepKeys = [];
} }
stepMap = getClaimsHistoryList.isNotEmpty
? getClaimsHistoryList.first
: <String, dynamic>{};
stepKeys = List.generate(getClaimsHistoryList.length, (i) => '$i');
final claimsDocs = root['claims_files'] ?? final claimsDocs = root['claims_files'] ??
root['claim_files'] ?? root['claim_files'] ??
nestedData['claims_files'] ?? nestedData['claims_files'] ??
@ -688,13 +703,25 @@ class _NonEBClaimsHistoryState extends State<NonEBClaimsHistory>
child: getClaimsHistoryList.isNotEmpty child: getClaimsHistoryList.isNotEmpty
? Column( ? Column(
crossAxisAlignment: CrossAxisAlignment.start, 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]; final stepData = getClaimsHistoryList[index];
Widget content = _getStepContentFromApi(stepData);
return _buildStep( return _buildStep(
stepNumber: index + 1, stepNumber: index + 1,
title: _getStepTitleFromApi(stepData), title: _getStepTitleFromApi(stepData),
content: content, content: _getStepContentFromApi(stepData),
isLast: index == getClaimsHistoryList.length - 1, 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) { Widget _getStepTitleFromApi(Map<String, dynamic> data) {
final status = (data['status'] ?? '').toString(); final status = (data['status'] ?? '').toString();
final changedAt = final changedAt =
@ -1526,11 +1582,22 @@ class _NonEBClaimsHistoryState extends State<NonEBClaimsHistory>
key == 'changed_at') { key == 'changed_at') {
return; return;
} }
if (value is! Map) return;
String displayName = value['display_name'] ?? key; if (value is Map) {
String displayValue = value['display_value'] ?? 'N/A'; final displayName = value['display_name']?.toString() ?? key;
rows.add(_buildHistoryListData(displayName, displayValue)); final displayValue = value['display_value']?.toString() ?? 'N/A';
rows.add(SizedBox(height: 1)); 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); 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) { Widget _buildHistoryListData(String title, String value) {
final displayValue = final displayValue =
(value == null || value.trim().isEmpty) ? 'N/A' : value; (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 '../service/secure_pop_scope.dart';
import 'claimshistory.dart'; import 'claimshistory.dart';
import '../customAppBar/base_layout.dart'; import '../customAppBar/base_layout.dart';
import '../customAppBar/toastHelper.dart';
import '../service/api_service.dart'; import '../service/api_service.dart';
import '../service/token_storage_service.dart'; import '../service/token_storage_service.dart';
import 'RaiseClaimForm.dart'; import 'RaiseClaimForm.dart';
@ -81,34 +82,18 @@ class _NonEBClaimsListState extends State<NonEBClaimsList> {
int? appliedClaimStatus; int? appliedClaimStatus;
Color getStatusColor(String status) { Color getStatusColor(String status) {
switch (status.toLowerCase().trim()) { final normalized = status.toLowerCase().trim();
case 'claim intimation received': if (normalized == 'rejected' ||
return const Color(0xFF4A90E2); normalized == 'claim rejected' ||
case 'claim intimated': normalized.contains('rejected')) {
return const Color(0xFF5B9BD5); return const Color(0xFFE26728); // Export button orange
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);
} }
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() { Map<String, dynamic> claim_Detials() {
@ -169,7 +154,22 @@ class _NonEBClaimsListState extends State<NonEBClaimsList> {
// } // }
// getApiData(); // 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 { Future<void> _loadIds() async {

View File

@ -1,6 +1,5 @@
import 'dart:typed_data'; import 'dart:typed_data';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:google_fonts/google_fonts.dart'; import 'package:google_fonts/google_fonts.dart';
import 'package:jwt_decode/jwt_decode.dart'; import 'package:jwt_decode/jwt_decode.dart';
import 'package:nhancepolicy/customAppBar/customAppBar.dart'; import 'package:nhancepolicy/customAppBar/customAppBar.dart';

View File

@ -137,6 +137,15 @@ class TokenStorageService {
key: _branchNameKey, key: _branchNameKey,
value: branch['branch_name']?.toString() ?? '', 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 // 🔓 Decode JWT
@ -225,6 +234,15 @@ class TokenStorageService {
// ================= TOKEN ================= // ================= TOKEN =================
await _secureStorage.write(key: 'token', value: 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 { Future<String?> readValue(String key) async {
@ -257,6 +275,7 @@ class TokenStorageService {
'enrollmentEmpPrimaryId', 'enrollmentEmpPrimaryId',
'enrollmentHrId', 'enrollmentHrId',
'token', 'token',
'claims_sub_menu',
]; ];
for (final key in keysToRemove) { for (final key in keysToRemove) {

View File

@ -53,7 +53,7 @@ dependencies:
flutter_animated_button: ^2.0.3 flutter_animated_button: ^2.0.3
spreadsheet_decoder: ^2.2.0 spreadsheet_decoder: ^2.2.0
url_launcher: ^6.2.6 url_launcher: ^6.2.6
font_awesome_flutter: ^10.7.0 font_awesome_flutter: ^11.0.0
archive: ^3.4.9 archive: ^3.4.9
dropdown_search: ^6.0.2 dropdown_search: ^6.0.2
flutter_secure_storage: ^10.0.0 flutter_secure_storage: ^10.0.0