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 createState() => _HrPolicyDetailsState(); } class _HrPolicyDetailsState extends State 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> getEmpDependenceByClintId = []; List> getCDPolicies = []; bool isLoading = false; bool _isLoading = false; bool _isSendingReminder = false; bool _isLoadingPolicyTerms = false; bool _isLoadingHospitalList = false; // dynamic clintID; late TabController _tabController; // List dataPolicy = []; List reversedDataPolicy = []; List> originalData = []; // Original data source List> 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> _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 selectedEmployeeIds = {}; /// ids shown in the current page (for header checkbox) List currentPageIds = []; List 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 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 _persistPolicyContext() async { Future 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 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 _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 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? 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>.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 _loadAllowedModules() async { modulesString = await tokenService.readValue('empAllowed_modules'); logDebug("empmodulesString - $modulesString"); if (modulesString != null && modulesString!.trim().isNotEmpty) { final List? moduleList = modulesString ?.replaceAll('[', '') .replaceAll(']', '') .split(',') .map((e) => int.tryParse(e.trim()) ?? -1) .where((id) => id != -1) .toList(); hasModule = moduleList?.contains(storeModuleId) ?? false; } } Future> _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 = { '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> _parseFilterChips(Map 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.from(source); final chips = >[]; 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> _parseEmployeeList(Map response) { final data = response['data']; if (data is List) { return data .whereType() .map((e) => Map.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((e) => Map.from(e)) .toList(); } } return []; } /// Initial load: fetch filter chips only (`card_data=all`). No employee table. Future 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 _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 _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>.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 _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>.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 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 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 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 _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 row) { final raw = localTokenType == 'pre' ? (row['emp_status'] ?? row['status']) : row['status']; return raw?.toString().toLowerCase().trim() ?? ''; } String? _getRowStatusRaw(Map 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 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 row) => _getLoggedInValue(row) == 'yes'; bool _isSelfRow(Map row) => row['relationship']?.toString().toLowerCase().trim() == 'self'; String _getLoggedInDisplayLabel(Map row) => _getLoggedInValue(row) == 'yes' ? 'Yes' : 'No'; Color _getLoggedInChipColor(Map row) => _getLoggedInValue(row) == 'yes' ? const Color(0xFFBDF9D9) : const Color(0xFFE8EAF6); bool _matchesPreStatusFilter(Map 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 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>.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 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 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 _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 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( 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 _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.from(response['data'] as Map); final hospitalLinks = _parseHospitalLinks(data['tpa_network_hospitals']); await showDialog( 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 _parseHospitalLinks(dynamic raw) { if (raw is! Map) return {}; final parsed = {}; 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 _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>.from(response['data'] ?? []); Map? 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 = {}; 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( 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 _openReminderDialog() async { final clientId = localClientId ?? widget.ClientId; final clientPolicyId = localClientPolicyId ?? widget.ClientPoliyId; final clientBranchId = localClientBranchId ?? widget.clientBranchId; final token = localToken ?? widget.Token; await showDialog( 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 _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 _logReminderActivity() async { final postId = await tokenService.readValue('empHrId'); final preId = await tokenService.readValue('enrollmentEmpPrimaryId'); const activityPre = 'send_enrollment_reminder'; try { await apiService.getPreLogHrActivity( postId!, preId!, localToken!, activityPre, ); } catch (e) { logDebug('Reminder activity log failed: $e'); } } Future getEcardBulkDownload() async { try { final emp_policy_ids = selectedEmployeeIds.toList(); 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( 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 _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 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 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( 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() // .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( // // value: _rowsPerPage, // // items: [5, 10, 15, 20, 50].map((int value) { // // return DropdownMenuItem( // // 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 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 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( value: _rowsPerPage, isDense: true, focusColor: Colors.transparent, items: [5, 10, 15, 20, 50].map((int value) { return DropdownMenuItem( 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 hospitalLinks; const _NetworkHospitalListDialog({required this.hospitalLinks}); Future _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 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 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 = { '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 _selectedWorkingDays = {}; List> _workingDayOptions = []; late final TextEditingController _reminderDaysController; @override void initState() { super.initState(); _reminderDaysController = TextEditingController(); _loadConfig(); } @override void dispose() { _reminderDaysController.dispose(); super.dispose(); } Future _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> _parseWorkingDayOptions(dynamic raw) { if (raw is! List) return _defaultWorkingDayOptions(); return raw .whereType() .map((item) => Map.from(item)) .toList(); } List> _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.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 = { 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 _resolveHrId() async { final tokenService = TokenStorageService(); final hrId = await tokenService.readValue('enrollmentHrId') ?? await tokenService.readValue('empHrId'); return int.tryParse(hrId?.toString() ?? ''); } Future _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 = { '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 _openTemplateEditor() async { await showDialog( 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( 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)), ), ], ); } }