dashboard fix

This commit is contained in:
Surendiran 2026-06-09 15:08:54 +05:30
parent 64d0b0b7d9
commit 440da00ee5
3 changed files with 611 additions and 117 deletions

View File

@ -287,6 +287,17 @@ class _NhanceSideBarState extends State<NhanceSideBar> {
// logout(context);
// },
// ),
if (postModules.isNotEmpty && postModules.contains(5))
_SideItem(
icon: const Icon(
Icons.space_dashboard_outlined,
color: Colors.white,
size: 30,
),
label: "Dashboard",
isActive: activeRoute == 'claimsOverviewDashboard',
onTap: () => _navigate('claimsOverviewDashboard'),
),
...sideMenuItems.map((item) {
final isClaims = item['route'] == 'ClaimsPolicies';
@ -323,34 +334,24 @@ class _NhanceSideBarState extends State<NhanceSideBar> {
);
}),
if (postModules.isNotEmpty)
_SideItem(
icon: const Icon(
Icons.space_dashboard_outlined,
color: Colors.white,
size: 30,
),
label: "Dashboard",
isActive: activeRoute == 'claimsOverviewDashboard',
onTap: () => _navigate('claimsOverviewDashboard'),
),
if (postModules.isNotEmpty && postModules.contains(5))
_SideItem(
// icon: Icons.dashboard,
icon: SvgPicture.string(
SvgService.getSvg('dashboard'),
width: 35,
height: 35,
colorFilter: const ColorFilter.mode(
Colors.white,
BlendMode.srcIn,
),
),
label: "Insights",
isActive: activeRoute == 'hrDashboard',
onTap: () => _navigate('hrDashboard'),
),
// if (postModules.isNotEmpty && postModules.contains(5))
// _SideItem(
// // icon: Icons.dashboard,
// icon: SvgPicture.string(
// SvgService.getSvg('dashboard'),
// width: 35,
// height: 35,
// colorFilter: const ColorFilter.mode(
// Colors.white,
// BlendMode.srcIn,
// ),
// ),
// label: "Insights",
// isActive: activeRoute == 'hrDashboard',
// onTap: () => _navigate('hrDashboard'),
// ),
const Spacer(),

View File

@ -101,6 +101,7 @@ class _HrPolicyDetailsState extends State<hrPolicyDetails>
List<Map<String, dynamic>> getCDPolicies = [];
bool isLoading = false;
bool _isLoading = false;
bool _isSendingReminder = false;
// dynamic clintID;
late TabController _tabController;
// List<dynamic> dataPolicy = [];
@ -124,6 +125,9 @@ class _HrPolicyDetailsState extends State<hrPolicyDetails>
int _currentPage = 1;
int _rowsPerPage = 5;
/// Pre-enrollment status chip filter (null = show all).
String? _preStatusFilter;
/// holds selected employee ids as String or int (be consistent)
final Set<String> selectedEmployeeIds = {};
@ -314,7 +318,7 @@ class _HrPolicyDetailsState extends State<hrPolicyDetails>
setState(() {
getCDPolicies = List<Map<String, dynamic>>.from(response['data']);
originalData = getCDPolicies;
filteredData = List.from(originalData);
_refreshFilteredData();
logDebug('filteredData');
logDebug(filteredData);
});
@ -387,7 +391,7 @@ class _HrPolicyDetailsState extends State<hrPolicyDetails>
setState(() {
getCDPolicies = List<Map<String, dynamic>>.from(data);
originalData = getCDPolicies;
filteredData = List.from(originalData);
_refreshFilteredData();
isLoading = false;
});
} else {
@ -455,65 +459,100 @@ class _HrPolicyDetailsState extends State<hrPolicyDetails>
}
}
void search(String query) {
final lowerQuery = query.toLowerCase().trim();
bool _isEnrolledStatus(String status) =>
status == 'under process' || status == 'enrolled';
if (lowerQuery.isEmpty) {
setState(() {
_currentPage = 1;
filteredData = List.from(originalData);
});
} else {
setState(() {
_currentPage = 1;
filteredData = originalData.where((row) {
final status = row['status']?.toString().toLowerCase().trim() ?? '';
bool _isLoggedInRow(Map<String, dynamic> row) =>
row['emp_is_active']?.toString() == '1';
bool statusMatch;
bool _matchesPreStatusFilter(Map<String, dynamic> row, String filter) {
final status = row['status']?.toString().toLowerCase().trim() ?? '';
final relationship = row['relationship']?.toString().toLowerCase().trim() ?? '';
if (lowerQuery == 'active' || lowerQuery == 'inactive') {
// Exact match for these two
statusMatch = status == lowerQuery;
} else {
// Partial match for others
statusMatch = status.contains(lowerQuery);
}
return row['name']?.toString().toLowerCase().contains(lowerQuery) ==
true ||
row['emp_code']
?.toString()
.toLowerCase()
.contains(lowerQuery) ==
true ||
row['uhid']?.toString().toLowerCase().contains(lowerQuery) ==
true ||
row['relationship']
?.toString()
.toLowerCase()
.contains(lowerQuery) ==
true ||
row['formatted_dob']
?.toString()
.replaceAll("/", "-")
.toLowerCase()
.contains(lowerQuery) ==
true ||
row['gender']?.toString().toLowerCase().contains(lowerQuery) ==
true ||
row['mobile']?.toString().toLowerCase().contains(lowerQuery) ==
true ||
row['email_corporate']
?.toString()
.toLowerCase()
.contains(lowerQuery) ==
true ||
statusMatch;
}).toList();
});
switch (filter) {
case 'emp_count':
return relationship == 'self';
case 'enrolled':
return _isEnrolledStatus(status);
case 'not_enrolled':
return !_isEnrolledStatus(status);
case 'logged_in':
return _isLoggedInRow(row);
case 'not_logged_in':
return !_isLoggedInRow(row);
case 'draft':
return status == 'draft';
default:
return true;
}
}
// emp_is_active
bool _matchesSearch(Map<String, dynamic> row, String lowerQuery) {
final status = row['status']?.toString().toLowerCase().trim() ?? '';
bool statusMatch;
if (lowerQuery == 'active' || lowerQuery == 'inactive') {
statusMatch = status == lowerQuery;
} else {
statusMatch = status.contains(lowerQuery);
}
return row['name']?.toString().toLowerCase().contains(lowerQuery) == true ||
row['emp_code']?.toString().toLowerCase().contains(lowerQuery) ==
true ||
row['uhid']?.toString().toLowerCase().contains(lowerQuery) == true ||
row['relationship']?.toString().toLowerCase().contains(lowerQuery) ==
true ||
row['formatted_dob']
?.toString()
.replaceAll("/", "-")
.toLowerCase()
.contains(lowerQuery) ==
true ||
row['gender']?.toString().toLowerCase().contains(lowerQuery) == true ||
row['mobile']?.toString().toLowerCase().contains(lowerQuery) == true ||
row['email_corporate']?.toString().toLowerCase().contains(lowerQuery) ==
true ||
statusMatch;
}
void _refreshFilteredData() {
Iterable<Map<String, dynamic>> data = originalData;
final lowerQuery = searchController.text.toLowerCase().trim();
if (lowerQuery.isNotEmpty) {
data = data.where((row) => _matchesSearch(row, lowerQuery));
}
if (localTokenType == 'pre' && _preStatusFilter != null) {
data = data.where((row) => _matchesPreStatusFilter(row, _preStatusFilter!));
}
filteredData = data.toList();
}
void search(String query) {
setState(() {
_currentPage = 1;
_refreshFilteredData();
});
}
void _onPreStatusFilterTap(String filter) {
setState(() {
_preStatusFilter = filter;
_currentPage = 1;
_refreshFilteredData();
});
}
void _resetPreStatusFilter() {
setState(() {
_preStatusFilter = null;
_currentPage = 1;
_refreshFilteredData();
});
}
void exportToCsv(List<Map<String, dynamic>> data) {
List<List<String>> rows = [];
@ -605,6 +644,260 @@ class _HrPolicyDetailsState extends State<hrPolicyDetails>
return value[0].toUpperCase() + value.substring(1).toLowerCase();
}
String _defaultReminderSubject() {
final policyName = localCardPolicyName ?? widget.cardPolicy_name;
return 'Reminder: Complete Your Enrollment - $policyName';
}
String _defaultReminderBody() {
final policyName = localCardPolicyName ?? widget.cardPolicy_name;
final policyNo = localCardPolicyNo ?? widget.cardPolicyNo;
return '''Dear Employee,
This is a gentle reminder to complete your insurance enrollment for $policyName ($policyNo).
Please log in to the Nhance portal and complete your enrollment at the earliest.
If you have already completed enrollment, please ignore this email.
Regards,
HR Team''';
}
Future<({String subject, String body})> _loadReminderEmailTemplate() async {
var subject = _defaultReminderSubject();
var body = _defaultReminderBody();
try {
final response = await apiService.getEnrollmentReminderEmailTemplateApi(
localClientId ?? widget.ClientId,
localClientPolicyId ?? widget.ClientPoliyId,
localClientBranchId ?? widget.clientBranchId,
localToken ?? widget.Token,
);
if (response['status'] == 'success' || response['status'] == true) {
final data = response['data'];
if (data is Map<String, dynamic>) {
subject = data['email_subject']?.toString().trim().isNotEmpty == true
? data['email_subject'].toString()
: data['subject']?.toString().trim().isNotEmpty == true
? data['subject'].toString()
: subject;
body = data['email_body']?.toString().trim().isNotEmpty == true
? data['email_body'].toString()
: data['body']?.toString().trim().isNotEmpty == true
? data['body'].toString()
: body;
}
}
} catch (e) {
logDebug('Reminder template fetch failed, using default: $e');
}
return (subject: subject, body: body);
}
Future<void> _confirmAndSendReminder() async {
final template = await _loadReminderEmailTemplate();
if (!mounted) return;
final subjectController = TextEditingController(text: template.subject);
final bodyController = TextEditingController(text: template.body);
final result = await showDialog<Map<String, String>?>(
context: context,
barrierDismissible: false,
builder: (dialogContext) {
return AlertDialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
title: Text(
'Send Reminder Email',
style: GoogleFonts.poppins(
fontWeight: FontWeight.w600,
fontSize: 18,
),
),
content: SizedBox(
width: 620,
child: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Review and edit the email template before sending to employees who have not completed enrollment.',
style: GoogleFonts.poppins(
fontSize: 13,
color: Colors.black54,
),
),
const SizedBox(height: 16),
Text(
'Subject',
style: GoogleFonts.poppins(
fontSize: 13,
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 6),
TextField(
controller: subjectController,
style: GoogleFonts.poppins(fontSize: 14),
decoration: InputDecoration(
hintText: 'Email subject',
filled: true,
fillColor: const Color(0xFFF5F5F5),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: BorderSide.none,
),
contentPadding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 10,
),
),
),
const SizedBox(height: 14),
Text(
'Email Body',
style: GoogleFonts.poppins(
fontSize: 13,
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 6),
TextField(
controller: bodyController,
minLines: 10,
maxLines: 14,
style: GoogleFonts.poppins(fontSize: 14, height: 1.45),
decoration: InputDecoration(
hintText: 'Email body',
filled: true,
fillColor: const Color(0xFFF5F5F5),
alignLabelWithHint: true,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: BorderSide.none,
),
contentPadding: const EdgeInsets.all(12),
),
),
],
),
),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(dialogContext),
child: Text(
'Cancel',
style: GoogleFonts.poppins(color: Colors.black54),
),
),
ElevatedButton(
onPressed: () {
final subject = subjectController.text.trim();
final body = bodyController.text.trim();
if (subject.isEmpty || body.isEmpty) {
ToastHelper.showWarningToast(
dialogContext,
'Subject and email body are required',
);
return;
}
Navigator.pop(dialogContext, {
'subject': subject,
'body': body,
});
},
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFFE26728),
),
child: Text(
'Send',
style: GoogleFonts.poppins(color: Colors.white),
),
),
],
);
},
);
subjectController.dispose();
bodyController.dispose();
if (result != null) {
await _sendEnrollmentReminder(
emailSubject: result['subject']!,
emailBody: result['body']!,
);
}
}
Future<void> _sendEnrollmentReminder({
required String emailSubject,
required String emailBody,
}) async {
setState(() => _isSendingReminder = true);
try {
final hrId = await tokenService.readValue('enrollmentHrId');
final response = await apiService.sendEnrollmentReminderApi(
localClientId ?? widget.ClientId,
localClientPolicyId ?? widget.ClientPoliyId,
localClientBranchId ?? widget.clientBranchId,
hrId ?? '',
localToken ?? widget.Token,
emailSubject: emailSubject,
emailBody: emailBody,
);
final ok = response['status'] == 'success' || response['status'] == true;
if (ok) {
final message = response['message']?.toString() ??
'Reminder sent successfully';
ToastHelper.showSuccessToast(context, message);
await _logReminderActivity();
} else {
ToastHelper.showErrorToast(
context,
response['message']?.toString() ?? 'Failed to send reminder',
);
}
} catch (e) {
logDebug('Reminder exception: $e');
ToastHelper.showErrorToast(
context,
'Failed to send reminder. Please try again.',
);
} finally {
if (mounted) {
setState(() => _isSendingReminder = false);
}
}
}
Future<void> _logReminderActivity() async {
final postId = await tokenService.readValue('empHrId');
final preId = await tokenService.readValue('enrollmentEmpPrimaryId');
const activityPre = 'send_enrollment_reminder';
try {
await apiService.getPreLogHrActivity(
postId!,
preId!,
localToken!,
activityPre,
);
} catch (e) {
logDebug('Reminder activity log failed: $e');
}
}
Future<void> getEcardBulkDownload() async {
try {
final emp_policy_ids = selectedEmployeeIds.toList();
@ -810,6 +1103,48 @@ class _HrPolicyDetailsState extends State<hrPolicyDetails>
const SizedBox(width: 12),
if (localTokenType == 'pre') ...[
SizedBox(
width: 142,
height: 37,
child: ElevatedButton(
onPressed: _isSendingReminder
? null
: _confirmAndSendReminder,
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFFE26728),
elevation: 0,
padding: EdgeInsets.zero,
disabledBackgroundColor:
const Color(0xFFE26728).withValues(alpha: 0.6),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
),
child: _isSendingReminder
? const SizedBox(
width: 18,
height: 18,
child: CircularProgressIndicator(
strokeWidth: 2,
color: Colors.white,
),
)
: Text(
'Reminder',
maxLines: 1,
softWrap: false,
style: GoogleFonts.poppins(
fontSize: 15,
fontWeight: FontWeight.w700,
color: Colors.white,
),
),
),
),
const SizedBox(width: 12),
],
SizedBox(
width: 116,
height: 37,
@ -1339,60 +1674,158 @@ class _HrPolicyDetailsState extends State<hrPolicyDetails>
// );
// }
Map<String, int> getFixedStatusCounts() {
int draftCount = 0;
int enrolledCount = 0;
Map<String, int> getPreStatusCounts() {
int empCount = 0;
int enrolled = 0;
int notEnrolled = 0;
int loggedIn = 0;
int notLoggedIn = 0;
int draft = 0;
for (final item in filteredData) {
final status = item['status']?.toString().toLowerCase();
for (final item in originalData) {
final status = item['status']?.toString().toLowerCase().trim() ?? '';
final relationship =
item['relationship']?.toString().toLowerCase().trim() ?? '';
if (status == 'draft') {
draftCount++;
} else if (status == 'under process') {
enrolledCount++;
if (relationship == 'self') empCount++;
if (_isEnrolledStatus(status)) {
enrolled++;
} else {
notEnrolled++;
}
if (_isLoggedInRow(item)) {
loggedIn++;
} else {
notLoggedIn++;
}
if (status == 'draft') draft++;
}
return {
'draft': draftCount,
'under process': enrolledCount,
'total': filteredData.length,
'emp_count': empCount,
'enrolled': enrolled,
'not_enrolled': notEnrolled,
'logged_in': loggedIn,
'not_logged_in': notLoggedIn,
'draft': draft,
};
}
Color _getPreFilterChipColor(String filter) {
switch (filter) {
case 'emp_count':
return const Color(0xFFE2FBCB);
case 'enrolled':
return const Color(0xFFBDF9D9);
case 'not_enrolled':
return const Color(0xFFFFE8AC);
case 'logged_in':
return const Color(0xFFC5F2F4);
case 'not_logged_in':
return const Color(0xFFE8EAF6);
case 'draft':
return const Color(0xFFF9EBBD);
default:
return const Color(0xFFB0BEC5);
}
}
Widget _buildPreStatusFilterChip({
required String filterKey,
required String label,
required int count,
}) {
final selected = _preStatusFilter == filterKey;
final color = _getPreFilterChipColor(filterKey);
return Material(
color: Colors.transparent,
child: InkWell(
onTap: () => _onPreStatusFilterTap(filterKey),
borderRadius: BorderRadius.circular(6),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
decoration: BoxDecoration(
color: color,
borderRadius: BorderRadius.circular(6),
border: Border.all(
color: selected ? const Color(0xFF009195) : Colors.transparent,
width: 2,
),
),
child: Text(
'$label - $count',
style: GoogleFonts.poppins(
fontSize: 12,
fontWeight: selected ? FontWeight.w700 : FontWeight.w600,
color: Colors.black,
),
),
),
),
);
}
Widget _buildStatusSummary() {
if (filteredData.isEmpty) {
if (originalData.isEmpty) {
return const SizedBox();
}
final statusCounts = getFixedStatusCounts();
final List<String> order = ['draft', 'under process', 'total'];
final statusCounts = getPreStatusCounts();
const filters = <Map<String, String>>[
{'key': 'emp_count', 'label': 'Emp Count'},
{'key': 'enrolled', 'label': 'Enroled'},
{'key': 'not_enrolled', 'label': 'Not Enroled'},
{'key': 'logged_in', 'label': 'Logged-In'},
{'key': 'not_logged_in', 'label': 'Not Logged-In'},
{'key': 'draft', 'label': 'Draft'},
];
return SizedBox(
height: 34,
height: 38,
child: ListView.separated(
scrollDirection: Axis.horizontal,
itemCount: order.length,
itemCount: filters.length + 1,
separatorBuilder: (_, __) => const SizedBox(width: 10),
itemBuilder: (context, index) {
final status = order[index];
final count = statusCounts[status] ?? 0;
final color = getStatusColor(status);
return Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
decoration: BoxDecoration(
color: color,
borderRadius: BorderRadius.circular(6),
),
child: Text(
'${status.toUpperCase()} - $count',
style: GoogleFonts.poppins(
fontSize: 12,
fontWeight: FontWeight.w600,
color: Colors.black,
if (index == filters.length) {
return Material(
color: Colors.transparent,
child: InkWell(
onTap: _preStatusFilter == null ? null : _resetPreStatusFilter,
borderRadius: BorderRadius.circular(6),
child: Container(
padding:
const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
decoration: BoxDecoration(
color: const Color(0xFFF0F0F0),
borderRadius: BorderRadius.circular(6),
border: Border.all(
color: _preStatusFilter != null
? const Color(0xFF009195)
: const Color(0xFFD0D0D0),
),
),
child: Text(
'Reset',
style: GoogleFonts.poppins(
fontSize: 12,
fontWeight: FontWeight.w600,
color: _preStatusFilter != null
? const Color(0xFF009195)
: Colors.black54,
),
),
),
),
),
);
}
final filter = filters[index];
return _buildPreStatusFilterChip(
filterKey: filter['key']!,
label: filter['label']!,
count: statusCounts[filter['key']] ?? 0,
);
},
),

View File

@ -1028,6 +1028,66 @@ class ApiService {
// return response;
// }
Future<Map<String, dynamic>> getEnrollmentReminderEmailTemplateApi(
String clientId,
String clientPolicyId,
String clientBranchId,
String token,
) async {
final url = Uri.parse(
'${Environment.apiUrl}getEnrollmentReminderEmailTemplate'
'?client_id=$clientId&client_policy_id=$clientPolicyId'
'&client_branch_id=$clientBranchId',
);
final headers = {
'Authorization': 'Bearer ${token ?? ''}',
'APP-SIGNATURE':
'nhance-2025-signature-T8f6gV9k5fK2wf5V5c4vId3b0J8z6cAm2x8Y',
};
return _makeGetRequest(url, headers);
}
Future<Map<String, dynamic>> sendEnrollmentReminderApi(
String clientId,
String clientPolicyId,
String clientBranchId,
String hrId,
String token, {
required String emailSubject,
required String emailBody,
}) async {
final url = Uri.parse('${Environment.apiUrl}sendEnrollmentReminderEmail');
final headers = {
'Authorization': 'Bearer ${token ?? ''}',
'APP-SIGNATURE':
'nhance-2025-signature-T8f6gV9k5fK2wf5V5c4vId3b0J8z6cAm2x8Y',
'Content-Type': 'application/json',
};
final body = {
'client_id': clientId,
'client_policy_id': clientPolicyId,
'client_branch_id': clientBranchId,
'hr_id': hrId,
'email_subject': emailSubject,
'email_body': emailBody,
};
final response = await http.post(
url,
headers: headers,
body: jsonEncode(body),
);
if (response.statusCode == 200) {
return jsonDecode(response.body);
} else {
throw Exception(
'Failed to send enrollment reminder: ${response.statusCode} ${response.body}');
}
}
Future<Map<String, dynamic>> getEmployeeAndDependenceToApiPre(
String clintID, String getPolicyNo, String empRefId, String token) async {
logDebug(_hrtoken);