enrollment-app/lib/presentation/hrPolicyDetails.dart
2026-08-06 15:03:54 +05:30

5311 lines
183 KiB
Dart
Executable File

import 'dart:math' as math;
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:nhancepolicy/customAppBar/customAppBar.dart';
import 'package:nhancepolicy/presentation/policies.dart';
import 'package:nhancepolicy/presentation/preFileUpload.dart';
import 'package:nhancepolicy/presentation/postFileUpload.dart';
import 'package:nhancepolicy/service/api_service.dart';
import 'package:nhancepolicy/presentation/claims.dart';
import 'package:jwt_decode/jwt_decode.dart';
import 'dart:convert';
import 'dart:async';
import 'package:http/http.dart' as http;
import 'package:nhancepolicy/customAppBar/toastHelper.dart';
import 'package:nhancepolicy/service/token_storage_service.dart';
import 'package:universal_html/html.dart' as html;
import 'dart:typed_data';
import 'dart:io';
import 'package:flutter/foundation.dart' show kIsWeb;
import 'package:flutter/services.dart';
import 'package:intl/intl.dart';
import 'package:path_provider/path_provider.dart';
import 'package:path/path.dart' as path;
import 'package:collection/collection.dart';
import 'package:url_launcher/url_launcher.dart';
import '../customAppBar/base_layout.dart';
import '../customAppBar/customFooter.dart';
import '../service/secure_pop_scope.dart';
import 'package:nhancepolicy/presentation/email_template/email_template_models.dart';
import 'package:nhancepolicy/presentation/email_template/reminder_email_template_dialog.dart';
import 'package:nhancepolicy/logger.dart';
class hrPolicyDetails extends StatefulWidget {
final String ClientId;
final String policyTypeId;
// final String ClientBranchId;
final String ClientPoliyId;
final String clientBranchId;
final String Token;
final String TokenType;
final String cardType;
final String cardPolicyNo;
final String cardInsurer_name;
final String cardPolicy_name;
final String cardPolicy_ExpDate;
final String total_premium;
final int is_ecard_bulk_download_for_employee;
const hrPolicyDetails(
{Key? key,
required this.ClientId,
required this.policyTypeId,
// required this.ClientBranchId,
required this.ClientPoliyId,
required this.clientBranchId,
required this.Token,
required this.TokenType,
required this.cardType,
required this.cardPolicyNo,
required this.cardInsurer_name,
required this.cardPolicy_name,
required this.cardPolicy_ExpDate,
required this.total_premium,
required this.is_ecard_bulk_download_for_employee})
: super(key: key);
@override
State<hrPolicyDetails> createState() => _HrPolicyDetailsState();
}
class _HrPolicyDetailsState extends State<hrPolicyDetails>
with TickerProviderStateMixin {
String? localClientId;
String? localPolicyTypeId;
String? localClientPolicyId;
String? localClientBranchId;
String? localToken;
String? localTokenType;
String? localCardType;
String? localCardPolicyNo;
String? localCardInsurerName;
String? localCardPolicyName;
String? localCardPolicyExpDate;
String? localTotalPremium;
int localIsEcardBulkDownload = 0;
final tokenService = TokenStorageService();
dynamic empPrimaryId;
dynamic empClientId;
dynamic empClientBranchId;
dynamic empHrId;
dynamic enrollmentClient_id;
dynamic enrollmentEmpClientBranchId;
dynamic enrollmentHrId;
String? _postPreToken = '';
Uint8List? fileBytes;
String empCodeFromHrPolcy = '';
bool hasAnyEcardLink = false;
late String _token;
List<Map<String, dynamic>> getEmpDependenceByClintId = [];
List<Map<String, dynamic>> getCDPolicies = [];
bool isLoading = false;
bool _isLoading = false;
bool _isSendingReminder = false;
bool _isLoadingPolicyTerms = false;
bool _isLoadingHospitalList = false;
// dynamic clintID;
late TabController _tabController;
// List<dynamic> dataPolicy = [];
List<dynamic> reversedDataPolicy = [];
List<Map<String, dynamic>> originalData = []; // Original data source
List<Map<String, dynamic>> filteredData = []; // Filtered data source
dynamic argumentsData;
dynamic policyType;
dynamic policyName;
dynamic clientPolicyId;
dynamic clientId;
dynamic empRefId;
String? modulesString;
final storeModuleId = 4;
bool hasModule = false;
int inceptionType = 0;
TextEditingController searchController = TextEditingController();
late ApiService apiService;
int _currentPage = 1;
int _rowsPerPage = 5;
/// Selected filter chip key (null = none). Sent as `card_data` on search.
String? _preStatusFilter;
/// Filter chips from initial `card_data=all` API response.
List<Map<String, dynamic>> _filterChips = [];
/// Employee table is hidden until the user searches.
bool _hasSearched = false;
bool _isSearching = false;
bool _isLoadingCards = false;
bool _isExportingExcel = false;
Timer? _searchDebounce;
/// holds selected employee ids as String or int (be consistent)
final Set<String> selectedEmployeeIds = {};
/// ids shown in the current page (for header checkbox)
List<String> currentPageIds = [];
List<dynamic> get _paginatedData {
final total = filteredData.length;
if (total == 0) return [];
final startIndex = ((_currentPage - 1) * _rowsPerPage).clamp(0, total);
final endIndex = (_currentPage * _rowsPerPage).clamp(0, total);
if (startIndex >= endIndex) return [];
return filteredData.sublist(startIndex, endIndex);
}
Color getStatusColor(String status) {
switch (status.toLowerCase()) {
case 'draft':
return const Color(0xFFF9EBBD);
case 'enrolled':
return const Color(0xFFBDF9D9);
case 'total':
return const Color(0xFFE2FBCB);
case 'under process':
return Color(0xFFBDF9D9);
case 'active':
return Colors.green;
case 'inactive':
return Colors.red;
default:
return const Color(0xFFB0BEC5);
}
}
//
// @override
// void initState() {
// super.initState();
// apiService = ApiService(context); // Initialize ApiService here
//
// logDebug("_PreEnrollmentState 1");
// getCDPoliciesDetails();
// logDebug('allowed_modules');
// }
@override
void initState() {
super.initState();
apiService = ApiService(context);
// If you are using TabBar, you MUST initialize this:
_tabController = TabController(length: 2, vsync: this);
restorePolicyData();
}
Future<void> restorePolicyData() async {
localClientId = widget.ClientId.isNotEmpty
? widget.ClientId
: await tokenService.readValue('hr_ClientId');
localPolicyTypeId = widget.policyTypeId.isNotEmpty
? widget.policyTypeId
: await tokenService.readValue('hr_policyTypeId');
localClientPolicyId = widget.ClientPoliyId.isNotEmpty
? widget.ClientPoliyId
: await tokenService.readValue('hr_ClientPoliyId');
localClientBranchId = widget.clientBranchId.isNotEmpty
? widget.clientBranchId
: await tokenService.readValue('hr_clientBranchId');
localToken = widget.Token.isNotEmpty
? widget.Token
: await tokenService.readValue('hr_Token');
// Browser refresh often drops constructor Token — use session token.
if (localToken == null || localToken!.toString().trim().isEmpty) {
localToken = tokenService.getCurrentToken();
}
localTokenType = widget.TokenType.isNotEmpty
? widget.TokenType
: await tokenService.readValue('hr_TokenType');
localCardType = widget.cardType.isNotEmpty
? widget.cardType
: await tokenService.readValue('hr_cardType');
localCardPolicyNo = widget.cardPolicyNo.isNotEmpty
? widget.cardPolicyNo
: await tokenService.readValue('hr_cardPolicyNo');
localCardInsurerName = widget.cardInsurer_name.isNotEmpty
? widget.cardInsurer_name
: await tokenService.readValue('hr_cardInsurer_name');
localCardPolicyName = widget.cardPolicy_name.isNotEmpty
? widget.cardPolicy_name
: await tokenService.readValue('hr_cardPolicy_name');
localCardPolicyExpDate = widget.cardPolicy_ExpDate.isNotEmpty
? widget.cardPolicy_ExpDate
: await tokenService.readValue('hr_cardPolicy_ExpDate');
localTotalPremium = widget.total_premium.isNotEmpty
? widget.total_premium
: await tokenService.readValue('hr_total_premium');
final savedBulk =
await tokenService.readValue('hr_is_ecard_bulk_download_for_employee');
localIsEcardBulkDownload = widget.is_ecard_bulk_download_for_employee != 0
? widget.is_ecard_bulk_download_for_employee
: int.tryParse(savedBulk ?? '0') ?? 0;
// Persist so browser refresh can rebuild this page without empty API params.
await _persistPolicyContext();
if (!_hasRequiredPolicyContext()) {
logDebug(
'hrPolicyDetails missing client/policy/branch after restore — '
'skipping API and returning to policies',
);
if (!mounted) return;
setState(() {
_isLoadingCards = false;
isLoading = false;
});
WidgetsBinding.instance.addPostFrameCallback((_) {
if (!mounted) return;
Navigator.pushReplacementNamed(context, 'policies');
});
return;
}
getCDPoliciesDetails();
}
bool _hasRequiredPolicyContext() {
final clientId = (localClientId ?? '').toString().trim();
final policyId = (localClientPolicyId ?? '').toString().trim();
final branchId = (localClientBranchId ?? '').toString().trim();
final token = (localToken ?? '').toString().trim();
return clientId.isNotEmpty &&
policyId.isNotEmpty &&
branchId.isNotEmpty &&
token.isNotEmpty;
}
Future<void> _persistPolicyContext() async {
Future<void> writeIfPresent(String key, dynamic value) async {
final text = value?.toString().trim() ?? '';
if (text.isEmpty) return;
await tokenService.writeValue(key, text);
}
await writeIfPresent('hr_ClientId', localClientId);
await writeIfPresent('hr_policyTypeId', localPolicyTypeId);
await writeIfPresent('hr_ClientPoliyId', localClientPolicyId);
await writeIfPresent('hr_clientBranchId', localClientBranchId);
await writeIfPresent('hr_Token', localToken);
await writeIfPresent('hr_TokenType', localTokenType);
await writeIfPresent('hr_cardType', localCardType);
await writeIfPresent('hr_cardPolicyNo', localCardPolicyNo);
await writeIfPresent('hr_cardInsurer_name', localCardInsurerName);
await writeIfPresent('hr_cardPolicy_name', localCardPolicyName);
await writeIfPresent('hr_cardPolicy_ExpDate', localCardPolicyExpDate);
await writeIfPresent('hr_total_premium', localTotalPremium);
await tokenService.writeValue(
'hr_is_ecard_bulk_download_for_employee',
localIsEcardBulkDownload.toString(),
);
}
Future<void> clearPolicyStorage() async {
await tokenService.removeValue('hr_ClientId');
await tokenService.removeValue('hr_policyTypeId');
await tokenService.removeValue('hr_ClientPoliyId');
await tokenService.removeValue('hr_clientBranchId');
await tokenService.removeValue('hr_Token');
await tokenService.removeValue('hr_TokenType');
await tokenService.removeValue('hr_cardType');
await tokenService.removeValue('hr_cardPolicyNo');
await tokenService.removeValue('hr_cardInsurer_name');
await tokenService.removeValue('hr_cardPolicy_name');
await tokenService.removeValue('hr_cardPolicy_ExpDate');
await tokenService.removeValue('hr_total_premium');
await tokenService.removeValue('hr_is_ecard_bulk_download_for_employee');
}
// Future<void> _loadToken() async {
// _postPreToken = tokenService.getCurrentToken();
// if(localTokenType == "post") {
// empClientId = await tokenService.readValue('empClientId');
// empClientBranchId = await tokenService.readValue('empClientBranchId');
// empHrId = await tokenService.readValue('empHrId');
// }
// if(localTokenType == "pre") {
// enrollmentClient_id = await tokenService.readValue('enrollmentClient_id');
// enrollmentEmpClientBranchId = await tokenService.readValue('enrollmentEmpClientBranchId');
// enrollmentHrId = await tokenService.readValue('enrollmentHrId');
// }
//
// getCDPoliciesDetails();
// }
Future<void> getCDPoliciesDetails_06FEB() async {
logDebug('9');
setState(() {
isLoading = true;
});
try {
logDebug('10');
modulesString = await tokenService.readValue('empAllowed_modules');
// modulesString = "[2,3]";
logDebug("empmodulesString - $modulesString");
if (modulesString != null && modulesString!.trim().isNotEmpty) {
final List<int>? moduleList = modulesString
?.replaceAll('[', '')
.replaceAll(']', '')
.split(',')
.map((e) => int.tryParse(e.trim()) ?? -1) // convert to int safely
.where((id) => id != -1) // filter out invalid
.toList();
hasModule = moduleList!.contains(storeModuleId);
}
final response = localTokenType == "post"
? await apiService.getEmployeeAndDependenceToApi(
localClientId ?? widget.ClientId,
localClientPolicyId ?? widget.ClientPoliyId,
localClientBranchId ?? widget.clientBranchId,
localToken ?? widget.Token)
: await apiService.getEmployeeAndDependenceToApiPre(
localClientId ?? widget.ClientId,
localClientPolicyId ?? widget.ClientPoliyId,
localClientBranchId ?? widget.clientBranchId,
localToken ?? widget.Token!);
if (response['status'] == 'success') {
setState(() {
isLoading = false;
});
setState(() {
getCDPolicies = List<Map<String, dynamic>>.from(response['data']);
originalData = getCDPolicies;
_refreshFilteredData();
logDebug('filteredData');
logDebug(filteredData);
});
} else {
setState(() {
isLoading = false;
});
// ToastHelper.showWarningToast(
// context, 'Request failed with status: ${response.statusCode}');
// logDebug('Request failed with status: ${response['code']}');
}
} catch (e) {
setState(() {
isLoading = false;
});
logDebug('Exception occurred: $e');
} finally {
setState(() {
_isLoading = false;
});
}
}
Future<void> _loadAllowedModules() async {
modulesString = await tokenService.readValue('empAllowed_modules');
logDebug("empmodulesString - $modulesString");
if (modulesString != null && modulesString!.trim().isNotEmpty) {
final List<int>? moduleList = modulesString
?.replaceAll('[', '')
.replaceAll(']', '')
.split(',')
.map((e) => int.tryParse(e.trim()) ?? -1)
.where((id) => id != -1)
.toList();
hasModule = moduleList?.contains(storeModuleId) ?? false;
}
}
Future<Map<String, dynamic>> _fetchEmployees({
dynamic cardData,
String? searchKey,
}) async {
final clientId = (localClientId ?? widget.ClientId).toString().trim();
final policyId = (localClientPolicyId ?? widget.ClientPoliyId).toString().trim();
final branchId =
(localClientBranchId ?? widget.clientBranchId).toString().trim();
var token = (localToken ?? widget.Token).toString().trim();
if (token.isEmpty) {
token = tokenService.getCurrentToken()?.trim() ?? '';
localToken = token;
}
if (clientId.isEmpty || policyId.isEmpty || branchId.isEmpty || token.isEmpty) {
logDebug(
'Skipping getEmployeeAndDependenceByClientId — empty params '
'client=$clientId policy=$policyId branch=$branchId tokenEmpty=${token.isEmpty}',
);
return {
'status': 'error',
'code': 400,
'message': 'Missing policy context',
'data': [],
};
}
if (localTokenType == 'post') {
return apiService.getEmployeeAndDependenceToApi(
clientId,
policyId,
branchId,
token,
cardData: cardData,
searchKey: searchKey,
);
}
return apiService.getEmployeeAndDependenceToApiPre(
clientId,
policyId,
branchId,
token,
cardData: cardData,
searchKey: searchKey,
);
}
String _chipLabelForKey(String key) {
const labels = <String, String>{
'emp_count': 'Emp Count',
'submitted': 'Submitted',
'logged_in': 'Logged-In',
'not_logged_in': 'Not Logged-In',
'draft': 'Draft',
};
final normalized = key.toLowerCase().trim();
return labels[normalized] ?? key;
}
List<Map<String, dynamic>> _parseFilterChips(Map<String, dynamic> response) {
// API returns counts in top-level `card_data`, e.g.
// { "emp_count": 10, "submitted": 2, "logged_in": 6, ... }
final source = response['card_data'];
if (source is! Map) return [];
const allowedKeys = [
'emp_count',
'submitted',
'logged_in',
'not_logged_in',
'draft',
];
final map = Map<String, dynamic>.from(source);
final chips = <Map<String, dynamic>>[];
for (final key in allowedKeys) {
if (!map.containsKey(key)) continue;
final count = int.tryParse('${map[key]}') ?? 0;
chips.add({
'key': key,
'label': _chipLabelForKey(key),
'count': count,
});
}
return chips;
}
List<Map<String, dynamic>> _parseEmployeeList(Map<String, dynamic> response) {
final data = response['data'];
if (data is List) {
return data
.whereType<Map>()
.map((e) => Map<String, dynamic>.from(e))
.toList();
}
if (data is Map) {
final nested = data['employees'] ??
data['list'] ??
data['records'] ??
data['rows'] ??
data['data'];
if (nested is List) {
return nested
.whereType<Map>()
.map((e) => Map<String, dynamic>.from(e))
.toList();
}
}
return [];
}
/// Initial load: fetch filter chips only (`card_data=all`). No employee table.
Future<void> getCDPoliciesDetails() async {
logDebug('getCDPoliciesDetails started (card_data=all)');
setState(() {
_isLoadingCards = true;
isLoading = true;
_hasSearched = false;
originalData = [];
filteredData = [];
getCDPolicies = [];
});
try {
await _loadAllowedModules();
logDebug('Calling card_data=all API with TokenType: ${localTokenType}');
final response = await _fetchEmployees(cardData: 'all');
logDebug('Card API Response: ${response.toString()}');
if (response != null && response['status'] == 'success') {
final chips = _parseFilterChips(response);
if (mounted) {
setState(() {
_filterChips = chips;
_isLoadingCards = false;
isLoading = false;
});
}
} else {
if (mounted) {
setState(() {
_filterChips = [];
_isLoadingCards = false;
isLoading = false;
});
}
}
} catch (e, stackTrace) {
logDebug('❌ Exception occurred: $e');
logDebug('Stack trace: $stackTrace');
if (mounted) {
setState(() {
_isLoadingCards = false;
isLoading = false;
});
ToastHelper.showErrorToast(context,
'Failed to load filters. Please check your connection and try again.');
}
}
}
/// Search / filter employee load.
/// - Chip selected + search → local filter only (no API).
/// - No chip + search → API with empty `card_data`.
/// - Chip tap → API with chip `card_data`, then apply local search if any.
Future<void> _searchEmployees() async {
final query = searchController.text.trim();
final hasChip = _preStatusFilter != null;
if (query.isEmpty && !hasChip) {
if (!mounted) return;
setState(() {
_hasSearched = false;
_isSearching = false;
originalData = [];
filteredData = [];
getCDPolicies = [];
_currentPage = 1;
});
return;
}
// Chip already loaded: filter that list on the frontend.
if (hasChip && _hasSearched && originalData.isNotEmpty && query.isNotEmpty) {
_applyLocalSearchFilter();
return;
}
if (hasChip) {
await _loadEmployeesByChip();
return;
}
await _searchEmployeesViaApi(query);
}
/// No chip selected — search via API (`card_data` empty).
Future<void> _searchEmployeesViaApi(String query) async {
if (!mounted) return;
setState(() {
_isSearching = true;
isLoading = true;
});
try {
final response = await _fetchEmployees(
cardData: '',
searchKey: query,
);
logDebug('Search API Response: ${response.toString()}');
if (!mounted) return;
if (response != null && response['status'] == 'success') {
final employees = _parseEmployeeList(response);
final chips = _parseFilterChips(response);
setState(() {
getCDPolicies = employees;
originalData = employees;
filteredData = List<Map<String, dynamic>>.from(employees);
hasAnyEcardLink = filteredData.any(
(item) => item['ecard_download_link'] != null,
);
_hasSearched = true;
_currentPage = 1;
if (chips.isNotEmpty) {
_filterChips = chips;
}
_isSearching = false;
isLoading = false;
});
} else {
setState(() {
getCDPolicies = [];
originalData = [];
filteredData = [];
_hasSearched = true;
_isSearching = false;
isLoading = false;
});
}
} catch (e, stackTrace) {
logDebug('❌ Search exception: $e');
logDebug('Stack trace: $stackTrace');
if (mounted) {
setState(() {
_isSearching = false;
isLoading = false;
_hasSearched = true;
});
ToastHelper.showErrorToast(
context, 'Failed to search members. Please try again.');
}
}
}
/// Chip selected — load that filter from API (no search param).
Future<void> _loadEmployeesByChip() async {
final chip = _preStatusFilter;
if (chip == null) return;
if (!mounted) return;
setState(() {
_isSearching = true;
isLoading = true;
});
try {
final response = await _fetchEmployees(
cardData: chip,
searchKey: null,
);
logDebug('Chip API Response: ${response.toString()}');
if (!mounted) return;
if (response != null && response['status'] == 'success') {
final employees = _parseEmployeeList(response);
final chips = _parseFilterChips(response);
setState(() {
getCDPolicies = employees;
originalData = employees;
_hasSearched = true;
_currentPage = 1;
if (chips.isNotEmpty) {
_filterChips = chips;
}
_isSearching = false;
isLoading = false;
});
_applyLocalSearchFilter();
} else {
setState(() {
getCDPolicies = [];
originalData = [];
filteredData = [];
_hasSearched = true;
_isSearching = false;
isLoading = false;
});
}
} catch (e, stackTrace) {
logDebug('❌ Chip load exception: $e');
logDebug('Stack trace: $stackTrace');
if (mounted) {
setState(() {
_isSearching = false;
isLoading = false;
_hasSearched = true;
});
ToastHelper.showErrorToast(
context, 'Failed to load members. Please try again.');
}
}
}
/// Filter already-loaded chip data locally (no API).
void _applyLocalSearchFilter() {
if (!mounted) return;
final query = searchController.text.trim().toLowerCase();
setState(() {
if (query.isEmpty) {
filteredData = List<Map<String, dynamic>>.from(originalData);
} else {
filteredData = originalData
.where((row) => _matchesSearch(row, query))
.toList();
}
_currentPage = 1;
hasAnyEcardLink = filteredData.any(
(item) => item['ecard_download_link'] != null,
);
_isSearching = false;
isLoading = false;
});
}
Future<void> getEcardDownload(String? empCode, String? empId,
String? clientPolicyId, String? policyNo) async {
final eCarDParams = {
'id': empId,
'emp_code': empCode,
'client_policy_id': clientPolicyId,
'policy_no': policyNo
};
final response = await apiService.getEcardRequest(eCarDParams, localToken!);
logDebug('check 1');
final ecardDownloadUrl = response['data']['eCardDownload'];
final message = response['data']['message'];
if (ecardDownloadUrl != null) {
logDebug('✅ Link: $ecardDownloadUrl');
await _launchURL(ecardDownloadUrl); // Only launch if status is success
// ToastHelper.showSuccessToast(context, message);
} else {
logDebug('❌ Error: $message');
ToastHelper.showErrorToast(context, message);
}
}
Future<void> copyEcardLink(String? ecardLink) async {
if (ecardLink == null || ecardLink.trim().isEmpty) {
if (mounted) {
ToastHelper.showErrorToast(context, 'E-card link not available');
}
return;
}
if (kIsWeb) {
await html.window.navigator.clipboard?.writeText(ecardLink);
} else {
await Clipboard.setData(ClipboardData(text: ecardLink));
}
if (mounted) {
ToastHelper.showSuccessToast(context, 'Link copied to clipboard');
}
}
Future<void> sendEcardViaEmail(
dynamic empPolicyId,
dynamic clientPolicyId,
) async {
try {
final response = await apiService.sendMailForIndividualEmployeeEcard(
empPolicyId: empPolicyId,
clientPolicyId: clientPolicyId,
token: localToken!,
);
if (!mounted) return;
if (response['status'] == true) {
ToastHelper.showSuccessToast(
context,
response['message']?.toString() ?? 'Mail sent successfully',
);
} else {
ToastHelper.showErrorToast(
context,
response['message']?.toString() ?? 'Failed to send mail.',
);
}
} catch (e) {
logDebug('❌ sendEcardViaEmail error: $e');
if (mounted) {
ToastHelper.showErrorToast(
context,
'Failed to send mail. Please try again.',
);
}
}
}
Future<void> _launchURL(String url) async {
final Uri uri = Uri.parse(url); // Parse the URL properly
logDebug('_launchURL $uri');
logDebug('If $uri');
if (kIsWeb) {
// Open in current browser tab on web.
await launchUrl(uri, webOnlyWindowName: '_self');
} else {
await launchUrl(uri, mode: LaunchMode.externalApplication);
}
}
bool _isEnrolledStatus(String status) =>
status == 'under process' ||
status == 'enrolled' ||
status == 'submitted';
String _getRowStatus(Map<String, dynamic> row) {
final raw = localTokenType == 'pre'
? (row['emp_status'] ?? row['status'])
: row['status'];
return raw?.toString().toLowerCase().trim() ?? '';
}
String? _getRowStatusRaw(Map<String, dynamic> row) {
final raw = localTokenType == 'pre'
? (row['emp_status'] ?? row['status'])
: row['status'];
final value = raw?.toString().trim();
return value == null || value.isEmpty ? null : value;
}
String _getLoggedInValue(Map<String, dynamic> row) {
final loggedInRaw = row['logged_in'] ?? row['emp_is_active'];
final value = loggedInRaw?.toString().toLowerCase().trim() ?? '';
// API can return: null / Yes / No / 1 / 0 / true / false.
if (value.isEmpty || value == 'null' || value == 'no' || value == '0' || value == 'false') {
return 'no';
}
return (value == 'yes' || value == '1' || value == 'true') ? 'yes' : 'no';
}
bool _isLoggedInRow(Map<String, dynamic> row) =>
_getLoggedInValue(row) == 'yes';
bool _isSelfRow(Map<String, dynamic> row) =>
row['relationship']?.toString().toLowerCase().trim() == 'self';
String _getLoggedInDisplayLabel(Map<String, dynamic> row) =>
_getLoggedInValue(row) == 'yes' ? 'Yes' : 'No';
Color _getLoggedInChipColor(Map<String, dynamic> row) =>
_getLoggedInValue(row) == 'yes'
? const Color(0xFFBDF9D9)
: const Color(0xFFE8EAF6);
bool _matchesPreStatusFilter(Map<String, dynamic> row, String filter) {
final status = _getRowStatus(row);
final relationship = row['relationship']?.toString().toLowerCase().trim() ?? '';
switch (filter) {
case 'emp_count':
return relationship == 'self';
case 'enrolled':
return _isSelfRow(row) && _isEnrolledStatus(status);
case 'not_enrolled':
return !_isEnrolledStatus(status);
case 'logged_in':
return _isSelfRow(row) && _isLoggedInRow(row);
case 'not_logged_in':
return _isSelfRow(row) && !_isLoggedInRow(row);
case 'draft':
return _isSelfRow(row) && status == 'draft';
default:
return true;
}
}
bool _matchesSearch(Map<String, dynamic> row, String lowerQuery) {
final status = _getRowStatus(row);
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['tpa_id']?.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() {
// Server already filters by card_data; keep local copy for pagination.
filteredData = List<Map<String, dynamic>>.from(originalData);
hasAnyEcardLink = filteredData.any(
(item) => item['ecard_download_link'] != null,
);
}
void search(String query) {
setState(() {}); // refresh clear / loading suffix
_searchDebounce?.cancel();
_searchDebounce = Timer(const Duration(milliseconds: 450), () {
// Chip selected → local filter only. No chip → API search.
if (_preStatusFilter != null && _hasSearched) {
_applyLocalSearchFilter();
} else {
_searchEmployees();
}
});
}
void _onPreStatusFilterTap(String filter) {
setState(() {
_preStatusFilter = filter;
_currentPage = 1;
});
_searchDebounce?.cancel();
_loadEmployeesByChip();
}
void _resetPreStatusFilter() {
_resetToLanding();
}
Future<void> downloadEmployeeListExcel() async {
if (_isExportingExcel) return;
final clientId = (localClientId ?? widget.ClientId).toString().trim();
final policyId =
(localClientPolicyId ?? widget.ClientPoliyId).toString().trim();
final branchId =
(localClientBranchId ?? widget.clientBranchId).toString().trim();
if (clientId.isEmpty || policyId.isEmpty || branchId.isEmpty) {
if (!mounted) return;
ToastHelper.showErrorToast(context, 'Missing client/policy/branch details');
return;
}
setState(() => _isExportingExcel = true);
try {
final response = await apiService.downloadEmployeeListExcel(
clientId: clientId,
policyId: policyId,
branchId: branchId,
isPost: localTokenType == 'post',
token: localToken,
);
final apiMessage = _excelDownloadErrorMessage(response);
if (apiMessage != null) {
if (!mounted) return;
ToastHelper.showErrorToast(context, apiMessage);
return;
}
if (response.statusCode != 200 || response.bodyBytes.isEmpty) {
throw Exception('Download failed (${response.statusCode})');
}
final apiContentType = response.headers['content-type'] ?? '';
final contentDisposition =
response.headers['content-disposition'] ?? '';
final excelContentType =
apiContentType.contains('spreadsheetml') ||
apiContentType.contains('ms-excel')
? apiContentType
: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet';
final utf8FileNameMatch = RegExp(
"filename\\*=UTF-8''([^;]+)",
caseSensitive: false,
).firstMatch(contentDisposition);
final plainFileNameMatch = RegExp(
'filename="?([^";]+)"?',
caseSensitive: false,
).firstMatch(contentDisposition);
final rawFileName =
utf8FileNameMatch?.group(1) ?? plainFileNameMatch?.group(1);
final fileName = rawFileName != null && rawFileName.trim().isNotEmpty
? Uri.decodeComponent(rawFileName.trim())
: 'employee_list_${localCardPolicyNo ?? policyId}.xlsx';
final blob = html.Blob([response.bodyBytes], excelContentType);
final url = html.Url.createObjectUrlFromBlob(blob);
final anchor = html.AnchorElement(href: url)
..setAttribute('download', fileName)
..click();
html.Url.revokeObjectUrl(url);
await handleExportAction();
if (!mounted) return;
ToastHelper.showSuccessToast(context, 'Employee list Excel downloaded');
} catch (e) {
logDebug('Employee list Excel download failed: $e');
if (!mounted) return;
ToastHelper.showErrorToast(
context,
'Could not download Excel. Please try again.',
);
} finally {
if (mounted) setState(() => _isExportingExcel = false);
}
}
/// Returns API error message when Excel endpoint responds with JSON error.
String? _excelDownloadErrorMessage(http.Response response) {
final contentType = response.headers['content-type'] ?? '';
final body = response.body.trim();
final looksLikeJson = contentType.contains('application/json') ||
body.startsWith('{') ||
body.startsWith('[');
if (!looksLikeJson || body.isEmpty) return null;
try {
final decoded = jsonDecode(body);
if (decoded is! Map) return null;
final status = decoded['status'];
final isFailure = status == false ||
status == 0 ||
status?.toString().toLowerCase() == 'false' ||
status?.toString().toLowerCase() == 'error' ||
status?.toString().toLowerCase() == 'failed';
if (!isFailure && response.statusCode == 200) return null;
final message = decoded['message']?.toString().trim();
if (message != null && message.isNotEmpty) return message;
return 'Could not download Excel.';
} catch (_) {
return null;
}
}
Future<void> handleExportAction() async {
logDebug('handleExportAction');
final postId = await tokenService.readValue('empHrId');
final preId = await tokenService.readValue('enrollmentEmpPrimaryId');
var activity = "export_empdata";
var activityPre = "export_preempdata";
dynamic response;
logDebug('postId - $postId');
logDebug('preId - $preId');
logDebug('activity - $activity');
try {
logDebug('10');
if (localTokenType == 'pre') {
response = await apiService.getPreLogHrActivity(
postId!, preId!, localToken!, activityPre);
} else if (localTokenType == 'post') {
response = await apiService.getPostLogHrActivity(
postId!, preId!, localToken!, activity);
}
if (response['status'] == 'success') {
logDebug('Request success');
} else {
// ToastHelper.showWarningToast(
// context, 'Request failed with status: ${response.statusCode}');
logDebug('Request failed with status: ${response['code']}');
}
} catch (e) {
logDebug('Exception occurred: $e');
}
}
dynamic _parseNumericId(dynamic value) {
if (value == null) return value;
return int.tryParse(value.toString()) ?? value;
}
List<String> _getInceptionExportStatus() {
switch (_preStatusFilter) {
case 'draft':
return ['draft'];
case 'enrolled':
return ['enrolled'];
default:
return ['enrolled'];
}
}
({String empCode, String empName}) _getInceptionSearchParams() {
final query = searchController.text.trim();
if (query.isEmpty) {
return (empCode: '', empName: '');
}
final lowerQuery = query.toLowerCase();
final codeMatch = originalData.any(
(row) =>
row['emp_code']?.toString().toLowerCase().contains(lowerQuery) ??
false,
);
final nameMatch = originalData.any(
(row) =>
row['name']?.toString().toLowerCase().contains(lowerQuery) ?? false,
);
if (codeMatch && !nameMatch) {
return (empCode: query, empName: '');
}
if (nameMatch && !codeMatch) {
return (empCode: '', empName: query);
}
return (empCode: query, empName: query);
}
Future<void> downloadInceptionExport() async {
try {
final clientId = await tokenService.readValue('enrollmentClient_id') ??
localClientId ??
widget.ClientId;
final branchId =
await tokenService.readValue('enrollmentEmpClientBranchId') ??
localClientBranchId ??
widget.clientBranchId;
final policyId = localClientPolicyId ?? widget.ClientPoliyId;
final searchParams = _getInceptionSearchParams();
final status = _getInceptionExportStatus();
logDebug(
'downloadInceptionExport client=$clientId branch=$branchId '
'policies=$policyId status=$status '
'empCode=${searchParams.empCode} empName=${searchParams.empName}',
);
final response = await apiService.downloadInception(
client: _parseNumericId(clientId),
branch: _parseNumericId(branchId),
policies: _parseNumericId(policyId),
status: status,
empCode: searchParams.empCode,
empName: searchParams.empName,
token: localToken!,
);
if (!mounted) return;
if (response['status'] == true) {
final downloadUrl = response['data']?['downloadUrl'];
if (downloadUrl != null && downloadUrl.toString().isNotEmpty) {
await _launchURL(downloadUrl.toString());
ToastHelper.showSuccessToast(
context,
response['message']?.toString() ??
'Inception export generated successfully.',
);
} else {
ToastHelper.showErrorToast(context, 'Download URL not available');
}
} else {
ToastHelper.showErrorToast(
context,
response['message']?.toString() ??
'Failed to generate inception export.',
);
}
} catch (e) {
logDebug('downloadInceptionExport error: $e');
if (mounted) {
ToastHelper.showErrorToast(
context,
'Failed to generate inception export. Please try again.',
);
}
}
}
Widget _buildHeaderActionButton({
required String label,
required Color color,
required double width,
required VoidCallback? onPressed,
bool isLoading = false,
}) {
return SizedBox(
width: width,
height: 40,
child: ElevatedButton(
onPressed: onPressed,
style: ElevatedButton.styleFrom(
backgroundColor: color,
elevation: 0,
padding: EdgeInsets.zero,
disabledBackgroundColor: color.withValues(alpha: 0.6),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
child: isLoading
? const SizedBox(
width: 18,
height: 18,
child: CircularProgressIndicator(
strokeWidth: 2,
color: Colors.white,
),
)
: Text(
label,
maxLines: 1,
softWrap: false,
overflow: TextOverflow.ellipsis,
style: GoogleFonts.poppins(
fontSize: 13,
fontWeight: FontWeight.w700,
color: Colors.white,
),
),
),
);
}
Widget _buildImportButton() {
return SizedBox(
width: 116,
height: 40,
child: ElevatedButton(
onPressed: () async {
await tokenService.writeValue(
'upload_ClientId', widget.ClientId.toString());
await tokenService.writeValue(
'upload_policyTypeId', widget.policyTypeId.toString());
await tokenService.writeValue(
'upload_ClientPoliyId', widget.ClientPoliyId.toString());
await tokenService.writeValue(
'upload_clientBranchId', widget.clientBranchId.toString());
await tokenService.writeValue(
'upload_Token', widget.Token.toString());
await tokenService.writeValue(
'upload_TokenType', widget.TokenType.toString());
await tokenService.writeValue(
'upload_cardType', widget.cardType.toString());
await tokenService.writeValue(
'upload_cardPolicyNo', widget.cardPolicyNo.toString());
await tokenService.writeValue(
'upload_cardInsurer_name', widget.cardInsurer_name.toString());
await tokenService.writeValue(
'upload_cardPolicy_name', widget.cardPolicy_name.toString());
await tokenService.writeValue(
'upload_cardPolicy_ExpDate', widget.cardPolicy_ExpDate.toString());
await tokenService.writeValue(
'upload_total_premium', widget.total_premium.toString());
Navigator.push(
context,
MaterialPageRoute(
settings: localTokenType != "post"
? const RouteSettings(name: 'preFileUpload')
: const RouteSettings(name: 'postFileUpload'),
builder: (context) => localTokenType != "post"
? preFileUpload(
ClientId: widget.ClientId,
policyTypeId: widget.policyTypeId,
ClientPoliyId: widget.ClientPoliyId,
clientBranchId: widget.clientBranchId,
Token: widget.Token,
TokenType: widget.TokenType,
cardType: widget.cardType,
cardPolicyNo: widget.cardPolicyNo,
cardInsurer_name: widget.cardInsurer_name,
cardPolicy_name: widget.cardPolicy_name,
cardPolicy_ExpDate: widget.cardPolicy_ExpDate,
total_premium: widget.total_premium,
)
: postFileUpload(
ClientId: widget.ClientId,
policyTypeId: widget.policyTypeId,
ClientPoliyId: widget.ClientPoliyId,
clientBranchId: widget.clientBranchId,
Token: widget.Token,
TokenType: widget.TokenType,
cardType: widget.cardType,
cardPolicyNo: widget.cardPolicyNo,
cardInsurer_name: widget.cardInsurer_name,
cardPolicy_name: widget.cardPolicy_name,
cardPolicy_ExpDate: widget.cardPolicy_ExpDate,
total_premium: widget.total_premium,
allocgType: 'EB',
),
),
);
},
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFFE26728),
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
child: Text(
'Import',
style: GoogleFonts.poppins(
fontSize: 15,
fontWeight: FontWeight.w700,
color: Colors.white,
letterSpacing: 0.5,
),
),
),
);
}
Widget _buildExportButton() {
final buttonStyle = ElevatedButton.styleFrom(
backgroundColor: const Color(0xFFE26728),
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
);
final textStyle = GoogleFonts.poppins(
fontSize: 15,
fontWeight: FontWeight.w700,
color: Colors.white,
letterSpacing: 0.5,
);
if (localTokenType == 'pre') {
return SizedBox(
width: 116,
height: 40,
child: PopupMenuButton<String>(
offset: const Offset(0, 40),
onSelected: (value) {
if (value == 'csv') {
downloadEmployeeListExcel();
} else if (value == 'inception') {
downloadInceptionExport();
}
},
itemBuilder: (context) => [
PopupMenuItem(
value: 'csv',
child: Text(
'Export Excel',
style: GoogleFonts.poppins(fontSize: 13),
),
),
PopupMenuItem(
value: 'inception',
child: Text(
'Inception Export',
style: GoogleFonts.poppins(fontSize: 13),
),
),
],
child: IgnorePointer(
child: ElevatedButton(
onPressed: () {},
style: buttonStyle,
child: Text(
_isExportingExcel ? 'Exporting…' : 'Export',
style: textStyle,
),
),
),
),
);
}
return SizedBox(
width: 116,
height: 40,
child: ElevatedButton(
onPressed: _isExportingExcel ? null : downloadEmployeeListExcel,
style: buttonStyle,
child: Text(
_isExportingExcel ? 'Exporting…' : 'Export',
style: textStyle,
),
),
);
}
String _capitalize(String? value) {
if (value == null || value.isEmpty) return '';
return value[0].toUpperCase() + value.substring(1).toLowerCase();
}
String _getStatusDisplayLabel(String? status) {
final normalized = status?.toLowerCase().trim() ?? '';
if (localTokenType == 'pre' && _isEnrolledStatus(normalized)) {
return 'Submitted';
}
return _capitalize(status);
}
Future<void> _openNetworkHospitalListDialog() async {
setState(() => _isLoadingHospitalList = true);
try {
final clientId = await tokenService.readValue('empClientId');
final token = localToken ??
widget.Token ??
tokenService.getCurrentToken() ??
'';
if (clientId == null ||
clientId.toString().isEmpty ||
token.toString().isEmpty) {
if (mounted) {
ToastHelper.showErrorToast(context, 'Client details not found');
}
return;
}
final response = await apiService.getClientRMApi(
clientId.toString(),
token.toString(),
);
if (!mounted) return;
if (response['status'] != 'success' || response['data'] is! Map) {
ToastHelper.showErrorToast(
context,
response['message']?.toString() ??
'Failed to load network hospital list',
);
return;
}
final data = Map<String, dynamic>.from(response['data'] as Map);
final hospitalLinks = _parseHospitalLinks(data['tpa_network_hospitals']);
await showDialog<void>(
context: context,
builder: (dialogContext) => _NetworkHospitalListDialog(
hospitalLinks: hospitalLinks,
),
);
} catch (e) {
logDebug('Network hospital list failed: $e');
if (mounted) {
ToastHelper.showErrorToast(
context,
'Failed to load network hospital list',
);
}
} finally {
if (mounted) setState(() => _isLoadingHospitalList = false);
}
}
Map<String, String> _parseHospitalLinks(dynamic raw) {
if (raw is! Map) return {};
final parsed = <String, String>{};
raw.forEach((key, value) {
final name = key.toString().trim();
final url = value?.toString().trim() ?? '';
if (name.isNotEmpty && url.isNotEmpty) {
parsed[name] = url;
}
});
return parsed;
}
Future<void> _openPolicyTermsDialog() async {
setState(() => _isLoadingPolicyTerms = true);
try {
final clientId = localClientId ?? widget.ClientId;
final branchId = localClientBranchId ?? widget.clientBranchId;
final clientPolicyId = localClientPolicyId ?? widget.ClientPoliyId;
final token = localToken ?? widget.Token;
final hrId = await tokenService.readValue('empHrId');
if (hrId == null || hrId.toString().isEmpty) {
if (mounted) {
ToastHelper.showErrorToast(context, 'HR ID not found');
}
return;
}
final response = await apiService.getActiveCashDepositDetailsToApi(
clientId,
branchId,
hrId.toString(),
token,
1,
);
if (!mounted) return;
if (response['status'] != 'success') {
ToastHelper.showErrorToast(
context,
response['message']?.toString() ?? 'Failed to load policy terms',
);
return;
}
final policies = List<Map<String, dynamic>>.from(response['data'] ?? []);
Map<String, dynamic>? matchedPolicy;
for (final policy in policies) {
if (policy['client_policy_id'].toString() == clientPolicyId) {
matchedPolicy = policy;
break;
}
}
if (matchedPolicy == null) {
ToastHelper.showErrorToast(context, 'Policy not found');
return;
}
final rawTerms =
matchedPolicy['policy_terms'] ?? matchedPolicy['Policy_Terms'];
final terms = <String, String>{};
if (rawTerms is Map) {
rawTerms.forEach((key, value) {
if (value != null && value.toString().trim().isNotEmpty) {
terms[key.toString()] = value.toString();
}
});
}
if (terms.isEmpty) {
ToastHelper.showWarningToast(context, 'No policy terms available');
return;
}
await showDialog<void>(
context: context,
builder: (dialogContext) => _PolicyTermsDialog(terms: terms),
);
} catch (e) {
logDebug('Policy terms load failed: $e');
if (mounted) {
ToastHelper.showErrorToast(context, 'Failed to load policy terms');
}
} finally {
if (mounted) {
setState(() => _isLoadingPolicyTerms = false);
}
}
}
Future<void> _openReminderDialog() async {
final clientId = localClientId ?? widget.ClientId;
final clientPolicyId = localClientPolicyId ?? widget.ClientPoliyId;
final clientBranchId = localClientBranchId ?? widget.clientBranchId;
final token = localToken ?? widget.Token;
await showDialog<void>(
context: context,
barrierDismissible: false,
builder: (dialogContext) {
return _ReminderHubDialog(
clientId: clientId,
clientPolicyId: clientPolicyId,
clientBranchId: clientBranchId,
token: token,
apiService: apiService,
defaultSubject: _defaultReminderSubject(),
defaultHtmlBody: _defaultReminderHtmlBody(),
onSend: (subject, htmlBody) => _sendEnrollmentReminder(
emailSubject: subject,
emailBody: htmlBody,
),
);
},
);
}
String _defaultReminderSubject() {
final policyName = localCardPolicyName ?? widget.cardPolicy_name;
return 'Reminder: Complete Your Enrollment - $policyName';
}
String _defaultReminderHtmlBody() {
return kDefaultEnrollmentReminderHtmlBody;
}
Future<void> _sendEnrollmentReminder({
required String emailSubject,
required String emailBody,
}) async {
setState(() => _isSendingReminder = true);
try {
final hrId = await tokenService.readValue('enrollmentHrId');
final response = await apiService.sendReminderMailApi(
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();
logDebug('10 $emp_policy_ids');
empHrId = await tokenService.readValue('empHrId');
final response = await apiService.getEcardBulkDownloadApi(
'', empHrId, emp_policy_ids, localToken!);
if (response['status'] == true) {
logDebug('Request success');
_showBulkDownloadSuccessPopup(response['message']);
} else {
ToastHelper.showErrorToast(context, response['message']);
logDebug('Request failed with status: ${response['code']}');
}
} catch (e) {
logDebug('Exception occurred: $e');
}
}
void _showBulkDownloadSuccessPopup(String message) {
showDialog(
context: context,
barrierDismissible: false,
builder: (context) {
return AlertDialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(
Icons.check_circle,
color: Color(0xFF009195),
size: 60,
),
const SizedBox(height: 16),
Text(
message,
textAlign: TextAlign.center,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
),
),
const SizedBox(height: 24),
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: () => Navigator.pop(context),
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF009195),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
child: Text(
'OK',
style: GoogleFonts.poppins(color: Colors.white),
),
),
),
],
),
);
},
);
}
@override
void dispose() {
_searchDebounce?.cancel();
searchController.dispose();
_tabController.dispose();
super.dispose();
}
@override
void _resetToLanding() {
_searchDebounce?.cancel();
searchController.clear();
setState(() {
_preStatusFilter = null;
_hasSearched = false;
_isSearching = false;
isLoading = false;
originalData = [];
filteredData = [];
getCDPolicies = [];
_currentPage = 1;
});
}
Widget build(BuildContext context) {
return BaseLayout(
child: _buildContent(context),
);
}
Widget _buildContent(BuildContext context) {
final showResults = _hasSearched || _isSearching;
return Scaffold(
backgroundColor: Colors.white,
body: SafeArea(
child: AnimatedSwitcher(
duration: const Duration(milliseconds: 280),
switchInCurve: Curves.easeOutCubic,
switchOutCurve: Curves.easeInCubic,
child: showResults
? KeyedSubtree(
key: const ValueKey('results'),
child: _buildResultsWorkspace(),
)
: KeyedSubtree(
key: const ValueKey('landing'),
child: _buildGoogleLandingView(),
),
),
),
);
}
String get _policyTitleLine =>
'${localCardType ?? ''} - ${localCardPolicyNo ?? ''}'.trim();
String get _policySubtitleLine => localTokenType == 'pre'
? '${localCardPolicyName ?? ''} (${localCardPolicyExpDate ?? ''})'
: '${localCardInsurerName ?? ''} - ${localCardPolicyName ?? ''} (${localCardPolicyExpDate ?? ''})';
/// First-entry Google-style home: title → search → action icons → filter icons.
Widget _buildGoogleLandingView() {
return Stack(
children: [
Positioned(
top: 12,
left: 12,
child: IconButton(
tooltip: 'Previous Page',
onPressed: () async {
await clearPolicyStorage();
if (!mounted) return;
Navigator.push(
context,
MaterialPageRoute(builder: (context) => policies()),
);
},
icon: const Icon(
Icons.arrow_back_ios_new_rounded,
size: 18,
color: Color(0xFF0F172A),
),
),
),
Center(
child: SingleChildScrollView(
padding: const EdgeInsets.fromLTRB(24, 48, 24, 32),
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 720),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
_policyTitleLine,
textAlign: TextAlign.center,
style: GoogleFonts.poppins(
color: const Color(0xFF0F172A),
fontSize: 34,
fontWeight: FontWeight.w700,
height: 1.15,
),
),
const SizedBox(height: 8),
Text(
_policySubtitleLine,
textAlign: TextAlign.center,
style: GoogleFonts.poppins(
color: const Color(0xFF64748B),
fontSize: 14,
fontWeight: FontWeight.w400,
),
),
const SizedBox(height: 36),
_buildLandingSearchPill(),
const SizedBox(height: 28),
_buildLandingShortcutRow(
items: _landingActionShortcuts(),
),
const SizedBox(height: 22),
if (_isLoadingCards)
const Padding(
padding: EdgeInsets.only(top: 12),
child: CircularProgressIndicator(
color: Color(0xFF009195),
),
)
else if (_filterChips.isNotEmpty)
_buildLandingFilterMetricsRow(),
],
),
),
),
),
],
);
}
Widget _buildLandingSearchPill() {
return Container(
height: 54,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(999),
border: Border.all(color: const Color(0xFFE2E8F0)),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.06),
blurRadius: 16,
offset: const Offset(0, 6),
),
],
),
child: TextField(
controller: searchController,
onChanged: search,
onSubmitted: (_) {
_searchDebounce?.cancel();
_searchEmployees();
},
textInputAction: TextInputAction.search,
style: GoogleFonts.poppins(
fontSize: 15,
fontWeight: FontWeight.w500,
color: const Color(0xFF0F172A),
),
decoration: InputDecoration(
hintText: 'Search by name, emp code, mobile, email…',
hintStyle: GoogleFonts.poppins(
fontSize: 14,
color: const Color(0xFF94A3B8),
),
prefixIcon: const Icon(
Icons.search_rounded,
color: Color(0xFF64748B),
),
suffixIcon: searchController.text.isNotEmpty
? IconButton(
tooltip: 'Clear',
onPressed: () {
searchController.clear();
setState(() {});
},
icon: const Icon(Icons.close_rounded, size: 18),
)
: null,
border: InputBorder.none,
contentPadding:
const EdgeInsets.symmetric(horizontal: 8, vertical: 16),
),
),
);
}
List<({String label, IconData icon, Color color, VoidCallback? onTap})>
_landingActionShortcuts() {
return [
if (localTokenType == 'pre') ...[
(
label: 'Reminder',
icon: Icons.notifications_active_outlined,
color: const Color(0xFF009195),
onTap: _isSendingReminder ? null : _openReminderDialog,
),
],
if (localTokenType == 'post') ...[
(
label: 'Policy Terms',
icon: Icons.description_outlined,
color: const Color(0xFF009195),
onTap: _isLoadingPolicyTerms ? null : _openPolicyTermsDialog,
),
(
label: 'Hospitals',
icon: Icons.local_hospital_outlined,
color: const Color(0xFF009195),
onTap: _isLoadingHospitalList ? null : _openNetworkHospitalListDialog,
),
],
(
label: 'Import',
icon: Icons.file_download_outlined,
color: const Color(0xFFE26728),
onTap: _openImportFlow,
),
(
label: 'Export',
icon: Icons.file_upload_outlined,
color: const Color(0xFFE26728),
onTap: localTokenType == 'pre'
? null
: (_isExportingExcel ? null : downloadEmployeeListExcel),
),
];
}
List<({String key, String label, int count, IconData icon, Color color})>
_landingFilterMetrics() {
return _filterChips.map((filter) {
final key = filter['key'].toString();
return (
key: key,
label: (filter['label'] ?? _chipLabelForKey(key)).toString(),
count: int.tryParse('${filter['count']}') ?? 0,
icon: _landingFilterIcon(key),
color: _landingFilterColor(key),
);
}).toList();
}
/// Filter metrics with rounded-square icons and stylish dividers between.
Widget _buildLandingFilterMetricsRow() {
final items = _landingFilterMetrics();
if (items.isEmpty) return const SizedBox.shrink();
return Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 12),
decoration: BoxDecoration(
color: const Color(0xFFF8FAFC),
borderRadius: BorderRadius.circular(22),
border: Border.all(color: const Color(0xFFE2E8F0)),
),
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
for (var i = 0; i < items.length; i++) ...[
if (i > 0) _buildLandingFilterDivider(),
_buildLandingFilterMetricTile(items[i]),
],
],
),
),
);
}
Widget _buildLandingFilterDivider() {
return Container(
margin: const EdgeInsets.symmetric(horizontal: 6),
width: 1.5,
height: 52,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(999),
gradient: const LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Color(0x00CBD5E1),
Color(0xFF94A3B8),
Color(0x00CBD5E1),
],
),
),
);
}
Widget _buildLandingFilterMetricTile(
({String key, String label, int count, IconData icon, Color color}) item,
) {
return Material(
color: Colors.transparent,
child: InkWell(
onTap: () => _onPreStatusFilterTap(item.key),
borderRadius: BorderRadius.circular(16),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 52,
height: 52,
decoration: BoxDecoration(
color: item.color.withValues(alpha: 0.12),
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: item.color.withValues(alpha: 0.22),
),
),
child: Icon(item.icon, color: item.color, size: 24),
),
const SizedBox(height: 8),
Text(
'${item.count}',
style: GoogleFonts.poppins(
fontSize: 15,
fontWeight: FontWeight.w700,
height: 1.1,
color: const Color(0xFF0F172A),
),
),
const SizedBox(height: 2),
Text(
item.label,
textAlign: TextAlign.center,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: GoogleFonts.poppins(
fontSize: 11,
fontWeight: FontWeight.w500,
color: const Color(0xFF64748B),
),
),
],
),
),
),
);
}
IconData _landingFilterIcon(String key) {
switch (key.toLowerCase()) {
case 'emp_count':
return Icons.groups_rounded;
case 'submitted':
return Icons.verified_rounded;
case 'logged_in':
return Icons.login_rounded;
case 'not_logged_in':
return Icons.person_off_rounded;
case 'draft':
return Icons.edit_note_rounded;
default:
return Icons.filter_alt_rounded;
}
}
Color _landingFilterColor(String key) {
switch (key.toLowerCase()) {
case 'emp_count':
return const Color(0xFF3B82F6);
case 'submitted':
return const Color(0xFF16A34A);
case 'logged_in':
return const Color(0xFF009195);
case 'not_logged_in':
return const Color(0xFF6366F1);
case 'draft':
return const Color(0xFFE26728);
default:
return const Color(0xFF009195);
}
}
Widget _buildLandingShortcutRow({
required List<
({String label, IconData icon, Color color, VoidCallback? onTap})>
items,
}) {
return Wrap(
alignment: WrapAlignment.center,
spacing: 18,
runSpacing: 16,
children: [
for (final item in items)
_buildLandingShortcutTile(
label: item.label,
icon: item.icon,
color: item.color,
onTap: item.onTap,
isExportMenu: item.label == 'Export' && localTokenType == 'pre',
),
],
);
}
Widget _buildLandingShortcutTile({
required String label,
required IconData icon,
required Color color,
required VoidCallback? onTap,
bool isExportMenu = false,
}) {
final tile = SizedBox(
width: 84,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 56,
height: 56,
decoration: BoxDecoration(
color: color.withValues(alpha: 0.1),
shape: BoxShape.circle,
border: Border.all(color: color.withValues(alpha: 0.18)),
),
child: Icon(icon, color: color, size: 24),
),
const SizedBox(height: 8),
Text(
label,
textAlign: TextAlign.center,
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: GoogleFonts.poppins(
fontSize: 11,
fontWeight: FontWeight.w500,
height: 1.2,
color: const Color(0xFF334155),
),
),
],
),
);
if (isExportMenu) {
return PopupMenuButton<String>(
tooltip: 'Export',
offset: const Offset(0, 8),
onSelected: (value) {
if (value == 'csv') {
downloadEmployeeListExcel();
} else if (value == 'inception') {
downloadInceptionExport();
}
},
itemBuilder: (context) => [
PopupMenuItem(
value: 'csv',
child: Text('Export Excel', style: GoogleFonts.poppins(fontSize: 13)),
),
PopupMenuItem(
value: 'inception',
child: Text(
'Inception Export',
style: GoogleFonts.poppins(fontSize: 13),
),
),
],
child: tile,
);
}
return Material(
color: Colors.transparent,
child: InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(16),
child: tile,
),
);
}
Future<void> _openImportFlow() async {
await tokenService.writeValue(
'upload_ClientId', widget.ClientId.toString());
await tokenService.writeValue(
'upload_policyTypeId', widget.policyTypeId.toString());
await tokenService.writeValue(
'upload_ClientPoliyId', widget.ClientPoliyId.toString());
await tokenService.writeValue(
'upload_clientBranchId', widget.clientBranchId.toString());
await tokenService.writeValue('upload_Token', widget.Token.toString());
await tokenService.writeValue(
'upload_TokenType', widget.TokenType.toString());
await tokenService.writeValue(
'upload_cardType', widget.cardType.toString());
await tokenService.writeValue(
'upload_cardPolicyNo', widget.cardPolicyNo.toString());
await tokenService.writeValue(
'upload_cardInsurer_name', widget.cardInsurer_name.toString());
await tokenService.writeValue(
'upload_cardPolicy_name', widget.cardPolicy_name.toString());
await tokenService.writeValue(
'upload_cardPolicy_ExpDate', widget.cardPolicy_ExpDate.toString());
await tokenService.writeValue(
'upload_total_premium', widget.total_premium.toString());
if (!mounted) return;
Navigator.push(
context,
MaterialPageRoute(
settings: localTokenType != "post"
? const RouteSettings(name: 'preFileUpload')
: const RouteSettings(name: 'postFileUpload'),
builder: (context) => localTokenType != "post"
? preFileUpload(
ClientId: widget.ClientId,
policyTypeId: widget.policyTypeId,
ClientPoliyId: widget.ClientPoliyId,
clientBranchId: widget.clientBranchId,
Token: widget.Token,
TokenType: widget.TokenType,
cardType: widget.cardType,
cardPolicyNo: widget.cardPolicyNo,
cardInsurer_name: widget.cardInsurer_name,
cardPolicy_name: widget.cardPolicy_name,
cardPolicy_ExpDate: widget.cardPolicy_ExpDate,
total_premium: widget.total_premium,
)
: postFileUpload(
ClientId: widget.ClientId,
policyTypeId: widget.policyTypeId,
ClientPoliyId: widget.ClientPoliyId,
clientBranchId: widget.clientBranchId,
Token: widget.Token,
TokenType: widget.TokenType,
cardType: widget.cardType,
cardPolicyNo: widget.cardPolicyNo,
cardInsurer_name: widget.cardInsurer_name,
cardPolicy_name: widget.cardPolicy_name,
cardPolicy_ExpDate: widget.cardPolicy_ExpDate,
total_premium: widget.total_premium,
allocgType: 'EB',
),
),
);
}
/// Current design: header actions + find members + table.
Widget _buildResultsWorkspace() {
return Column(
children: [
Padding(
padding: const EdgeInsets.fromLTRB(16, 16, 16, 8),
child: LayoutBuilder(
builder: (context, constraints) {
final isCompact = constraints.maxWidth < 900;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
IconButton(
tooltip: 'Back to home',
onPressed: _resetToLanding,
icon: const Icon(
Icons.arrow_back_ios_new_rounded,
size: 18,
color: Color(0xFF0F172A),
),
padding: EdgeInsets.zero,
constraints: const BoxConstraints(),
),
const SizedBox(width: 10),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
_policyTitleLine,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: GoogleFonts.poppins(
color: const Color(0xFF0F172A),
fontSize: isCompact ? 15 : 17,
fontWeight: FontWeight.w700,
),
),
const SizedBox(height: 2),
Text(
_policySubtitleLine,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: GoogleFonts.poppins(
color: const Color(0xFF64748B),
fontSize: 12,
fontWeight: FontWeight.w400,
),
),
],
),
),
const SizedBox(width: 12),
Flexible(
child: Align(
alignment: Alignment.centerRight,
child: Wrap(
spacing: 8,
runSpacing: 8,
alignment: WrapAlignment.end,
crossAxisAlignment: WrapCrossAlignment.center,
children: [
if (localIsEcardBulkDownload == 1)
SizedBox(
width: 40,
height: 40,
child: ElevatedButton(
onPressed: getEcardBulkDownload,
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFFE26728),
elevation: 0,
padding: EdgeInsets.zero,
alignment: Alignment.center,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
child: const Icon(
Icons.credit_card,
size: 18,
color: Colors.white,
),
),
),
if (localTokenType == 'pre') ...[
_buildHeaderActionButton(
label: 'Reminder',
color: const Color(0xFF009195),
width: isCompact ? 120 : 130,
onPressed: _isSendingReminder
? null
: _openReminderDialog,
isLoading: _isSendingReminder,
),
],
if (localTokenType == 'post') ...[
_buildHeaderActionButton(
label: 'Policy Terms',
color: const Color(0xFF009195),
width: 130,
onPressed: _isLoadingPolicyTerms
? null
: _openPolicyTermsDialog,
isLoading: _isLoadingPolicyTerms,
),
_buildHeaderActionButton(
label: 'Network Hospital List',
color: const Color(0xFF009195),
width: 190,
onPressed: _isLoadingHospitalList
? null
: _openNetworkHospitalListDialog,
isLoading: _isLoadingHospitalList,
),
],
_buildImportButton(),
_buildExportButton(),
],
),
),
),
],
),
],
);
},
),
),
const SizedBox(height: 8),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: LayoutBuilder(
builder: (context, constraints) {
return _buildSearchAndFiltersSection(constraints);
},
),
),
const SizedBox(height: 12),
Expanded(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: AnimatedSwitcher(
duration: const Duration(milliseconds: 320),
switchInCurve: Curves.easeOutCubic,
switchOutCurve: Curves.easeInCubic,
child: (_isSearching || (isLoading && _hasSearched))
? Center(
key: const ValueKey('loader'),
child: Image.asset(
height: 60,
width: 60,
'assets/nhance-loader.gif',
),
)
: filteredData.isEmpty
? Center(
key: const ValueKey('empty'),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
'No matching members found',
style: GoogleFonts.poppins(
fontSize: 14,
color: const Color(0xFF64748B),
),
),
const SizedBox(height: 8),
TextButton(
onPressed: _resetToLanding,
child: Text(
'Back to home',
style: GoogleFonts.poppins(
fontWeight: FontWeight.w600,
color: const Color(0xFF009195),
),
),
),
],
),
)
: LayoutBuilder(
key: const ValueKey('table'),
builder: (context, constraints) {
return Align(
alignment: Alignment.topCenter,
child: ConstrainedBox(
constraints: BoxConstraints(
maxHeight: constraints.maxHeight,
minWidth: constraints.maxWidth,
),
child: DecoratedBox(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: const Color(0xFFE2E8F0),
),
boxShadow: [
BoxShadow(
color: Colors.black
.withValues(alpha: 0.04),
blurRadius: 16,
offset: const Offset(0, 6),
),
],
),
child: ClipRRect(
borderRadius: BorderRadius.circular(16),
child: _buildCDDataTable(context),
),
),
),
);
},
),
),
),
),
const SizedBox(height: 24),
],
);
}
// Widget _buildContent(BuildContext context) {
// return Container(
// // padding: EdgeInsets.only(top: 30, bottom: 200, left: 50, right: 50),
// child: Column(
// children: [
// Row(
// crossAxisAlignment: CrossAxisAlignment.center,
// children: [
// /// 🔙 Back + Title (LEFT)
// Row(
// children: [
// IconButton(
// onPressed: () => {Navigator.pop(context)},
// icon: const Icon(
// Icons.arrow_back_ios,
// size: 18,
// color: Colors.black,
// ),
// padding: EdgeInsets.zero,
// constraints: const BoxConstraints(),
// ),
// const SizedBox(width: 6),
// Container(
// // color: Colors.redAccent.shade100,
// child: Column(
// crossAxisAlignment: CrossAxisAlignment.start,
// mainAxisAlignment: MainAxisAlignment.start,
// children: [
// Text(
// "${widget.cardType} - ${widget.cardPolicyNo} " ??
// '',
// style: GoogleFonts.poppins(
// color: Colors.black,
// fontSize: 14,
// fontWeight: FontWeight.w500,
// ),
// ),
// Text(
// localTokenType == 'pre'
// ? "${widget.cardPolicy_name} (${widget.cardPolicy_ExpDate})"
// : "${widget.cardInsurer_name} - ${widget.cardPolicy_name} (${widget.cardPolicy_ExpDate})",
// style: GoogleFonts.poppins(
// color: Colors.grey,
// fontSize: 12,
// fontWeight: FontWeight.w400,
// ),
// ),
// ],
// ),
// ),
// ],
// ),
//
// /// Push right content to end
// const Spacer(),
//
// /// 🔍 Search Box
// Container(
// width: 380,
// height: 37,
// decoration: BoxDecoration(
// color: const Color(0xFFF0F0F0),
// borderRadius: BorderRadius.circular(8),
// ),
// child: TextField(
// controller: searchController,
// onChanged: search,
// style: GoogleFonts.poppins(fontSize: 14),
// decoration: const InputDecoration(
// hintText: 'Search',
// prefixIcon: Icon(Icons.search, size: 18),
// border: InputBorder.none,
// contentPadding:
// EdgeInsets.symmetric(horizontal: 12, vertical: 8),
// ),
// ),
// ),
//
// if (localIsEcardBulkDownload == 1) ...[
// const SizedBox(width: 12),
// SizedBox(
// width: 40,
// height: 37,
// child: ElevatedButton(
// onPressed: () {
// getEcardBulkDownload();
// },
// style: ElevatedButton.styleFrom(
// backgroundColor: const Color(0xFFE26728),
// elevation: 0,
// padding: EdgeInsets.zero, // ✅ IMPORTANT
// alignment: Alignment.center, // ✅ FORCE CENTER
// shape: RoundedRectangleBorder(
// borderRadius: BorderRadius.circular(10),
// ),
// ),
// child: Icon(
// Icons.credit_card,
// size: 18,
// color: Colors.white,
// )),
// ),
// ],
//
// const SizedBox(width: 12),
//
// SizedBox(
// width: 116,
// height: 37,
// child: ElevatedButton(
// onPressed: () {
// Navigator.push(
// context,
// MaterialPageRoute(
// builder: (context) => localTokenType != "post"
// ? preFileUpload(
// ClientId:
// localClientId, // <-- from map
// policyTypeId: localPolicyTypeId,
// ClientPoliyId: localClientPolicyId,
// clientBranchId: widget.clientBranchId,
// Token: widget.Token,
// TokenType: localTokenType,
// cardType: widget.cardType,
// cardPolicyNo: widget.cardPolicyNo,
// cardInsurer_name:
// widget.cardInsurer_name,
// cardPolicy_name: widget.cardPolicy_name,
// cardPolicy_ExpDate:
// widget.cardPolicy_ExpDate,
// total_premium: widget.total_premium,
//
// // Token: widget.Token,
// // ClientId: localClientId,
// // ClientPolicyId : localClientPolicyId,
// // PolicyName: widget.cardPolicy_name,
// // PolicyNo: widget.cardPolicyNo,
// // ClientBranchId: widget.HrId,
// // PolicyType: widget.cardType,
// )
// : postFileUpload(
// ClientId:
// localClientId, // <-- from map
// policyTypeId: localPolicyTypeId,
// ClientPoliyId: localClientPolicyId,
// clientBranchId: widget.clientBranchId,
// Token: widget.Token,
// TokenType: localTokenType,
// cardType: widget.cardType,
// cardPolicyNo: widget.cardPolicyNo,
// cardInsurer_name:
// widget.cardInsurer_name,
// cardPolicy_name: widget.cardPolicy_name,
// cardPolicy_ExpDate:
// widget.cardPolicy_ExpDate,
// total_premium: widget.total_premium,
//
// // Token: widget.Token,
// // ClientId: localClientId,
// // ClientPolicyId : localClientPolicyId,
// // PolicyName: widget.cardPolicy_name,
// // PolicyNo: widget.cardPolicyNo,
// // ClientBranchId: widget.HrId,
// // PolicyType: widget.cardType,
// )),
// );
// },
// style: ElevatedButton.styleFrom(
// backgroundColor: const Color(0xFFE26728),
// elevation: 0,
// shape: RoundedRectangleBorder(
// borderRadius: BorderRadius.circular(10),
// ),
// ),
// child: Text(
// 'Import',
// style: GoogleFonts.poppins(
// fontSize: 16,
// fontWeight: FontWeight.w700,
// color: Colors.white,
// letterSpacing: 1,
// ),
// ),
// ),
// ),
// const SizedBox(width: 12),
//
// /// ⬇️ Export Button
// SizedBox(
// width: 116,
// height: 37,
// child: ElevatedButton(
// onPressed: () {
// exportToCsv(filteredData);
// },
// style: ElevatedButton.styleFrom(
// backgroundColor: const Color(0xFFE26728),
// elevation: 0,
// shape: RoundedRectangleBorder(
// borderRadius: BorderRadius.circular(10),
// ),
// ),
// child: Text(
// 'Export',
// style: GoogleFonts.poppins(
// fontSize: 16,
// fontWeight: FontWeight.w700,
// color: Colors.white,
// letterSpacing: 1,
// ),
// ),
// ),
// ),
// ],
// ),
// SizedBox(height: 20),
// if (localTokenType == "pre") ...[_buildStatusSummary()],
// if (localTokenType == "post") ...[
// Row(
// crossAxisAlignment: CrossAxisAlignment.start,
// children: [
// Container(
// padding: const EdgeInsets.symmetric(
// horizontal: 12, vertical: 8),
// decoration: BoxDecoration(
// color: Color(0xFFF9EBBD),
// borderRadius: BorderRadius.circular(6),
// ),
// child: Text(
// 'Premium - ₹${widget.total_premium ?? ''}*',
// style: GoogleFonts.poppins(
// fontSize: 12,
// fontWeight: FontWeight.w600,
// color: Color(0xFF009195),
// ),
// ),
// )
// ],
// ),
// ],
// SizedBox(height: 20),
// Container(
// // decoration: BoxDecoration(
// // color: Colors.white,
// // borderRadius:
// // BorderRadius.circular(10), // 👈 set your desired radius
// // ),
// // height: 400,
// // margin: const EdgeInsets.only(left: 40.0, right: 40.0),
// // padding: const EdgeInsets.all(16.0),
// child: Column(
// children: [
// Row(
// children: [
// Expanded(
// child: Container(
// child: SingleChildScrollView(
// scrollDirection: Axis.vertical,
// child: _buildCDDataTable(context),
// ),
// ),
// )
// ],
// ),
// ],
// ),
// ),
// Positioned(
// bottom: 12,
// right: 16,
// child: Text(
// '(* Premium may vary subject to claims)',
// textAlign: TextAlign.right,
// style: GoogleFonts.poppins(
// fontSize: 11,
// color: Colors.red,
// fontStyle: FontStyle.italic,
// ),
// ),
// ),
// ],
// ),
// );
// }
Map<String, int> getPreStatusCounts() {
// Prefer API chip counts; fall back to client counts from loaded rows.
if (_filterChips.isNotEmpty) {
return {
for (final chip in _filterChips)
chip['key'].toString():
int.tryParse('${chip['count']}') ?? 0,
};
}
int empCount = 0;
int enrolled = 0;
int notEnrolled = 0;
int loggedIn = 0;
int notLoggedIn = 0;
int draft = 0;
for (final item in originalData) {
final status = _getRowStatus(item);
final relationship =
item['relationship']?.toString().toLowerCase().trim() ?? '';
if (relationship == 'self') empCount++;
if (_isSelfRow(item) && _isEnrolledStatus(status)) {
enrolled++;
} else if (!_isEnrolledStatus(status)) {
notEnrolled++;
}
if (_isSelfRow(item)) {
if (_isLoggedInRow(item)) {
loggedIn++;
} else {
notLoggedIn++;
}
}
if (status == 'draft' && _isSelfRow(item)) draft++;
}
return {
'emp_count': empCount,
'enrolled': enrolled,
'not_enrolled': notEnrolled,
'logged_in': loggedIn,
'not_logged_in': notLoggedIn,
'draft': draft,
};
}
Color _getPreFilterChipColor(String filter) {
switch (filter.toLowerCase()) {
case 'emp_count':
return const Color(0xFFE2FBCB);
case 'submitted':
return const Color(0xFFBDF9D9);
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(0xFFE8F5F5);
}
}
Widget _buildModernSearchField({double? width}) {
final field = AnimatedContainer(
duration: const Duration(milliseconds: 220),
curve: Curves.easeOutCubic,
height: 48,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(14),
border: Border.all(
color: const Color(0xFF009195).withValues(alpha: 0.25),
),
boxShadow: [
BoxShadow(
color: const Color(0xFF009195).withValues(alpha: 0.08),
blurRadius: 16,
offset: const Offset(0, 6),
),
],
),
child: TextField(
controller: searchController,
onChanged: search,
onSubmitted: (_) {
_searchDebounce?.cancel();
_searchEmployees();
},
textInputAction: TextInputAction.search,
style: GoogleFonts.poppins(
fontSize: 14,
fontWeight: FontWeight.w500,
color: const Color(0xFF1E293B),
),
decoration: InputDecoration(
hintText: 'Search by name, emp code, mobile, email…',
hintStyle: GoogleFonts.poppins(
fontSize: 13,
color: const Color(0xFF94A3B8),
),
prefixIcon: const Icon(
Icons.search_rounded,
size: 22,
color: Color(0xFF009195),
),
suffixIcon: searchController.text.isNotEmpty
? IconButton(
tooltip: 'Clear',
onPressed: () {
searchController.clear();
search('');
setState(() {});
},
icon: const Icon(Icons.close_rounded, size: 18),
)
: (_isSearching
? const Padding(
padding: EdgeInsets.all(12),
child: SizedBox(
width: 18,
height: 18,
child: CircularProgressIndicator(
strokeWidth: 2,
color: Color(0xFF009195),
),
),
)
: null),
border: InputBorder.none,
contentPadding:
const EdgeInsets.symmetric(horizontal: 8, vertical: 14),
),
),
);
if (width != null) {
return SizedBox(width: width, child: field);
}
return field;
}
Widget _buildPreStatusFilterChip({
required String filterKey,
required String label,
required int count,
}) {
final selected = _preStatusFilter == filterKey;
final color = _getPreFilterChipColor(filterKey);
return AnimatedScale(
scale: selected ? 1.03 : 1.0,
duration: const Duration(milliseconds: 180),
curve: Curves.easeOutCubic,
child: Material(
color: Colors.transparent,
child: InkWell(
onTap: () => _onPreStatusFilterTap(filterKey),
borderRadius: BorderRadius.circular(12),
child: AnimatedContainer(
duration: const Duration(milliseconds: 200),
curve: Curves.easeOutCubic,
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
decoration: BoxDecoration(
color: color,
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: selected
? const Color(0xFF009195)
: Colors.white.withValues(alpha: 0.6),
width: selected ? 2 : 1,
),
boxShadow: selected
? [
BoxShadow(
color: const Color(0xFF009195).withValues(alpha: 0.22),
blurRadius: 10,
offset: const Offset(0, 4),
),
]
: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.04),
blurRadius: 6,
offset: const Offset(0, 2),
),
],
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(
label,
style: GoogleFonts.poppins(
fontSize: 12,
fontWeight: selected ? FontWeight.w700 : FontWeight.w600,
color: const Color(0xFF0F172A),
),
),
const SizedBox(width: 8),
Container(
padding:
const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.75),
borderRadius: BorderRadius.circular(999),
),
child: Text(
'$count',
style: GoogleFonts.poppins(
fontSize: 11,
fontWeight: FontWeight.w700,
color: const Color(0xFF009195),
),
),
),
],
),
),
),
),
);
}
Widget _buildStatusSummary() {
if (_filterChips.isEmpty && !_isLoadingCards) {
return const SizedBox.shrink();
}
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
if (_isLoadingCards)
const Padding(
padding: EdgeInsets.only(bottom: 12),
child: LinearProgressIndicator(
minHeight: 3,
color: Color(0xFF009195),
backgroundColor: Color(0xFFE0F2F1),
),
),
if (_filterChips.isNotEmpty)
Wrap(
spacing: 10,
runSpacing: 10,
crossAxisAlignment: WrapCrossAlignment.center,
children: [
..._filterChips.map((filter) {
final key = filter['key'].toString();
return _buildPreStatusFilterChip(
filterKey: key,
label: (filter['label'] ?? _chipLabelForKey(key)).toString(),
count: int.tryParse('${filter['count']}') ?? 0,
);
}),
Material(
color: Colors.transparent,
child: InkWell(
onTap:
_preStatusFilter == null ? null : _resetPreStatusFilter,
borderRadius: BorderRadius.circular(12),
child: AnimatedContainer(
duration: const Duration(milliseconds: 200),
padding: const EdgeInsets.symmetric(
horizontal: 14, vertical: 10),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: _preStatusFilter != null
? const Color(0xFF009195)
: const Color(0xFFE2E8F0),
),
),
child: Text(
'Reset',
style: GoogleFonts.poppins(
fontSize: 12,
fontWeight: FontWeight.w600,
color: _preStatusFilter != null
? const Color(0xFF009195)
: const Color(0xFF94A3B8),
),
),
),
),
),
],
),
],
);
}
Widget _buildSearchAndFiltersSection(BoxConstraints constraints) {
final isNarrow = constraints.maxWidth < 720;
return Container(
width: double.infinity,
padding: EdgeInsets.all(isNarrow ? 14 : 18),
decoration: BoxDecoration(
gradient: const LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
Color(0xFFF0FDFA),
Color(0xFFFFFFFF),
Color(0xFFFFF7ED),
],
),
borderRadius: BorderRadius.circular(18),
border: Border.all(color: const Color(0xFFE2E8F0)),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.04),
blurRadius: 18,
offset: const Offset(0, 8),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Find members',
style: GoogleFonts.poppins(
fontSize: 15,
fontWeight: FontWeight.w700,
color: const Color(0xFF0F172A),
),
),
const SizedBox(height: 4),
Text(
'Select a filter chip or search to load matching employee records.',
style: GoogleFonts.poppins(
fontSize: 12,
color: const Color(0xFF64748B),
),
),
const SizedBox(height: 14),
_buildStatusSummary(),
const SizedBox(height: 14),
_buildModernSearchField(),
],
),
);
}
Widget _buildSearchPromptEmptyState() {
return Center(
child: AnimatedOpacity(
opacity: 1,
duration: const Duration(milliseconds: 300),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 32),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
width: 72,
height: 72,
decoration: BoxDecoration(
color: const Color(0xFF009195).withValues(alpha: 0.1),
shape: BoxShape.circle,
),
child: const Icon(
Icons.manage_search_rounded,
size: 36,
color: Color(0xFF009195),
),
),
const SizedBox(height: 18),
Text(
'Select a filter or search to view members',
textAlign: TextAlign.center,
style: GoogleFonts.poppins(
fontSize: 16,
fontWeight: FontWeight.w700,
color: const Color(0xFF0F172A),
),
),
const SizedBox(height: 8),
Text(
'Use a filter chip or the search box above to load employee details.',
textAlign: TextAlign.center,
style: GoogleFonts.poppins(
fontSize: 13,
height: 1.45,
color: const Color(0xFF64748B),
),
),
],
),
),
),
);
}
Widget _buildCDDataTable(BuildContext context) {
final showUHID = localTokenType == 'post' &&
localPolicyTypeId != '6' &&
localPolicyTypeId != '7';
final showLoggedIn = localTokenType == 'pre';
final showAction =
localTokenType != 'pre' && (hasAnyEcardLink || hasModule);
return CustomScrollView(
shrinkWrap: true,
slivers: [
SliverPersistentHeader(
pinned: true,
delegate: _CDHeaderDelegate(
showUHID: showUHID,
uhidHeaderLabel: 'TPA ID',
showLoggedIn: showLoggedIn,
showAction: showAction,
),
),
SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) {
final item = _paginatedData[index];
return _buildCDRow(item, index);
},
childCount: _paginatedData.length,
),
),
SliverToBoxAdapter(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Divider(height: 1, thickness: 1, color: Color(0xFFE2E8F0)),
_buildPagination(context),
],
),
),
],
);
}
Widget _buildCDRow(Map<String, dynamic> item, [int index = 0]) {
return AnimatedContainer(
duration: const Duration(milliseconds: 180),
padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 16),
decoration: BoxDecoration(
color: index.isEven ? Colors.white : const Color(0xFFF8FFFE),
border: const Border(
bottom: BorderSide(color: Color(0xFFE2E8F0)),
),
),
child: Row(
children: [
/// NAME
Expanded(
flex: 3,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(item['name'] ?? '-', style: _dataBold),
Text(item['emp_code'] ?? '-', style: _dataSub),
],
),
),
/// TPA ID (post only)
if (localTokenType == 'post' &&
localPolicyTypeId != '6' &&
localPolicyTypeId != '7')
Expanded(
flex: 2,
child: Text(
item['tpa_id'] ?? '-',
style: _dataBold,
),
),
Expanded(
flex: 2,
child: Text(item['relationship'] ?? '-', style: _dataBold),
),
Expanded(
flex: 2,
child: Text(item['formatted_dob']?.replaceAll("/", "-") ?? '-',
style: _dataBold),
),
Expanded(
flex: 2,
child: Text(item['gender'] ?? '-', style: _dataBold),
),
Expanded(
flex: 2,
child: Text(item['mobile'] ?? '-', style: _dataBold),
),
Expanded(
flex: 5,
child: Text(item['email_corporate'] ?? '-', style: _dataBold),
),
if (localTokenType == "pre")
Expanded(
flex: 2,
child: _isSelfRow(item)
? Container(
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 4,
),
decoration: BoxDecoration(
color: _getLoggedInChipColor(item),
borderRadius: BorderRadius.circular(10),
),
child: Text(
_getLoggedInDisplayLabel(item),
textAlign: TextAlign.center,
style: _dataBold,
),
)
: Text('-', style: _dataBold),
),
if (localTokenType == "pre") const SizedBox(width: 8),
/// STATUS
Expanded(
flex: 2,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: getStatusColor(_getRowStatusRaw(item) ?? ''),
borderRadius: BorderRadius.circular(10),
),
child: Text(
_getStatusDisplayLabel(_getRowStatusRaw(item)),
textAlign: TextAlign.center,
style: _dataBold,
),
),
),
/// ACTION
if (localTokenType != "pre" && (hasAnyEcardLink || hasModule))
Expanded(
flex: 3,
child: Builder(
builder: (context) {
final isSelf = item['relationship'] == 'Self';
final hasEcard = item['ecard_download_link'] != null;
final showEcard = isSelf && hasEcard;
final showClaim = localTokenType == "post" && hasModule;
if (!showEcard && !showClaim) {
return SizedBox(); // No icon to show
}
return SizedBox(
height: 40,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
/// --- eCard Menu (Fixed Space) ---
SizedBox(
width: 40,
height: 40,
child: Visibility(
visible: showEcard,
maintainSize: true,
maintainAnimation: true,
maintainState: true,
child: PopupMenuButton<String>(
tooltip: 'Ecard',
offset: const Offset(0, 40),
onSelected: (value) {
switch (value) {
case 'download':
getEcardDownload(
item['emp_code'],
item['employee_id'],
item['client_policy_id'],
item['policy_no'],
);
break;
case 'email':
sendEcardViaEmail(
item['id'],
item['client_policy_id'],
);
break;
case 'copy':
copyEcardLink(
item['ecard_download_link']?.toString(),
);
break;
}
},
itemBuilder: (context) => [
PopupMenuItem(
value: 'download',
child: Text(
'Download Ecard',
style: GoogleFonts.poppins(fontSize: 13),
),
),
if (isSelf)
PopupMenuItem(
value: 'email',
child: Text(
'Send E-card via email',
style: GoogleFonts.poppins(fontSize: 13),
),
),
PopupMenuItem(
value: 'copy',
child: Text(
'Copy link',
style: GoogleFonts.poppins(fontSize: 13),
),
),
],
child: MouseRegion(
cursor: SystemMouseCursors.click,
child: Container(
decoration: BoxDecoration(
color: const Color(0xFFE6F5F6),
borderRadius: BorderRadius.circular(8),
),
padding: const EdgeInsets.all(6),
child: Image.asset(
'assets/credit_card.png',
fit: BoxFit.contain,
),
),
),
),
),
),
const SizedBox(width: 8),
/// --- Claim Button (Fixed Space) ---
SizedBox(
width: 40,
height: 40,
child: Visibility(
visible: showClaim,
maintainSize: true,
maintainAnimation: true,
maintainState: true,
child: Tooltip(
message: 'View Claims',
child: MouseRegion(
cursor: SystemMouseCursors.click,
child: GestureDetector(
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => ClaimsPolicies(
empCode: item['emp_code']!,
),
),
);
},
child: Container(
decoration: BoxDecoration(
color: const Color(0xFFE6F5F6),
borderRadius: BorderRadius.circular(8),
),
padding: const EdgeInsets.all(6),
child: Image.asset(
'assets/claim.png',
fit: BoxFit.contain,
),
),
),
),
),
),
),
],
),
);
},
),
),
],
),
);
}
// Widget _buildCDDataTable(BuildContext context) {
// if (_paginatedData.isNotEmpty) {
// hasAnyEcardLink = _paginatedData.any(
// (item) => item['ecard_download_link'] != null,
// );
//
// if (localTokenType == "post" && hasModule) {
// logDebug("paginatTtt - $_paginatedData");
//
// logDebug("📥 Any e-card link present: $hasAnyEcardLink");
// }
// }
//
// logDebug('widgetpolicyTypeId');
// logDebug(localPolicyTypeId);
//
// if (filteredData.isEmpty) {
// return SizedBox(
// // height: 50,
// child: Center(
// child: Text(
// 'No data is available for the selected policy',
// style: GoogleFonts.poppins(
// color: Colors.grey,
// fontWeight: FontWeight.w400,
// ),
// )),
// );
// }
//
// final currentPageIds = _paginatedData
// .map((e) => e['id']?.toString())
// .whereType<String>()
// .toList();
//
// return Container(
// child: Column(
// crossAxisAlignment: CrossAxisAlignment.start,
// children: [
// // Header row
// Container(
// decoration: BoxDecoration(
// color: Color(0xFFD7E9EB),
// borderRadius: BorderRadius.circular(6),
// ),
// padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16),
// child: Row(
// children: [
// if (localTokenType == 'post' &&
// localIsEcardBulkDownload == 1)
// SizedBox(
// width: 40,
// child: Checkbox(
// value: currentPageIds.isNotEmpty &&
// currentPageIds.every(selectedEmployeeIds.contains),
// onChanged: (checked) {
// setState(() {
// if (checked == true) {
// selectedEmployeeIds.addAll(currentPageIds);
// } else {
// selectedEmployeeIds.removeAll(currentPageIds);
// }
// });
// },
// ),
// ),
// Expanded(
// flex: 3,
// child: Text(
// 'Name',
// style: GoogleFonts.poppins(
// color: Colors.black,
// fontWeight: FontWeight.w600,
// ),
// ),
// ),
// if (localPolicyTypeId != '6' && localPolicyTypeId != '7')
// Expanded(
// flex: 2,
// child: Text(
// 'UHID',
// style: GoogleFonts.poppins(
// color: Colors.black,
// fontWeight: FontWeight.w600,
// ),
// ),
// ),
// Expanded(
// flex: 2,
// child: Text(
// 'Relationship',
// style: GoogleFonts.poppins(
// color: Colors.black,
// fontWeight: FontWeight.w600,
// ),
// ),
// ),
// Expanded(
// flex: 2,
// child: Text(
// 'Date Of Birth',
// style: GoogleFonts.poppins(
// color: Colors.black,
// fontWeight: FontWeight.w600,
// ),
// ),
// ),
// Expanded(
// flex: 2,
// child: Text(
// 'Gender',
// style: GoogleFonts.poppins(
// color: Colors.black,
// fontWeight: FontWeight.w600,
// ),
// ),
// ),
// Expanded(
// flex: 2,
// child: Text(
// 'Mobile',
// style: GoogleFonts.poppins(
// color: Colors.black,
// fontWeight: FontWeight.w600,
// ),
// ),
// ),
// Expanded(
// flex: 5,
// child: Text(
// 'Email',
// style: GoogleFonts.poppins(
// color: Colors.black,
// fontWeight: FontWeight.w600,
// ),
// ),
// ),
// Expanded(
// flex: 2,
// child: Text(
// 'Status',
// style: GoogleFonts.poppins(
// color: Colors.black,
// fontWeight: FontWeight.w600,
// ),
// ),
// ),
// if (localTokenType != "pre" && (hasAnyEcardLink || hasModule))
// Expanded(
// flex: 3,
// child: Text(
// 'Action',
// textAlign: TextAlign.center,
// style: GoogleFonts.poppins(
// color: Colors.black,
// fontWeight: FontWeight.w600,
// ),
// ),
// ),
// ],
// ),
// ),
//
// const SizedBox(height: 6),
//
// SingleChildScrollView(
// scrollDirection: Axis.vertical,
// child: Column(
// crossAxisAlignment: CrossAxisAlignment.start,
// children: _paginatedData.mapIndexed((index, item) {
// return Container(
// // margin: const EdgeInsets.only(bottom: 8),
// padding:
// const EdgeInsets.symmetric(vertical: 5, horizontal: 16),
// decoration: BoxDecoration(
// // color: Colors.white,
// border: Border(
// bottom: BorderSide(
// // color: Color(0xFFA1A1A1),
// color: Color(0xFFA9D9DE),
// width: 1,
// ),
// ),
// // color: index % 2 == 0 ? Color(0xFFE6FAFB) : Colors.white,
// // borderRadius: BorderRadius.circular(6),
// ),
// child: Row(
// children: [
// if (localTokenType == 'post' &&
// localIsEcardBulkDownload == 1)
// SizedBox(
// width: 40,
// child: Checkbox(
// value: selectedEmployeeIds
// .contains(item['id']?.toString()),
// onChanged: (checked) {
// setState(() {
// final id = item['id']?.toString();
// if (id == null) return;
//
// if (checked == true) {
// selectedEmployeeIds.add(id);
// } else {
// selectedEmployeeIds.remove(id);
// }
// });
// },
// ),
// ),
//
// Expanded(
// flex: 3,
// child: Column(
// mainAxisAlignment: MainAxisAlignment.start,
// crossAxisAlignment: CrossAxisAlignment.start,
// children: [
// Text(
// item['name'] ?? '-',
// style: GoogleFonts.poppins(
// color: Color(0xFF000000),
// fontWeight: FontWeight.w400,
// fontSize: 12),
// ),
// Text(
// item['emp_code'] ?? '-',
// style: GoogleFonts.poppins(
// color: Color(0xFF585757),
// fontWeight: FontWeight.w300,
// fontSize: 10),
// ),
// ],
// ),
// ),
// if (localPolicyTypeId != '6' &&
// localPolicyTypeId != '7')
// Expanded(
// flex: 2,
// child: Text(
// item['uhid'] ?? '-',
// style: GoogleFonts.poppins(
// color: Color(0xFF000000),
// fontWeight: FontWeight.w400,
// fontSize: 12),
// ),
// ),
// Expanded(
// flex: 2,
// child: Text(
// "${item['relationship'] ?? ''}",
// style: GoogleFonts.poppins(
// color: Color(0xFF000000),
// fontWeight: FontWeight.w400,
// fontSize: 12),
// ),
// ),
// Expanded(
// flex: 2,
// child: Text(
// "${item['formatted_dob'].replaceAll("/", "-") ?? ''}",
// style: GoogleFonts.poppins(
// color: Color(0xFF000000),
// fontWeight: FontWeight.w400,
// fontSize: 12),
// ),
// ),
// Expanded(
// flex: 2,
// child: Text(
// "${item['gender'] ?? ''}",
// style: GoogleFonts.poppins(
// color: Color(0xFF000000),
// fontWeight: FontWeight.w400,
// fontSize: 12),
// ),
// ),
// Expanded(
// flex: 2,
// child: Text(
// "${item['mobile'] ?? ''}",
// style: GoogleFonts.poppins(
// color: Color(0xFF000000),
// fontWeight: FontWeight.w400,
// fontSize: 12),
// ),
// ),
// Expanded(
// flex: 5,
// child: Text(
// "${item['email_corporate'] ?? ''}",
// style: GoogleFonts.poppins(
// color: Color(0xFF000000),
// fontWeight: FontWeight.w400,
// fontSize: 12),
// ),
// ),
// Expanded(
// flex: 2,
// child: Container(
// padding: const EdgeInsets.symmetric(
// horizontal: 4, vertical: 4),
// decoration: BoxDecoration(
// color: getStatusColor(item['status'] ?? ''),
// // color: (item['emp_is_active'] == "1")
// // ? Color(0xFF7BD9B6)
// // : Color(0xFFFFA6A6),
// borderRadius: BorderRadius.circular(10),
// ),
// child: Align(
// alignment: Alignment.center,
// child: Text(
// _capitalize(item['status']),
// style: GoogleFonts.poppins(
// color: Color(0xFF000000),
// fontWeight: FontWeight.w500,
// fontSize: 12,
// ),
// ),
// ),
// ),
// ),
//
// if (localTokenType != "pre" &&
// (hasAnyEcardLink || hasModule))
// Expanded(
// flex: 3,
// child: Builder(
// builder: (context) {
// final isSelf = item['relationship'] == 'Self';
// final hasEcard = item['ecard_download_link'] != null;
// final showEcard = isSelf && hasEcard;
// final showClaim = localTokenType == "post" && hasModule;
//
// if (!showEcard && !showClaim) {
// return SizedBox(); // No icon to show
// }
//
// return SizedBox(
// height: 40,
// child: Row(
// mainAxisAlignment: MainAxisAlignment.center,
// crossAxisAlignment: CrossAxisAlignment.center,
// children: [
//
// /// --- eCard Button (Fixed Space) ---
// SizedBox(
// width: 40,
// height: 40,
// child: Visibility(
// visible: showEcard,
// maintainSize: true,
// maintainAnimation: true,
// maintainState: true,
// child: Tooltip(
// message: 'Download e-Card',
// child: MouseRegion(
// cursor: SystemMouseCursors.click,
// child: GestureDetector(
// onTap: () {
// getEcardDownload(
// item['emp_code'],
// item['employee_id'],
// item['client_policy_id'],
// item['policy_no'],
// );
// },
// child: Container(
// decoration: BoxDecoration(
// color: const Color(0xFFE6F5F6),
// borderRadius: BorderRadius.circular(8),
// ),
// padding: const EdgeInsets.all(6),
// child: Image.asset(
// 'assets/credit_card.png',
// fit: BoxFit.contain,
// ),
// ),
// ),
// ),
// ),
// ),
// ),
//
// const SizedBox(width: 8),
//
// /// --- Claim Button (Fixed Space) ---
// SizedBox(
// width: 40,
// height: 40,
// child: Visibility(
// visible: showClaim,
// maintainSize: true,
// maintainAnimation: true,
// maintainState: true,
// child: Tooltip(
// message: 'View Claims',
// child: MouseRegion(
// cursor: SystemMouseCursors.click,
// child: GestureDetector(
// onTap: () {
// Navigator.push(
// context,
// MaterialPageRoute(
// builder: (context) => ClaimsPolicies(
// empCode: item['emp_code']!,
// ),
// ),
// );
// },
// child: Container(
// decoration: BoxDecoration(
// color: const Color(0xFFE6F5F6),
// borderRadius: BorderRadius.circular(8),
// ),
// padding: const EdgeInsets.all(6),
// child: Image.asset(
// 'assets/claim.png',
// fit: BoxFit.contain,
// ),
// ),
// ),
// ),
// ),
// ),
// ),
// ],
// ),
// );
// },
// ),
// ),
//
// // (hasAnyEcardLink || hasModule))
// // Expanded(
// // flex: 3,
// // child: Row(
// // mainAxisAlignment: MainAxisAlignment.center,
// // children: [
// // if (item['ecard_download_link'] != null)
// // GestureDetector(
// // onTap: () {
// // _launchURL(item['ecard_download_link']);
// // },
// // child: Container(
// // height: 35,
// // width: 35,
// // decoration: BoxDecoration(
// // color: Color(0xFFE6F5F6),
// // borderRadius: BorderRadius.circular(8),
// // ),
// // child: Padding(
// // padding: EdgeInsets.all(
// // 6), // You can adjust this value
// // child: Image.asset(
// // 'assets/credit_card.png',
// // fit: BoxFit.contain,
// // ),
// // ),
// // ),
// // ),
// // SizedBox(
// // width: 10,
// // ),
// // if (localTokenType == "post" && hasModule)
// // GestureDetector(
// // onTap: () {
// // setState(() {
// // logDebug(
// // "policytabdata - ${item['emp_code']!}");
// // Navigator.push(
// // context,
// // MaterialPageRoute(
// // builder: (context) => hrDashboard(
// // selectedIndex: 3,
// // empCodeFromHrPolicy:
// // item['emp_code']!,
// // isHrcode: 1,
// // ),
// // ),
// // );
// // });
// // },
// // child: Container(
// // height: 35,
// // width: 35,
// // decoration: BoxDecoration(
// // color: Color(0xFFE6F5F6),
// // borderRadius: BorderRadius.circular(8),
// // ),
// // child: Padding(
// // padding: EdgeInsets.all(
// // 6), // You can adjust this value
// // child: Image.asset(
// // 'assets/claim.png',
// // fit: BoxFit.contain,
// // ),
// // ),
// // ),
// // ),
// // ],
// // ),
// // ),
// ],
// ),
// );
// }).toList(),
// ),
// ),
// // ),
// _buildPagination(context)
// // Row(
// // mainAxisAlignment: MainAxisAlignment.end,
// // children: [
// // Padding(
// // padding: const EdgeInsets.symmetric(vertical: 12),
// // child: Row(
// // mainAxisAlignment: MainAxisAlignment.center,
// // children: [
// // DropdownButton<int>(
// // value: _rowsPerPage,
// // items: [5, 10, 15, 20, 50].map((int value) {
// // return DropdownMenuItem<int>(
// // value: value,
// // child: Text(
// // ' $value ',
// // style: GoogleFonts.poppins(fontSize: 15),
// // ),
// // );
// // }).toList(),
// // onChanged: (newValue) {
// // setState(() {
// // _rowsPerPage = newValue!;
// // _currentPage =
// // 1; // Reset to first page when rows per page changes
// // });
// // },
// // ),
// // IconButton(
// // onPressed: _currentPage > 1
// // ? () {
// // setState(() {
// // _currentPage--;
// // });
// // }
// // : null,
// // icon: Icon(Icons.chevron_left),
// // ),
// // for (int i = 1;
// // i <= (filteredData.length / _rowsPerPage).ceil();
// // i++)
// // Padding(
// // padding: const EdgeInsets.symmetric(horizontal: 4),
// // child: ElevatedButton(
// // style: ElevatedButton.styleFrom(
// // backgroundColor: _currentPage == i
// // ? Color(0xFF00A6A6)
// // : Colors.grey[300],
// // foregroundColor:
// // _currentPage == i ? Colors.white : Colors.black,
// // minimumSize: Size(36, 36),
// // padding: EdgeInsets.zero,
// // ),
// // onPressed: () {
// // setState(() {
// // _currentPage = i;
// // });
// // },
// // child: Text(i.toString()),
// // ),
// // ),
// // IconButton(
// // onPressed: _currentPage <
// // (filteredData.length / _rowsPerPage).ceil()
// // ? () {
// // setState(() {
// // _currentPage++;
// // });
// // }
// // : null,
// // icon: Icon(Icons.chevron_right),
// // ),
// // ],
// // ),
// // ),
// // ],
// // ),
// ],
// ),
// );
// }
Widget _buildPagination(BuildContext context) {
// 1. Calculate the range of entries being shown
final totalItems = filteredData.length;
final int startEntry =
totalItems == 0 ? 0 : ((_currentPage - 1) * _rowsPerPage) + 1;
int endEntry = _currentPage * _rowsPerPage;
if (endEntry > totalItems) endEntry = totalItems;
final totalPages = (filteredData.length / _rowsPerPage).ceil();
const visiblePageCount = 5;
List<int> getVisiblePages() {
if (totalPages <= visiblePageCount) {
return List.generate(totalPages, (i) => i + 1);
}
if (_currentPage <= 3) {
return [1, 2, 3, 4, 5];
}
if (_currentPage >= totalPages - 2) {
return [
totalPages - 4,
totalPages - 3,
totalPages - 2,
totalPages - 1,
totalPages
];
}
return [
_currentPage - 2,
_currentPage - 1,
_currentPage,
_currentPage + 1,
_currentPage + 2,
];
}
List<int> visiblePages = getVisiblePages();
return Padding(
padding: const EdgeInsets.fromLTRB(12, 6, 12, 6),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Flexible(
child: Text(
"Showing $startEntry to $endEntry of $totalItems entries",
style: GoogleFonts.poppins(
fontSize: 12,
color: const Color(0xFF585757),
fontWeight: FontWeight.w400,
),
overflow: TextOverflow.ellipsis,
),
),
Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(
'Rows',
style: GoogleFonts.poppins(
fontSize: 12,
color: const Color(0xFF64748B),
),
),
const SizedBox(width: 6),
DropdownButtonHideUnderline(
child: DropdownButton<int>(
value: _rowsPerPage,
isDense: true,
focusColor: Colors.transparent,
items: [5, 10, 15, 20, 50].map((int value) {
return DropdownMenuItem<int>(
value: value,
child: Text(
'$value',
style: GoogleFonts.poppins(fontSize: 13),
),
);
}).toList(),
onChanged: (newValue) {
setState(() {
_rowsPerPage = newValue!;
_currentPage = 1;
});
},
),
),
IconButton(
tooltip: 'Previous Page',
visualDensity: VisualDensity.compact,
constraints: const BoxConstraints(minWidth: 32, minHeight: 32),
padding: EdgeInsets.zero,
onPressed: _currentPage > 1
? () => setState(() => _currentPage--)
: null,
icon: const Icon(Icons.chevron_left, size: 20),
),
if (!visiblePages.contains(1))
Row(children: [
_buildPageButton(1),
const Padding(
padding: EdgeInsets.symmetric(horizontal: 2),
child: Text("..."),
),
]),
for (int page in visiblePages) _buildPageButton(page),
if (!visiblePages.contains(totalPages) && totalPages > 0)
Row(children: [
const Padding(
padding: EdgeInsets.symmetric(horizontal: 2),
child: Text("..."),
),
_buildPageButton(totalPages),
]),
IconButton(
tooltip: 'Next Page',
visualDensity: VisualDensity.compact,
constraints: const BoxConstraints(minWidth: 32, minHeight: 32),
padding: EdgeInsets.zero,
onPressed: _currentPage < totalPages
? () => setState(() => _currentPage++)
: null,
icon: const Icon(Icons.chevron_right, size: 20),
),
],
),
],
),
);
}
Widget _buildPageButton(int page) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 2),
child: SizedBox(
width: 30,
height: 30,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: _currentPage == page
? const Color(0xFF00A6A6)
: Colors.grey[300],
foregroundColor:
_currentPage == page ? Colors.white : Colors.black,
minimumSize: const Size(30, 30),
padding: EdgeInsets.zero,
elevation: 0,
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
),
onPressed: () {
setState(() {
_currentPage = page;
});
},
child: Text(
page.toString(),
style: GoogleFonts.poppins(fontSize: 12),
),
),
),
);
}
static final _dataBold = GoogleFonts.poppins(
fontSize: 12,
fontWeight: FontWeight.w400,
color: Color(0xFF000000),
);
static final _dataBoldStatus = GoogleFonts.poppins(
fontSize: 12,
fontWeight: FontWeight.w400,
color: Colors.white,
);
static final _dataSub = GoogleFonts.poppins(
fontSize: 10,
fontWeight: FontWeight.w300,
color: Color(0xFF585757),
);
static final _dataColorSub = GoogleFonts.poppins(
fontSize: 10,
fontWeight: FontWeight.w400,
color: Color(0xFFFF731C),
);
}
class _CDHeaderDelegate extends SliverPersistentHeaderDelegate {
final bool showUHID;
final String uhidHeaderLabel;
final bool showLoggedIn;
final bool showAction;
_CDHeaderDelegate({
required this.showUHID,
this.uhidHeaderLabel = 'UHID',
required this.showLoggedIn,
required this.showAction,
});
@override
double get minExtent => 55;
@override
double get maxExtent => 55;
@override
Widget build(
BuildContext context, double shrinkOffset, bool overlapsContent) {
return Container(
color: const Color(0xFFE6F5F6),
padding: const EdgeInsets.symmetric(horizontal: 16),
alignment: Alignment.centerLeft,
child: Row(
children: [
_headerCell('Name', 3),
if (showUHID) _headerCell(uhidHeaderLabel, 2),
_headerCell('Relationship', 2),
_headerCell('Date Of Birth', 2),
_headerCell('Gender', 2),
_headerCell('Mobile', 2),
_headerCell('Email', 5),
if (showLoggedIn) _headerCell('Logged In', 2),
if (showLoggedIn) const SizedBox(width: 8),
_headerCell('Status', 2),
if (showAction) _headerCell('Action', 3, center: true),
],
),
);
}
Widget _headerCell(String text, int flex, {bool center = false}) {
return Expanded(
flex: flex,
child: Text(
text,
textAlign: center ? TextAlign.center : TextAlign.left,
style: GoogleFonts.poppins(
fontWeight: FontWeight.w600,
),
),
);
}
@override
bool shouldRebuild(covariant SliverPersistentHeaderDelegate oldDelegate) =>
true;
}
class _NetworkHospitalListDialog extends StatelessWidget {
final Map<String, String> hospitalLinks;
const _NetworkHospitalListDialog({required this.hospitalLinks});
Future<void> _openHospitalUrl(String url) async {
final uri = Uri.tryParse(url);
if (uri == null) return;
await launchUrl(uri, webOnlyWindowName: '_blank');
}
@override
Widget build(BuildContext context) {
final maxHeight = MediaQuery.of(context).size.height * 0.65;
final cappedMaxHeight = maxHeight.clamp(280.0, 520.0);
return AlertDialog(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
title: Text(
'Network Hospital List',
style: GoogleFonts.poppins(fontWeight: FontWeight.w600, fontSize: 18),
),
content: SizedBox(
width: 520,
child: ConstrainedBox(
constraints: BoxConstraints(maxHeight: cappedMaxHeight),
child: hospitalLinks.isEmpty
? Center(
child: Text(
'No Network Hospital List',
style: GoogleFonts.poppins(
fontSize: 14,
color: Colors.black54,
),
),
)
: ListView.separated(
shrinkWrap: true,
itemCount: hospitalLinks.length,
separatorBuilder: (_, __) => const SizedBox(height: 10),
itemBuilder: (context, index) {
final entry = hospitalLinks.entries.elementAt(index);
return InkWell(
onTap: () => _openHospitalUrl(entry.value),
borderRadius: BorderRadius.circular(8),
child: Container(
width: double.infinity,
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: const Color(0xFFF5F5F5),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: const Color(0xFFE8E8E8)),
),
child: Row(
children: [
Expanded(
child: Text(
entry.key,
style: GoogleFonts.poppins(
fontSize: 13,
fontWeight: FontWeight.w600,
color: const Color(0xFF009195),
),
),
),
const SizedBox(width: 8),
const Icon(
Icons.open_in_new,
size: 16,
color: Color(0xFF009195),
),
],
),
),
);
},
),
),
),
actions: [
ElevatedButton(
onPressed: () => Navigator.pop(context),
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF009195),
),
child: Text(
'Close',
style: GoogleFonts.poppins(color: Colors.white),
),
),
],
);
}
}
class _PolicyTermsDialog extends StatelessWidget {
final Map<String, String> terms;
const _PolicyTermsDialog({required this.terms});
@override
Widget build(BuildContext context) {
final entries = terms.entries.toList();
final maxHeight = MediaQuery.of(context).size.height * 0.65;
final cappedMaxHeight = maxHeight.clamp(320.0, 560.0);
final contentHeight =
(entries.length * 88.0).clamp(120.0, cappedMaxHeight);
return AlertDialog(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
title: Text(
'Policy Terms',
style: GoogleFonts.poppins(fontWeight: FontWeight.w600, fontSize: 18),
),
content: SizedBox(
width: 640,
height: contentHeight,
child: Scrollbar(
thumbVisibility: entries.length > 4,
child: ListView.separated(
itemCount: entries.length,
separatorBuilder: (_, __) => const SizedBox(height: 10),
itemBuilder: (context, index) {
final entry = entries[index];
return Container(
width: double.infinity,
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: const Color(0xFFF5F5F5),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: const Color(0xFFE8E8E8)),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
entry.key,
style: GoogleFonts.poppins(
fontSize: 13,
fontWeight: FontWeight.w600,
color: const Color(0xFF009195),
),
),
const SizedBox(height: 4),
Text(
entry.value,
style: GoogleFonts.poppins(
fontSize: 13,
fontWeight: FontWeight.w400,
color: Colors.black87,
),
),
],
),
);
},
),
),
),
actions: [
ElevatedButton(
onPressed: () => Navigator.pop(context),
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF009195),
),
child: Text('Close', style: GoogleFonts.poppins(color: Colors.white)),
),
],
);
}
}
class _ReminderHubDialog extends StatefulWidget {
final String clientId;
final String clientPolicyId;
final String clientBranchId;
final String token;
final ApiService apiService;
final String defaultSubject;
final String defaultHtmlBody;
final Future<void> Function(String subject, String htmlBody) onSend;
const _ReminderHubDialog({
required this.clientId,
required this.clientPolicyId,
required this.clientBranchId,
required this.token,
required this.apiService,
required this.defaultSubject,
required this.defaultHtmlBody,
required this.onSend,
});
@override
State<_ReminderHubDialog> createState() => _ReminderHubDialogState();
}
class _ReminderHubDialogState extends State<_ReminderHubDialog>
with SingleTickerProviderStateMixin {
static const _teal = Color(0xFF009195);
late final TabController _tabController;
@override
void initState() {
super.initState();
_tabController = TabController(length: 2, vsync: this);
_tabController.addListener(_onTabChanged);
}
void _onTabChanged() {
if (_tabController.indexIsChanging) return;
setState(() {});
}
@override
void dispose() {
_tabController.removeListener(_onTabChanged);
_tabController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final size = MediaQuery.sizeOf(context);
final isConfigTab = _tabController.index == 0;
final dialogWidth = isConfigTab
? math.min(520.0, size.width - 32)
: size.width * 0.95;
final dialogHeight = isConfigTab
? math.min(460.0, size.height * 0.72)
: size.height * 0.92;
return Dialog(
insetPadding: const EdgeInsets.all(16),
backgroundColor: Colors.white,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
child: SizedBox(
width: dialogWidth,
height: dialogHeight,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Padding(
padding: const EdgeInsets.fromLTRB(20, 16, 8, 0),
child: Row(
children: [
Text(
'Enrollment Reminder',
style: GoogleFonts.poppins(
fontSize: 18,
fontWeight: FontWeight.w600,
),
),
const Spacer(),
IconButton(
onPressed: () => Navigator.pop(context),
icon: const Icon(Icons.close, size: 20),
),
],
),
),
TabBar(
controller: _tabController,
labelColor: _teal,
unselectedLabelColor: Colors.black54,
indicatorColor: _teal,
labelStyle: GoogleFonts.poppins(
fontSize: 13,
fontWeight: FontWeight.w600,
),
unselectedLabelStyle: GoogleFonts.poppins(
fontSize: 13,
fontWeight: FontWeight.w500,
),
tabs: const [
Tab(text: 'Setup'),
Tab(text: 'Mail Template'),
],
),
const Divider(height: 1),
Expanded(
child: TabBarView(
controller: _tabController,
children: [
_ReminderMailConfigDialog(
clientId: widget.clientId,
clientPolicyId: widget.clientPolicyId,
clientBranchId: widget.clientBranchId,
token: widget.token,
apiService: widget.apiService,
defaultSubject: widget.defaultSubject,
defaultHtmlBody: widget.defaultHtmlBody,
embedded: true,
),
ReminderEmailTemplateDialog(
clientId: widget.clientId,
clientPolicyId: widget.clientPolicyId,
clientBranchId: widget.clientBranchId,
token: widget.token,
apiService: widget.apiService,
defaultSubject: widget.defaultSubject,
defaultHtmlBody: widget.defaultHtmlBody,
mode: ReminderEmailTemplateMode.send,
onSend: widget.onSend,
embedded: true,
),
],
),
),
],
),
),
);
}
}
class _ReminderMailConfigDialog extends StatefulWidget {
final String clientId;
final String clientPolicyId;
final String clientBranchId;
final String token;
final ApiService apiService;
final String defaultSubject;
final String defaultHtmlBody;
final bool embedded;
const _ReminderMailConfigDialog({
required this.clientId,
required this.clientPolicyId,
required this.clientBranchId,
required this.token,
required this.apiService,
required this.defaultSubject,
required this.defaultHtmlBody,
this.embedded = false,
});
@override
State<_ReminderMailConfigDialog> createState() =>
_ReminderMailConfigDialogState();
}
class _ReminderMailConfigDialogState extends State<_ReminderMailConfigDialog> {
static const _frequencies = <String, String>{
'daily': 'Daily',
'weekly': 'Weekly',
'monthly': 'Monthly',
'custom': 'Custom',
'working_days': 'Working Days',
};
bool _isLoadingConfig = true;
bool _isSaving = false;
int? _configId;
String _frequency = 'daily';
bool _isEnabled = true;
String _reminderDaysText = '';
final Set<String> _selectedWorkingDays = {};
List<Map<String, dynamic>> _workingDayOptions = [];
late final TextEditingController _reminderDaysController;
@override
void initState() {
super.initState();
_reminderDaysController = TextEditingController();
_loadConfig();
}
@override
void dispose() {
_reminderDaysController.dispose();
super.dispose();
}
Future<void> _loadConfig() async {
setState(() => _isLoadingConfig = true);
try {
final response = await widget.apiService.getReminderMailConfigApi(
widget.clientPolicyId,
widget.token,
);
if (!mounted) return;
final ok = response['status'] == true || response['status'] == 'success';
if (ok) {
_workingDayOptions = _parseWorkingDayOptions(
response['working_day_options'],
);
_applyConfig(response['data']);
} else {
ToastHelper.showErrorToast(
context,
response['message']?.toString() ?? 'Failed to load reminder config',
);
}
} catch (e) {
logDebug('Reminder config load failed: $e');
if (mounted) {
ToastHelper.showErrorToast(
context,
'Failed to load reminder configuration',
);
}
} finally {
if (mounted) {
setState(() => _isLoadingConfig = false);
}
}
}
List<Map<String, dynamic>> _parseWorkingDayOptions(dynamic raw) {
if (raw is! List) return _defaultWorkingDayOptions();
return raw
.whereType<Map>()
.map((item) => Map<String, dynamic>.from(item))
.toList();
}
List<Map<String, dynamic>> _defaultWorkingDayOptions() {
return [
{'value': 1, 'label': 'Mon', 'key': 'mon'},
{'value': 2, 'label': 'Tue', 'key': 'tue'},
{'value': 3, 'label': 'Wed', 'key': 'wed'},
{'value': 4, 'label': 'Thu', 'key': 'thu'},
{'value': 5, 'label': 'Fri', 'key': 'fri'},
{'value': 6, 'label': 'Sat', 'key': 'sat'},
{'value': 7, 'label': 'Sun', 'key': 'sun'},
];
}
void _applyConfig(dynamic data) {
_configId = null;
_frequency = 'daily';
_isEnabled = true;
_reminderDaysText = '';
_reminderDaysController.clear();
_selectedWorkingDays.clear();
if (data is! Map) return;
final config = Map<String, dynamic>.from(data);
_configId = int.tryParse(config['id']?.toString() ?? '');
_frequency = config['frequency']?.toString() ?? 'daily';
_isEnabled = config['is_enabled']?.toString() != '0';
if (_frequency == 'working_days') {
final labels = config['working_day_labels'];
if (labels is List && labels.isNotEmpty) {
_selectedWorkingDays.addAll(labels.map((e) => e.toString()));
} else {
_applyWorkingDaysFromReminderDays(
config['reminder_days']?.toString() ?? '',
);
}
} else if (_frequency != 'daily') {
_reminderDaysText = config['reminder_days']?.toString() ?? '';
_reminderDaysController.text = _reminderDaysText;
}
}
void _applyWorkingDaysFromReminderDays(String reminderDays) {
if (reminderDays.isEmpty) return;
final options = _workingDayOptions.isNotEmpty
? _workingDayOptions
: _defaultWorkingDayOptions();
final valueToLabel = <String, String>{
for (final option in options)
option['value'].toString(): option['label'].toString(),
};
for (final part in reminderDays.split(',')) {
final token = part.trim();
if (token.isEmpty) continue;
if (RegExp(r'^\d+$').hasMatch(token)) {
final normalized = token == '0' ? '7' : token;
final label = valueToLabel[normalized];
if (label != null) {
_selectedWorkingDays.add(label);
}
} else {
final match = options.firstWhere(
(option) =>
option['label'].toString().toLowerCase() == token.toLowerCase() ||
option['key'].toString().toLowerCase() == token.toLowerCase(),
orElse: () => {},
);
if (match.isNotEmpty) {
_selectedWorkingDays.add(match['label'].toString());
}
}
}
}
String? _buildReminderDays() {
switch (_frequency) {
case 'daily':
return null;
case 'working_days':
if (_selectedWorkingDays.isEmpty) return null;
return _selectedWorkingDays.join(',');
default:
final value = _reminderDaysText.trim();
return value.isEmpty ? null : value;
}
}
String? _validateBeforeSave() {
if (_frequency == 'working_days' && _selectedWorkingDays.isEmpty) {
return 'Select at least one working day';
}
if (_frequency != 'daily' &&
_frequency != 'working_days' &&
_reminderDaysText.trim().isEmpty) {
return 'Reminder days are required for $_frequency frequency';
}
return null;
}
Future<int?> _resolveHrId() async {
final tokenService = TokenStorageService();
final hrId = await tokenService.readValue('enrollmentHrId') ??
await tokenService.readValue('empHrId');
return int.tryParse(hrId?.toString() ?? '');
}
Future<void> _saveConfig() async {
final validationError = _validateBeforeSave();
if (validationError != null) {
ToastHelper.showWarningToast(context, validationError);
return;
}
setState(() => _isSaving = true);
try {
final policyId = int.tryParse(widget.clientPolicyId);
if (policyId == null) {
ToastHelper.showErrorToast(context, 'Invalid client policy id');
return;
}
final payload = <String, dynamic>{
'client_policy_id': policyId,
'frequency': _frequency,
'is_enabled': _isEnabled ? 1 : 0,
};
final reminderDays = _buildReminderDays();
if (reminderDays != null) {
payload['reminder_days'] = reminderDays;
}
final hrId = await _resolveHrId();
if (hrId != null) {
payload['hr_id'] = hrId;
}
if (_configId != null) {
payload['id'] = _configId;
}
final response = await widget.apiService.saveReminderMailConfigApi(
widget.token,
payload,
);
if (!mounted) return;
final ok = response['status'] == true || response['status'] == 'success';
if (ok) {
final data = response['data'];
if (data is Map) {
_applyConfig(data);
}
ToastHelper.showSuccessToast(
context,
response['message']?.toString() ??
'Reminder mail configuration saved successfully',
);
Navigator.pop(context);
} else {
ToastHelper.showErrorToast(
context,
response['message']?.toString() ?? 'Failed to save configuration',
);
}
} catch (e) {
logDebug('Reminder config save failed: $e');
if (mounted) {
ToastHelper.showErrorToast(
context,
'Failed to save reminder configuration',
);
}
} finally {
if (mounted) {
setState(() => _isSaving = false);
}
}
}
Widget _buildDaysField() {
if (_frequency == 'daily') {
return const SizedBox.shrink();
}
if (_frequency == 'working_days') {
final options = _workingDayOptions.isNotEmpty
? _workingDayOptions
: _defaultWorkingDayOptions();
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Working Days',
style: GoogleFonts.poppins(
fontSize: 13,
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 8),
Wrap(
spacing: 8,
runSpacing: 8,
children: options.map((option) {
final label = option['label'].toString();
final selected = _selectedWorkingDays.contains(label);
return FilterChip(
label: Text(label, style: GoogleFonts.poppins(fontSize: 12)),
selected: selected,
onSelected: (value) {
setState(() {
if (value) {
_selectedWorkingDays.add(label);
} else {
_selectedWorkingDays.remove(label);
}
});
},
selectedColor: const Color(0xFFC5F2F4),
checkmarkColor: const Color(0xFF009195),
);
}).toList(),
),
],
);
}
final helperText = switch (_frequency) {
'weekly' => 'Weekday numbers 0-6 (Sun-Sat), comma-separated. Example: 1,3,5',
'monthly' => 'Days of month 1-31, comma-separated. Example: 1,15,28',
'custom' => 'Custom days of month 1-31, comma-separated. Example: 5,10,20',
_ => '',
};
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Reminder Days',
style: GoogleFonts.poppins(
fontSize: 13,
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 6),
TextField(
controller: _reminderDaysController,
onChanged: (value) => _reminderDaysText = value,
style: GoogleFonts.poppins(fontSize: 13),
decoration: InputDecoration(
isDense: true,
hintText: helperText,
hintStyle: GoogleFonts.poppins(fontSize: 12, color: Colors.black38),
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: 4),
Text(
helperText,
style: GoogleFonts.poppins(fontSize: 11, color: Colors.black45),
),
],
);
}
Future<void> _openTemplateEditor() async {
await showDialog<void>(
context: context,
barrierDismissible: false,
builder: (dialogContext) {
return ReminderEmailTemplateDialog(
clientId: widget.clientId,
clientPolicyId: widget.clientPolicyId,
clientBranchId: widget.clientBranchId,
token: widget.token,
apiService: widget.apiService,
defaultSubject: widget.defaultSubject,
defaultHtmlBody: widget.defaultHtmlBody,
mode: ReminderEmailTemplateMode.config,
);
},
);
}
Widget _buildConfigContent() {
final isBusy = _isSaving;
final formMaxWidth = widget.embedded ? 420.0 : 560.0;
if (_isLoadingConfig) {
return const Center(child: CircularProgressIndicator());
}
final form = ConstrainedBox(
constraints: BoxConstraints(maxWidth: formMaxWidth),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(
'Configure the schedule for automated enrollment reminder emails.',
style: GoogleFonts.poppins(
fontSize: 13,
color: Colors.black54,
),
),
const SizedBox(height: 16),
Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
decoration: BoxDecoration(
color: const Color(0xFFF8FAFA),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: const Color(0xFFE8EEF0)),
),
child: Row(
children: [
Expanded(
child: Text(
'Enable scheduled reminders',
style: GoogleFonts.poppins(
fontSize: 13,
fontWeight: FontWeight.w600,
),
),
),
Switch(
value: _isEnabled,
activeColor: const Color(0xFF009195),
onChanged: isBusy
? null
: (value) => setState(() => _isEnabled = value),
),
],
),
),
const SizedBox(height: 16),
Text(
'Frequency',
style: GoogleFonts.poppins(
fontSize: 13,
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 6),
SizedBox(
width: double.infinity,
child: DropdownButtonFormField<String>(
value: _frequency,
isExpanded: true,
isDense: true,
decoration: InputDecoration(
filled: true,
fillColor: const Color(0xFFF5F5F5),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: BorderSide.none,
),
contentPadding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 8,
),
),
items: _frequencies.entries
.map(
(entry) => DropdownMenuItem(
value: entry.key,
child: Text(
entry.value,
style: GoogleFonts.poppins(fontSize: 13),
),
),
)
.toList(),
onChanged: isBusy
? null
: (value) {
if (value == null) return;
setState(() {
_frequency = value;
_reminderDaysText = '';
_reminderDaysController.clear();
_selectedWorkingDays.clear();
});
},
),
),
const SizedBox(height: 14),
_buildDaysField(),
],
),
);
return SingleChildScrollView(
padding: const EdgeInsets.fromLTRB(20, 16, 20, 8),
child: Align(
alignment: Alignment.topCenter,
child: form,
),
);
}
Widget _buildConfigActions() {
final isBusy = _isSaving;
final formMaxWidth = widget.embedded ? 420.0 : 560.0;
return Padding(
padding: const EdgeInsets.fromLTRB(20, 8, 20, 16),
child: Align(
alignment: Alignment.topCenter,
child: ConstrainedBox(
constraints: BoxConstraints(maxWidth: formMaxWidth),
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
TextButton(
onPressed: isBusy ? null : () => Navigator.pop(context),
child: Text(
'Cancel',
style: GoogleFonts.poppins(color: Colors.black54),
),
),
const SizedBox(width: 8),
ElevatedButton(
onPressed: isBusy || _isLoadingConfig ? null : _saveConfig,
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF009195),
minimumSize: const Size(88, 36),
padding: const EdgeInsets.symmetric(horizontal: 16),
),
child: _isSaving
? const SizedBox(
width: 18,
height: 18,
child: CircularProgressIndicator(
strokeWidth: 2,
color: Colors.white,
),
)
: Text(
'Save',
style: GoogleFonts.poppins(color: Colors.white),
),
),
],
),
),
),
);
}
@override
Widget build(BuildContext context) {
if (widget.embedded) {
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Expanded(
child: Align(
alignment: Alignment.topCenter,
child: _buildConfigContent(),
),
),
const Divider(height: 1),
_buildConfigActions(),
],
);
}
return AlertDialog(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
title: Text(
'Email Reminder Config',
style: GoogleFonts.poppins(fontWeight: FontWeight.w600, fontSize: 18),
),
content: SizedBox(
width: 560,
child: _buildConfigContent(),
),
actions: [
TextButton(
onPressed: _isSaving ? null : () => Navigator.pop(context),
child: Text('Cancel', style: GoogleFonts.poppins(color: Colors.black54)),
),
ElevatedButton(
onPressed: _isSaving || _isLoadingConfig ? null : _saveConfig,
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF009195),
),
child: _isSaving
? const SizedBox(
width: 18,
height: 18,
child: CircularProgressIndicator(
strokeWidth: 2,
color: Colors.white,
),
)
: Text('Save', style: GoogleFonts.poppins(color: Colors.white)),
),
],
);
}
}