diff --git a/lib/customAppBar/side_bar.dart b/lib/customAppBar/side_bar.dart index 96394c6..18da3ea 100644 --- a/lib/customAppBar/side_bar.dart +++ b/lib/customAppBar/side_bar.dart @@ -287,6 +287,17 @@ class _NhanceSideBarState extends State { // 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 { ); }), - 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(), diff --git a/lib/presentation/hrPolicyDetails.dart b/lib/presentation/hrPolicyDetails.dart index 813cb75..4ccb894 100755 --- a/lib/presentation/hrPolicyDetails.dart +++ b/lib/presentation/hrPolicyDetails.dart @@ -101,6 +101,7 @@ class _HrPolicyDetailsState extends State List> getCDPolicies = []; bool isLoading = false; bool _isLoading = false; + bool _isSendingReminder = false; // dynamic clintID; late TabController _tabController; // List dataPolicy = []; @@ -124,6 +125,9 @@ class _HrPolicyDetailsState extends State 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 selectedEmployeeIds = {}; @@ -314,7 +318,7 @@ class _HrPolicyDetailsState extends State setState(() { getCDPolicies = List>.from(response['data']); originalData = getCDPolicies; - filteredData = List.from(originalData); + _refreshFilteredData(); logDebug('filteredData'); logDebug(filteredData); }); @@ -387,7 +391,7 @@ class _HrPolicyDetailsState extends State setState(() { getCDPolicies = List>.from(data); originalData = getCDPolicies; - filteredData = List.from(originalData); + _refreshFilteredData(); isLoading = false; }); } else { @@ -455,65 +459,100 @@ class _HrPolicyDetailsState extends State } } - 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 row) => + row['emp_is_active']?.toString() == '1'; - bool statusMatch; + bool _matchesPreStatusFilter(Map 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 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> 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> data) { List> rows = []; @@ -605,6 +644,260 @@ class _HrPolicyDetailsState extends State 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) { + 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 _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?>( + 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 _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 _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 getEcardBulkDownload() async { try { final emp_policy_ids = selectedEmployeeIds.toList(); @@ -810,6 +1103,48 @@ class _HrPolicyDetailsState extends State 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 // ); // } - Map getFixedStatusCounts() { - int draftCount = 0; - int enrolledCount = 0; + Map 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 order = ['draft', 'under process', 'total']; + final statusCounts = getPreStatusCounts(); + const filters = >[ + {'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, ); }, ), diff --git a/lib/service/api_service.dart b/lib/service/api_service.dart index 6d1b23e..2cc4c26 100755 --- a/lib/service/api_service.dart +++ b/lib/service/api_service.dart @@ -1028,6 +1028,66 @@ class ApiService { // return response; // } + Future> 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> 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> getEmployeeAndDependenceToApiPre( String clintID, String getPolicyNo, String empRefId, String token) async { logDebug(_hrtoken);