enrollment-app/lib/presentation/hrPolicyDetails.dart
2026-07-02 15:39:19 +05:30

3854 lines
142 KiB
Dart
Executable File

import 'package:csv/csv.dart';
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:nhancepolicy/customAppBar/customAppBar.dart';
import 'package:nhancepolicy/presentation/policies.dart';
import 'package:nhancepolicy/presentation/preFileUpload.dart';
import 'package:nhancepolicy/presentation/postFileUpload.dart';
import 'package:nhancepolicy/service/api_service.dart';
import 'package:nhancepolicy/presentation/claims.dart';
import 'package:jwt_decode/jwt_decode.dart';
import 'dart:convert';
import 'dart:async';
import 'package:http/http.dart' as http;
import 'package:nhancepolicy/customAppBar/toastHelper.dart';
import 'package:nhancepolicy/service/token_storage_service.dart';
import 'package:universal_html/html.dart' as html;
import 'dart:typed_data';
import 'dart:io';
import 'package:flutter/foundation.dart' show kIsWeb;
import 'package:flutter/services.dart';
import 'package:intl/intl.dart';
import 'package:path_provider/path_provider.dart';
import 'package:path/path.dart' as path;
import 'package:collection/collection.dart';
import 'package:url_launcher/url_launcher.dart';
import '../customAppBar/base_layout.dart';
import '../customAppBar/customFooter.dart';
import '../service/secure_pop_scope.dart';
import 'package:nhancepolicy/presentation/email_template/email_template_models.dart';
import 'package:nhancepolicy/presentation/email_template/reminder_email_template_dialog.dart';
import 'package:nhancepolicy/logger.dart';
class hrPolicyDetails extends StatefulWidget {
final String ClientId;
final String policyTypeId;
// final String ClientBranchId;
final String ClientPoliyId;
final String clientBranchId;
final String Token;
final String TokenType;
final String cardType;
final String cardPolicyNo;
final String cardInsurer_name;
final String cardPolicy_name;
final String cardPolicy_ExpDate;
final String total_premium;
final int is_ecard_bulk_download_for_employee;
const hrPolicyDetails(
{Key? key,
required this.ClientId,
required this.policyTypeId,
// required this.ClientBranchId,
required this.ClientPoliyId,
required this.clientBranchId,
required this.Token,
required this.TokenType,
required this.cardType,
required this.cardPolicyNo,
required this.cardInsurer_name,
required this.cardPolicy_name,
required this.cardPolicy_ExpDate,
required this.total_premium,
required this.is_ecard_bulk_download_for_employee})
: super(key: key);
@override
State<hrPolicyDetails> createState() => _HrPolicyDetailsState();
}
class _HrPolicyDetailsState extends State<hrPolicyDetails>
with TickerProviderStateMixin {
String? localClientId;
String? localPolicyTypeId;
String? localClientPolicyId;
String? localClientBranchId;
String? localToken;
String? localTokenType;
String? localCardType;
String? localCardPolicyNo;
String? localCardInsurerName;
String? localCardPolicyName;
String? localCardPolicyExpDate;
String? localTotalPremium;
int localIsEcardBulkDownload = 0;
final tokenService = TokenStorageService();
dynamic empPrimaryId;
dynamic empClientId;
dynamic empClientBranchId;
dynamic empHrId;
dynamic enrollmentClient_id;
dynamic enrollmentEmpClientBranchId;
dynamic enrollmentHrId;
String? _postPreToken = '';
Uint8List? fileBytes;
String empCodeFromHrPolcy = '';
bool hasAnyEcardLink = false;
late String _token;
List<Map<String, dynamic>> getEmpDependenceByClintId = [];
List<Map<String, dynamic>> getCDPolicies = [];
bool isLoading = false;
bool _isLoading = false;
bool _isSendingReminder = false;
bool _isLoadingPolicyTerms = false;
// dynamic clintID;
late TabController _tabController;
// List<dynamic> dataPolicy = [];
List<dynamic> reversedDataPolicy = [];
List<Map<String, dynamic>> originalData = []; // Original data source
List<Map<String, dynamic>> filteredData = []; // Filtered data source
dynamic argumentsData;
dynamic policyType;
dynamic policyName;
dynamic clientPolicyId;
dynamic clientId;
dynamic empRefId;
String? modulesString;
final storeModuleId = 4;
bool hasModule = false;
int inceptionType = 0;
TextEditingController searchController = TextEditingController();
late ApiService apiService;
int _currentPage = 1;
int _rowsPerPage = 5;
/// Pre-enrollment status chip filter (null = show all).
String? _preStatusFilter;
/// holds selected employee ids as String or int (be consistent)
final Set<String> selectedEmployeeIds = {};
/// ids shown in the current page (for header checkbox)
List<String> currentPageIds = [];
List<dynamic> get _paginatedData {
final total = filteredData.length;
if (total == 0) return [];
final startIndex = ((_currentPage - 1) * _rowsPerPage).clamp(0, total);
final endIndex = (_currentPage * _rowsPerPage).clamp(0, total);
if (startIndex >= endIndex) return [];
return filteredData.sublist(startIndex, endIndex);
}
Color getStatusColor(String status) {
switch (status.toLowerCase()) {
case 'draft':
return const Color(0xFFF9EBBD);
case 'enrolled':
return const Color(0xFFBDF9D9);
case 'total':
return const Color(0xFFE2FBCB);
case 'under process':
return Color(0xFFBDF9D9);
case 'active':
return Colors.green;
case 'inactive':
return Colors.red;
default:
return const Color(0xFFB0BEC5);
}
}
//
// @override
// void initState() {
// super.initState();
// apiService = ApiService(context); // Initialize ApiService here
//
// logDebug("_PreEnrollmentState 1");
// getCDPoliciesDetails();
// logDebug('allowed_modules');
// }
@override
void initState() {
super.initState();
apiService = ApiService(context);
// If you are using TabBar, you MUST initialize this:
_tabController = TabController(length: 2, vsync: this);
restorePolicyData();
}
Future<void> restorePolicyData() async {
localClientId = widget.ClientId.isNotEmpty
? widget.ClientId
: await tokenService.readValue('hr_ClientId');
localPolicyTypeId = widget.policyTypeId.isNotEmpty
? widget.policyTypeId
: await tokenService.readValue('hr_policyTypeId');
localClientPolicyId = widget.ClientPoliyId.isNotEmpty
? widget.ClientPoliyId
: await tokenService.readValue('hr_ClientPoliyId');
localClientBranchId = widget.clientBranchId.isNotEmpty
? widget.clientBranchId
: await tokenService.readValue('hr_clientBranchId');
localToken = widget.Token.isNotEmpty
? widget.Token
: await tokenService.readValue('hr_Token');
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;
getCDPoliciesDetails();
}
Future<void> clearPolicyStorage() async {
await tokenService.removeValue('hr_ClientId');
await tokenService.removeValue('hr_policyTypeId');
await tokenService.removeValue('hr_ClientPoliyId');
await tokenService.removeValue('hr_clientBranchId');
await tokenService.removeValue('hr_Token');
await tokenService.removeValue('hr_TokenType');
await tokenService.removeValue('hr_cardType');
await tokenService.removeValue('hr_cardPolicyNo');
await tokenService.removeValue('hr_cardInsurer_name');
await tokenService.removeValue('hr_cardPolicy_name');
await tokenService.removeValue('hr_cardPolicy_ExpDate');
await tokenService.removeValue('hr_total_premium');
await tokenService.removeValue('hr_is_ecard_bulk_download_for_employee');
}
// Future<void> _loadToken() async {
// _postPreToken = tokenService.getCurrentToken();
// if(localTokenType == "post") {
// empClientId = await tokenService.readValue('empClientId');
// empClientBranchId = await tokenService.readValue('empClientBranchId');
// empHrId = await tokenService.readValue('empHrId');
// }
// if(localTokenType == "pre") {
// enrollmentClient_id = await tokenService.readValue('enrollmentClient_id');
// enrollmentEmpClientBranchId = await tokenService.readValue('enrollmentEmpClientBranchId');
// enrollmentHrId = await tokenService.readValue('enrollmentHrId');
// }
//
// getCDPoliciesDetails();
// }
Future<void> getCDPoliciesDetails_06FEB() async {
logDebug('9');
setState(() {
isLoading = true;
});
try {
logDebug('10');
modulesString = await tokenService.readValue('empAllowed_modules');
// modulesString = "[2,3]";
logDebug("empmodulesString - $modulesString");
if (modulesString != null && modulesString!.trim().isNotEmpty) {
final List<int>? moduleList = modulesString
?.replaceAll('[', '')
.replaceAll(']', '')
.split(',')
.map((e) => int.tryParse(e.trim()) ?? -1) // convert to int safely
.where((id) => id != -1) // filter out invalid
.toList();
hasModule = moduleList!.contains(storeModuleId);
}
final response = localTokenType == "post"
? await apiService.getEmployeeAndDependenceToApi(
localClientId ?? widget.ClientId,
localClientPolicyId ?? widget.ClientPoliyId,
localClientBranchId ?? widget.clientBranchId,
localToken ?? widget.Token)
: await apiService.getEmployeeAndDependenceToApiPre(
localClientId ?? widget.ClientId,
localClientPolicyId ?? widget.ClientPoliyId,
localClientBranchId ?? widget.clientBranchId,
localToken ?? widget.Token!);
if (response['status'] == 'success') {
setState(() {
isLoading = false;
});
setState(() {
getCDPolicies = List<Map<String, dynamic>>.from(response['data']);
originalData = getCDPolicies;
_refreshFilteredData();
logDebug('filteredData');
logDebug(filteredData);
});
} else {
setState(() {
isLoading = false;
});
// ToastHelper.showWarningToast(
// context, 'Request failed with status: ${response.statusCode}');
// logDebug('Request failed with status: ${response['code']}');
}
} catch (e) {
setState(() {
isLoading = false;
});
logDebug('Exception occurred: $e');
} finally {
setState(() {
_isLoading = false;
});
}
}
Future<void> getCDPoliciesDetails() async {
logDebug('getCDPoliciesDetails started');
setState(() {
isLoading = true;
});
try {
logDebug('Fetching modules...');
modulesString = await tokenService.readValue('empAllowed_modules');
logDebug("empmodulesString - $modulesString");
if (modulesString != null && modulesString!.trim().isNotEmpty) {
final List<int>? moduleList = modulesString
?.replaceAll('[', '')
.replaceAll(']', '')
.split(',')
.map((e) => int.tryParse(e.trim()) ?? -1)
.where((id) => id != -1)
.toList();
hasModule = moduleList?.contains(storeModuleId) ?? false;
}
logDebug('Calling API with TokenType: ${localTokenType}');
logDebug(
'ClientId: ${localClientId}, ClientPoliyId: ${localClientPolicyId}');
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!);
logDebug('API Response: ${response.toString()}');
if (response != null && response['status'] == 'success') {
final data = response['data'];
if (data != null && data is List) {
// Check if it's actually a list
setState(() {
getCDPolicies = List<Map<String, dynamic>>.from(data);
originalData = getCDPolicies;
_refreshFilteredData();
isLoading = false;
});
} else {
setState(() => isLoading = false);
}
} else {
final errorCode = response?['code'] ?? 'Unknown';
// final errorMessage = response?['message'] ?? 'Request failed';
// logDebug('❌ Request failed - Code: $errorCode, Message: $errorMessage');
setState(() {
isLoading = false;
});
if (mounted) {
// ToastHelper.showErrorToast(context, errorMessage);
}
}
} catch (e, stackTrace) {
logDebug('❌ Exception occurred: $e');
logDebug('Stack trace: $stackTrace');
setState(() {
isLoading = false;
});
if (mounted) {
ToastHelper.showErrorToast(context,
'Failed to load data. Please check your connection and try again.');
}
}
}
Future<void> getEcardDownload(String? empCode, String? empId,
String? clientPolicyId, String? policyNo) async {
final eCarDParams = {
'id': empId,
'emp_code': empCode,
'client_policy_id': clientPolicyId,
'policy_no': policyNo
};
final response = await apiService.getEcardRequest(eCarDParams, localToken!);
logDebug('check 1');
final ecardDownloadUrl = response['data']['eCardDownload'];
final message = response['data']['message'];
if (ecardDownloadUrl != null) {
logDebug('✅ Link: $ecardDownloadUrl');
await _launchURL(ecardDownloadUrl); // Only launch if status is success
// ToastHelper.showSuccessToast(context, message);
} else {
logDebug('❌ Error: $message');
ToastHelper.showErrorToast(context, message);
}
}
Future<void> copyEcardLink(String? ecardLink) async {
if (ecardLink == null || ecardLink.trim().isEmpty) {
if (mounted) {
ToastHelper.showErrorToast(context, 'E-card link not available');
}
return;
}
if (kIsWeb) {
await html.window.navigator.clipboard?.writeText(ecardLink);
} else {
await Clipboard.setData(ClipboardData(text: ecardLink));
}
if (mounted) {
ToastHelper.showSuccessToast(context, 'Link copied to clipboard');
}
}
Future<void> sendEcardViaEmail(
dynamic empPolicyId,
dynamic clientPolicyId,
) async {
try {
final response = await apiService.sendMailForIndividualEmployeeEcard(
empPolicyId: empPolicyId,
clientPolicyId: clientPolicyId,
token: localToken!,
);
if (!mounted) return;
if (response['status'] == true) {
ToastHelper.showSuccessToast(
context,
response['message']?.toString() ?? 'Mail sent successfully',
);
} else {
ToastHelper.showErrorToast(
context,
response['message']?.toString() ?? 'Failed to send mail.',
);
}
} catch (e) {
logDebug('❌ sendEcardViaEmail error: $e');
if (mounted) {
ToastHelper.showErrorToast(
context,
'Failed to send mail. Please try again.',
);
}
}
}
Future<void> _launchURL(String url) async {
final Uri uri = Uri.parse(url); // Parse the URL properly
logDebug('_launchURL $uri');
logDebug('If $uri');
if (kIsWeb) {
// Open in current browser tab on web.
await launchUrl(uri, webOnlyWindowName: '_self');
} else {
await launchUrl(uri, mode: LaunchMode.externalApplication);
}
}
bool _isEnrolledStatus(String status) =>
status == 'under process' ||
status == 'enrolled' ||
status == 'submitted';
String _getRowStatus(Map<String, dynamic> row) {
final raw = localTokenType == 'pre'
? (row['emp_status'] ?? row['status'])
: row['status'];
return raw?.toString().toLowerCase().trim() ?? '';
}
String? _getRowStatusRaw(Map<String, dynamic> row) {
final raw = localTokenType == 'pre'
? (row['emp_status'] ?? row['status'])
: row['status'];
final value = raw?.toString().trim();
return value == null || value.isEmpty ? null : value;
}
String _getLoggedInValue(Map<String, dynamic> row) {
final loggedInRaw = row['logged_in'] ?? row['emp_is_active'];
final value = loggedInRaw?.toString().toLowerCase().trim() ?? '';
// API can return: null / Yes / No / 1 / 0 / true / false.
if (value.isEmpty || value == 'null' || value == 'no' || value == '0' || value == 'false') {
return 'no';
}
return (value == 'yes' || value == '1' || value == 'true') ? 'yes' : 'no';
}
bool _isLoggedInRow(Map<String, dynamic> row) =>
_getLoggedInValue(row) == 'yes';
bool _isSelfRow(Map<String, dynamic> row) =>
row['relationship']?.toString().toLowerCase().trim() == 'self';
String _getLoggedInDisplayLabel(Map<String, dynamic> row) =>
_getLoggedInValue(row) == 'yes' ? 'Yes' : 'No';
Color _getLoggedInChipColor(Map<String, dynamic> row) =>
_getLoggedInValue(row) == 'yes'
? const Color(0xFFBDF9D9)
: const Color(0xFFE8EAF6);
bool _matchesPreStatusFilter(Map<String, dynamic> row, String filter) {
final status = _getRowStatus(row);
final relationship = row['relationship']?.toString().toLowerCase().trim() ?? '';
switch (filter) {
case 'emp_count':
return relationship == 'self';
case 'enrolled':
return _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 status == 'draft';
default:
return true;
}
}
bool _matchesSearch(Map<String, dynamic> row, String lowerQuery) {
final status = _getRowStatus(row);
bool statusMatch;
if (lowerQuery == 'active' || lowerQuery == 'inactive') {
statusMatch = status == lowerQuery;
} else {
statusMatch = status.contains(lowerQuery);
}
return row['name']?.toString().toLowerCase().contains(lowerQuery) == true ||
row['emp_code']?.toString().toLowerCase().contains(lowerQuery) ==
true ||
row['uhid']?.toString().toLowerCase().contains(lowerQuery) == true ||
row['relationship']?.toString().toLowerCase().contains(lowerQuery) ==
true ||
row['formatted_dob']
?.toString()
.replaceAll("/", "-")
.toLowerCase()
.contains(lowerQuery) ==
true ||
row['gender']?.toString().toLowerCase().contains(lowerQuery) == true ||
row['mobile']?.toString().toLowerCase().contains(lowerQuery) == true ||
row['email_corporate']?.toString().toLowerCase().contains(lowerQuery) ==
true ||
statusMatch;
}
void _refreshFilteredData() {
Iterable<Map<String, dynamic>> data = originalData;
final lowerQuery = searchController.text.toLowerCase().trim();
if (lowerQuery.isNotEmpty) {
data = data.where((row) => _matchesSearch(row, lowerQuery));
}
if (localTokenType == 'pre' && _preStatusFilter != null) {
data = data.where((row) => _matchesPreStatusFilter(row, _preStatusFilter!));
}
filteredData = data.toList();
hasAnyEcardLink = filteredData.any(
(item) => item['ecard_download_link'] != null,
);
}
void search(String query) {
setState(() {
_currentPage = 1;
_refreshFilteredData();
});
}
void _onPreStatusFilterTap(String filter) {
setState(() {
_preStatusFilter = filter;
_currentPage = 1;
_refreshFilteredData();
});
}
void _resetPreStatusFilter() {
setState(() {
_preStatusFilter = null;
_currentPage = 1;
_refreshFilteredData();
});
}
void exportToCsv(List<Map<String, dynamic>> data) {
List<List<String>> rows = [];
// Header
rows.add([
'Emp Code',
'Name',
'UHID',
'Relationship',
'Date Of Birth',
'Gender',
'Mobile',
'Email',
'Status'
]);
// Data rows
for (var item in data) {
rows.add([
item['emp_code'] ?? '',
item['name'] ?? '',
item['uhid'] ?? '',
item['relationship'] ?? '',
item['formatted_dob'] ?? '',
item['gender'] ?? '',
item['mobile'] ?? '',
item['email_corporate'] ?? '',
_getRowStatusRaw(item) ?? '',
]);
}
// Convert to CSV string
String csvData = const ListToCsvConverter().convert(rows);
// For Web: Create download
final bytes = utf8.encode(csvData);
final blob = html.Blob([bytes]);
final url = html.Url.createObjectUrlFromBlob(blob);
final String csvFileName = "policies(${localCardPolicyNo}).csv";
final anchor = html.AnchorElement(href: url)
..setAttribute("download", csvFileName)
..click();
html.Url.revokeObjectUrl(url);
handleExportAction();
}
Future<void> handleExportAction() async {
logDebug('handleExportAction');
final postId = await tokenService.readValue('empHrId');
final preId = await tokenService.readValue('enrollmentEmpPrimaryId');
var activity = "export_empdata";
var activityPre = "export_preempdata";
dynamic response;
logDebug('postId - $postId');
logDebug('preId - $preId');
logDebug('activity - $activity');
try {
logDebug('10');
if (localTokenType == 'pre') {
response = await apiService.getPreLogHrActivity(
postId!, preId!, localToken!, activityPre);
} else if (localTokenType == 'post') {
response = await apiService.getPostLogHrActivity(
postId!, preId!, localToken!, activity);
}
if (response['status'] == 'success') {
logDebug('Request success');
} else {
// ToastHelper.showWarningToast(
// context, 'Request failed with status: ${response.statusCode}');
logDebug('Request failed with status: ${response['code']}');
}
} catch (e) {
logDebug('Exception occurred: $e');
}
}
dynamic _parseNumericId(dynamic value) {
if (value == null) return value;
return int.tryParse(value.toString()) ?? value;
}
List<String> _getInceptionExportStatus() {
switch (_preStatusFilter) {
case 'draft':
return ['draft'];
case 'enrolled':
return ['enrolled'];
default:
return ['enrolled'];
}
}
({String empCode, String empName}) _getInceptionSearchParams() {
final query = searchController.text.trim();
if (query.isEmpty) {
return (empCode: '', empName: '');
}
final lowerQuery = query.toLowerCase();
final codeMatch = originalData.any(
(row) =>
row['emp_code']?.toString().toLowerCase().contains(lowerQuery) ??
false,
);
final nameMatch = originalData.any(
(row) =>
row['name']?.toString().toLowerCase().contains(lowerQuery) ?? false,
);
if (codeMatch && !nameMatch) {
return (empCode: query, empName: '');
}
if (nameMatch && !codeMatch) {
return (empCode: '', empName: query);
}
return (empCode: query, empName: query);
}
Future<void> downloadInceptionExport() async {
try {
final clientId = await tokenService.readValue('enrollmentClient_id') ??
localClientId ??
widget.ClientId;
final branchId =
await tokenService.readValue('enrollmentEmpClientBranchId') ??
localClientBranchId ??
widget.clientBranchId;
final policyId = localClientPolicyId ?? widget.ClientPoliyId;
final searchParams = _getInceptionSearchParams();
final status = _getInceptionExportStatus();
logDebug(
'downloadInceptionExport client=$clientId branch=$branchId '
'policies=$policyId status=$status '
'empCode=${searchParams.empCode} empName=${searchParams.empName}',
);
final response = await apiService.downloadInception(
client: _parseNumericId(clientId),
branch: _parseNumericId(branchId),
policies: _parseNumericId(policyId),
status: status,
empCode: searchParams.empCode,
empName: searchParams.empName,
token: localToken!,
);
if (!mounted) return;
if (response['status'] == true) {
final downloadUrl = response['data']?['downloadUrl'];
if (downloadUrl != null && downloadUrl.toString().isNotEmpty) {
await _launchURL(downloadUrl.toString());
ToastHelper.showSuccessToast(
context,
response['message']?.toString() ??
'Inception export generated successfully.',
);
} else {
ToastHelper.showErrorToast(context, 'Download URL not available');
}
} else {
ToastHelper.showErrorToast(
context,
response['message']?.toString() ??
'Failed to generate inception export.',
);
}
} catch (e) {
logDebug('downloadInceptionExport error: $e');
if (mounted) {
ToastHelper.showErrorToast(
context,
'Failed to generate inception export. Please try again.',
);
}
}
}
Widget _buildExportButton() {
final buttonStyle = ElevatedButton.styleFrom(
backgroundColor: const Color(0xFFE26728),
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
);
final textStyle = GoogleFonts.poppins(
fontSize: 16,
fontWeight: FontWeight.w700,
color: Colors.white,
letterSpacing: 1,
);
if (localTokenType == 'pre') {
return SizedBox(
width: 116,
height: 37,
child: PopupMenuButton<String>(
offset: const Offset(0, 37),
onSelected: (value) {
if (value == 'csv') {
exportToCsv(filteredData);
} else if (value == 'inception') {
downloadInceptionExport();
}
},
itemBuilder: (context) => [
PopupMenuItem(
value: 'csv',
child: Text(
'Export',
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('Export', style: textStyle),
),
),
),
);
}
return SizedBox(
width: 116,
height: 37,
child: ElevatedButton(
onPressed: () => exportToCsv(filteredData),
style: buttonStyle,
child: Text('Export', style: textStyle),
),
);
}
String _capitalize(String? value) {
if (value == null || value.isEmpty) return '';
return value[0].toUpperCase() + value.substring(1).toLowerCase();
}
String _getStatusDisplayLabel(String? status) {
final normalized = status?.toLowerCase().trim() ?? '';
if (localTokenType == 'pre' && _isEnrolledStatus(normalized)) {
return 'Submitted';
}
return _capitalize(status);
}
Future<void> _openPolicyTermsDialog() async {
setState(() => _isLoadingPolicyTerms = true);
try {
final clientId = localClientId ?? widget.ClientId;
final branchId = localClientBranchId ?? widget.clientBranchId;
final clientPolicyId = localClientPolicyId ?? widget.ClientPoliyId;
final token = localToken ?? widget.Token;
final hrId = await tokenService.readValue('empHrId');
if (hrId == null || hrId.toString().isEmpty) {
if (mounted) {
ToastHelper.showErrorToast(context, 'HR ID not found');
}
return;
}
final response = await apiService.getActiveCashDepositDetailsToApi(
clientId,
branchId,
hrId.toString(),
token,
1,
);
if (!mounted) return;
if (response['status'] != 'success') {
ToastHelper.showErrorToast(
context,
response['message']?.toString() ?? 'Failed to load policy terms',
);
return;
}
final policies = List<Map<String, dynamic>>.from(response['data'] ?? []);
Map<String, dynamic>? matchedPolicy;
for (final policy in policies) {
if (policy['client_policy_id'].toString() == clientPolicyId) {
matchedPolicy = policy;
break;
}
}
if (matchedPolicy == null) {
ToastHelper.showErrorToast(context, 'Policy not found');
return;
}
final rawTerms =
matchedPolicy['policy_terms'] ?? matchedPolicy['Policy_Terms'];
final terms = <String, String>{};
if (rawTerms is Map) {
rawTerms.forEach((key, value) {
if (value != null && value.toString().trim().isNotEmpty) {
terms[key.toString()] = value.toString();
}
});
}
if (terms.isEmpty) {
ToastHelper.showWarningToast(context, 'No policy terms available');
return;
}
await showDialog<void>(
context: context,
builder: (dialogContext) => _PolicyTermsDialog(terms: terms),
);
} catch (e) {
logDebug('Policy terms load failed: $e');
if (mounted) {
ToastHelper.showErrorToast(context, 'Failed to load policy terms');
}
} finally {
if (mounted) {
setState(() => _isLoadingPolicyTerms = false);
}
}
}
Future<void> _openReminderConfigDialog() async {
final clientId = localClientId ?? widget.ClientId;
final clientPolicyId = localClientPolicyId ?? widget.ClientPoliyId;
final clientBranchId = localClientBranchId ?? widget.clientBranchId;
final token = localToken ?? widget.Token;
await showDialog<void>(
context: context,
barrierDismissible: false,
builder: (dialogContext) {
return _ReminderMailConfigDialog(
clientId: clientId,
clientPolicyId: clientPolicyId,
clientBranchId: clientBranchId,
token: token,
apiService: apiService,
defaultSubject: _defaultReminderSubject(),
defaultHtmlBody: _defaultReminderHtmlBody(),
);
},
);
}
String _defaultReminderSubject() {
final policyName = localCardPolicyName ?? widget.cardPolicy_name;
return 'Reminder: Complete Your Enrollment - $policyName';
}
String _defaultReminderHtmlBody() {
return kDefaultEnrollmentReminderHtmlBody;
}
Future<void> _openReminderTemplateDialog() async {
final clientId = localClientId ?? widget.ClientId;
final clientPolicyId = localClientPolicyId ?? widget.ClientPoliyId;
final clientBranchId = localClientBranchId ?? widget.clientBranchId;
final token = localToken ?? widget.Token;
await showDialog<void>(
context: context,
barrierDismissible: false,
builder: (dialogContext) {
return ReminderEmailTemplateDialog(
clientId: clientId,
clientPolicyId: clientPolicyId,
clientBranchId: clientBranchId,
token: token,
apiService: apiService,
defaultSubject: _defaultReminderSubject(),
defaultHtmlBody: _defaultReminderHtmlBody(),
mode: ReminderEmailTemplateMode.send,
onSend: (subject, htmlBody) => _sendEnrollmentReminder(
emailSubject: subject,
emailBody: htmlBody,
),
);
},
);
}
Future<void> _sendEnrollmentReminder({
required String emailSubject,
required String emailBody,
}) async {
setState(() => _isSendingReminder = true);
try {
final hrId = await tokenService.readValue('enrollmentHrId');
final response = await apiService.sendReminderMailApi(
localClientId ?? widget.ClientId,
localClientPolicyId ?? widget.ClientPoliyId,
localClientBranchId ?? widget.clientBranchId,
hrId ?? '',
localToken ?? widget.Token,
emailSubject: emailSubject,
emailBody: emailBody,
);
final ok = response['status'] == 'success' || response['status'] == true;
if (ok) {
final message =
response['message']?.toString() ?? 'Reminder sent successfully';
ToastHelper.showSuccessToast(context, message);
await _logReminderActivity();
} else {
ToastHelper.showErrorToast(
context,
response['message']?.toString() ?? 'Failed to send reminder',
);
}
} catch (e) {
logDebug('Reminder exception: $e');
ToastHelper.showErrorToast(
context,
'Failed to send reminder. Please try again.',
);
} finally {
if (mounted) {
setState(() => _isSendingReminder = false);
}
}
}
Future<void> _logReminderActivity() async {
final postId = await tokenService.readValue('empHrId');
final preId = await tokenService.readValue('enrollmentEmpPrimaryId');
const activityPre = 'send_enrollment_reminder';
try {
await apiService.getPreLogHrActivity(
postId!,
preId!,
localToken!,
activityPre,
);
} catch (e) {
logDebug('Reminder activity log failed: $e');
}
}
Future<void> getEcardBulkDownload() async {
try {
final emp_policy_ids = selectedEmployeeIds.toList();
logDebug('10 $emp_policy_ids');
empHrId = await tokenService.readValue('empHrId');
final response = await apiService.getEcardBulkDownloadApi(
'', empHrId, emp_policy_ids, localToken!);
if (response['status'] == true) {
logDebug('Request success');
_showBulkDownloadSuccessPopup(response['message']);
} else {
ToastHelper.showErrorToast(context, response['message']);
logDebug('Request failed with status: ${response['code']}');
}
} catch (e) {
logDebug('Exception occurred: $e');
}
}
void _showBulkDownloadSuccessPopup(String message) {
showDialog(
context: context,
barrierDismissible: false,
builder: (context) {
return AlertDialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(
Icons.check_circle,
color: Color(0xFF009195),
size: 60,
),
const SizedBox(height: 16),
Text(
message,
textAlign: TextAlign.center,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
),
),
const SizedBox(height: 24),
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: () => Navigator.pop(context),
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF009195),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
child: Text(
'OK',
style: GoogleFonts.poppins(color: Colors.white),
),
),
),
],
),
);
},
);
}
@override
void dispose() {
_tabController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return BaseLayout(
child: _buildContent(context),
);
}
Widget _buildContent(BuildContext context) {
return Scaffold(
body: SafeArea(
child: Stack(
children: [
// ===================== MAIN CONTENT =====================
Column(
children: [
// ---------------- HEADER ----------------
Padding(
padding: const EdgeInsets.all(16),
child: Column(children: [
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
/// 🔙 Back + Title (LEFT)
Row(
children: [
IconButton(
tooltip: 'Previous Page',
onPressed: () async => {
await clearPolicyStorage(),
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => policies(),
),
)
},
// splashRadius: 20,
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(
"${localCardType} - ${localCardPolicyNo} " ??
'',
style: GoogleFonts.poppins(
color: Colors.black,
fontSize: 14,
fontWeight: FontWeight.w500,
),
),
Text(
localTokenType == 'pre'
? "${localCardPolicyName} (${localCardPolicyExpDate})"
: "${localCardInsurerName} - ${localCardPolicyName} (${localCardPolicyExpDate})",
style: GoogleFonts.poppins(
color: Colors.grey,
fontSize: 12,
fontWeight: FontWeight.w400,
),
),
],
),
),
],
),
/// Push right content to end
const Spacer(),
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),
if (localTokenType == 'pre') ...[
SizedBox(
width: 150,
height: 37,
child: ElevatedButton(
onPressed: _openReminderConfigDialog,
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF009195),
elevation: 0,
padding: EdgeInsets.zero,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
),
child: Text(
'Reminder Config',
maxLines: 1,
softWrap: false,
overflow: TextOverflow.ellipsis,
style: GoogleFonts.poppins(
fontSize: 13,
fontWeight: FontWeight.w700,
color: Colors.white,
),
),
),
),
const SizedBox(width: 12),
SizedBox(
width: 142,
height: 37,
child: ElevatedButton(
onPressed: _isSendingReminder
? null
: _openReminderTemplateDialog,
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFFE26728),
elevation: 0,
padding: EdgeInsets.zero,
disabledBackgroundColor:
const Color(0xFFE26728).withValues(alpha: 0.6),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
),
child: _isSendingReminder
? const SizedBox(
width: 18,
height: 18,
child: CircularProgressIndicator(
strokeWidth: 2,
color: Colors.white,
),
)
: Text(
'Reminder',
maxLines: 1,
softWrap: false,
style: GoogleFonts.poppins(
fontSize: 15,
fontWeight: FontWeight.w700,
color: Colors.white,
),
),
),
),
const SizedBox(width: 12),
],
if (localTokenType == 'post') ...[
SizedBox(
width: 130,
height: 37,
child: ElevatedButton(
onPressed: _isLoadingPolicyTerms
? null
: _openPolicyTermsDialog,
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF009195),
elevation: 0,
padding: EdgeInsets.zero,
disabledBackgroundColor:
const Color(0xFF009195).withValues(alpha: 0.6),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
),
child: _isLoadingPolicyTerms
? const SizedBox(
width: 18,
height: 18,
child: CircularProgressIndicator(
strokeWidth: 2,
color: Colors.white,
),
)
: Text(
'Policy Terms',
maxLines: 1,
softWrap: false,
overflow: TextOverflow.ellipsis,
style: GoogleFonts.poppins(
fontSize: 13,
fontWeight: FontWeight.w700,
color: Colors.white,
),
),
),
),
const SizedBox(width: 12),
],
SizedBox(
width: 116,
height: 37,
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"
? RouteSettings(name: 'preFileUpload')
: RouteSettings(name: 'postFileUpload'),
builder: (context) => localTokenType !=
"post"
? preFileUpload(
ClientId:
widget.ClientId, // <-- from map
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,
// Token: widget.Token,
// ClientId: widget.ClientId,
// ClientPolicyId : widget.ClientPoliyId,
// PolicyName: widget.cardPolicy_name,
// PolicyNo: widget.cardPolicyNo,
// ClientBranchId: widget.HrId,
// PolicyType: widget.cardType,
)
: postFileUpload(
ClientId:
widget.ClientId, // <-- from map
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',
// Token: localToken,
// ClientId: widget.ClientId,
// ClientPolicyId : widget.ClientPoliyId,
// 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
_buildExportButton(),
],
),
]),
),
const SizedBox(height: 12),
// ---------------- LOADER CONDITION ----------------
// This shows a linear progress bar if the site is fetching data
if (isLoading)
const Padding(
padding: EdgeInsets.symmetric(horizontal: 16),
child: LinearProgressIndicator(color: Color(0xFFE26728)),
),
// ---------------- PREMIUM / STATUS ----------------
if (localTokenType == "pre")
Padding(
padding: const EdgeInsets.only(left: 15, right: 16),
child: _buildStatusSummary(),
),
if (localTokenType == "post")
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Container(
padding: const EdgeInsets.symmetric(
horizontal: 12, vertical: 8),
decoration: BoxDecoration(
color: const Color(0xFFF9EBBD),
borderRadius: BorderRadius.circular(6),
),
child: Text(
'Premium - ₹${localTotalPremium}',
style: GoogleFonts.poppins(
fontSize: 12,
fontWeight: FontWeight.w600,
color: const Color(0xFF009195),
),
),
),
const Spacer(),
_buildCompactSearchField(),
],
),
),
const SizedBox(height: 12),
// ---------------- TABLE (SCROLLABLE) ----------------
Expanded(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: isLoading
? Container(
color: Colors
.transparent, // Semi-transparent background
child: Center(
child: // Your GIF loader widget
Image.asset(
height: 60,
width: 60,
'assets/nhance-loader.gif'), // Adjust path to your GIF loader
),
)
: Column(
children: [
_buildCDDataTable(
context), // ← DO NOT wrap again in Expanded
],
)),
),
const SizedBox(height: 48),
],
),
// ===================== FOOTER TEXT =====================
Positioned(
bottom: 12,
right: 16,
child: Text(
'(* Premium may vary subject to claims)',
style: GoogleFonts.poppins(
fontSize: 11,
color: Colors.red,
fontStyle: FontStyle.italic,
),
),
),
],
),
),
);
}
// Widget _buildContent(BuildContext context) {
// return Container(
// // padding: EdgeInsets.only(top: 30, bottom: 200, left: 50, right: 50),
// child: Column(
// children: [
// Row(
// crossAxisAlignment: CrossAxisAlignment.center,
// children: [
// /// 🔙 Back + Title (LEFT)
// Row(
// children: [
// IconButton(
// onPressed: () => {Navigator.pop(context)},
// icon: const Icon(
// Icons.arrow_back_ios,
// size: 18,
// color: Colors.black,
// ),
// padding: EdgeInsets.zero,
// constraints: const BoxConstraints(),
// ),
// const SizedBox(width: 6),
// Container(
// // color: Colors.redAccent.shade100,
// child: Column(
// crossAxisAlignment: CrossAxisAlignment.start,
// mainAxisAlignment: MainAxisAlignment.start,
// children: [
// Text(
// "${widget.cardType} - ${widget.cardPolicyNo} " ??
// '',
// style: GoogleFonts.poppins(
// color: Colors.black,
// fontSize: 14,
// fontWeight: FontWeight.w500,
// ),
// ),
// Text(
// localTokenType == 'pre'
// ? "${widget.cardPolicy_name} (${widget.cardPolicy_ExpDate})"
// : "${widget.cardInsurer_name} - ${widget.cardPolicy_name} (${widget.cardPolicy_ExpDate})",
// style: GoogleFonts.poppins(
// color: Colors.grey,
// fontSize: 12,
// fontWeight: FontWeight.w400,
// ),
// ),
// ],
// ),
// ),
// ],
// ),
//
// /// Push right content to end
// const Spacer(),
//
// /// 🔍 Search Box
// Container(
// width: 380,
// height: 37,
// decoration: BoxDecoration(
// color: const Color(0xFFF0F0F0),
// borderRadius: BorderRadius.circular(8),
// ),
// child: TextField(
// controller: searchController,
// onChanged: search,
// style: GoogleFonts.poppins(fontSize: 14),
// decoration: const InputDecoration(
// hintText: 'Search',
// prefixIcon: Icon(Icons.search, size: 18),
// border: InputBorder.none,
// contentPadding:
// EdgeInsets.symmetric(horizontal: 12, vertical: 8),
// ),
// ),
// ),
//
// if (localIsEcardBulkDownload == 1) ...[
// const SizedBox(width: 12),
// SizedBox(
// width: 40,
// height: 37,
// child: ElevatedButton(
// onPressed: () {
// getEcardBulkDownload();
// },
// style: ElevatedButton.styleFrom(
// backgroundColor: const Color(0xFFE26728),
// elevation: 0,
// padding: EdgeInsets.zero, // ✅ IMPORTANT
// alignment: Alignment.center, // ✅ FORCE CENTER
// shape: RoundedRectangleBorder(
// borderRadius: BorderRadius.circular(10),
// ),
// ),
// child: Icon(
// Icons.credit_card,
// size: 18,
// color: Colors.white,
// )),
// ),
// ],
//
// const SizedBox(width: 12),
//
// SizedBox(
// width: 116,
// height: 37,
// child: ElevatedButton(
// onPressed: () {
// Navigator.push(
// context,
// MaterialPageRoute(
// builder: (context) => localTokenType != "post"
// ? preFileUpload(
// ClientId:
// localClientId, // <-- from map
// policyTypeId: localPolicyTypeId,
// ClientPoliyId: localClientPolicyId,
// clientBranchId: widget.clientBranchId,
// Token: widget.Token,
// TokenType: localTokenType,
// cardType: widget.cardType,
// cardPolicyNo: widget.cardPolicyNo,
// cardInsurer_name:
// widget.cardInsurer_name,
// cardPolicy_name: widget.cardPolicy_name,
// cardPolicy_ExpDate:
// widget.cardPolicy_ExpDate,
// total_premium: widget.total_premium,
//
// // Token: widget.Token,
// // ClientId: localClientId,
// // ClientPolicyId : localClientPolicyId,
// // PolicyName: widget.cardPolicy_name,
// // PolicyNo: widget.cardPolicyNo,
// // ClientBranchId: widget.HrId,
// // PolicyType: widget.cardType,
// )
// : postFileUpload(
// ClientId:
// localClientId, // <-- from map
// policyTypeId: localPolicyTypeId,
// ClientPoliyId: localClientPolicyId,
// clientBranchId: widget.clientBranchId,
// Token: widget.Token,
// TokenType: localTokenType,
// cardType: widget.cardType,
// cardPolicyNo: widget.cardPolicyNo,
// cardInsurer_name:
// widget.cardInsurer_name,
// cardPolicy_name: widget.cardPolicy_name,
// cardPolicy_ExpDate:
// widget.cardPolicy_ExpDate,
// total_premium: widget.total_premium,
//
// // Token: widget.Token,
// // ClientId: localClientId,
// // ClientPolicyId : localClientPolicyId,
// // PolicyName: widget.cardPolicy_name,
// // PolicyNo: widget.cardPolicyNo,
// // ClientBranchId: widget.HrId,
// // PolicyType: widget.cardType,
// )),
// );
// },
// style: ElevatedButton.styleFrom(
// backgroundColor: const Color(0xFFE26728),
// elevation: 0,
// shape: RoundedRectangleBorder(
// borderRadius: BorderRadius.circular(10),
// ),
// ),
// child: Text(
// 'Import',
// style: GoogleFonts.poppins(
// fontSize: 16,
// fontWeight: FontWeight.w700,
// color: Colors.white,
// letterSpacing: 1,
// ),
// ),
// ),
// ),
// const SizedBox(width: 12),
//
// /// ⬇️ Export Button
// SizedBox(
// width: 116,
// height: 37,
// child: ElevatedButton(
// onPressed: () {
// exportToCsv(filteredData);
// },
// style: ElevatedButton.styleFrom(
// backgroundColor: const Color(0xFFE26728),
// elevation: 0,
// shape: RoundedRectangleBorder(
// borderRadius: BorderRadius.circular(10),
// ),
// ),
// child: Text(
// 'Export',
// style: GoogleFonts.poppins(
// fontSize: 16,
// fontWeight: FontWeight.w700,
// color: Colors.white,
// letterSpacing: 1,
// ),
// ),
// ),
// ),
// ],
// ),
// SizedBox(height: 20),
// if (localTokenType == "pre") ...[_buildStatusSummary()],
// if (localTokenType == "post") ...[
// Row(
// crossAxisAlignment: CrossAxisAlignment.start,
// children: [
// Container(
// padding: const EdgeInsets.symmetric(
// horizontal: 12, vertical: 8),
// decoration: BoxDecoration(
// color: Color(0xFFF9EBBD),
// borderRadius: BorderRadius.circular(6),
// ),
// child: Text(
// 'Premium - ₹${widget.total_premium ?? ''}*',
// style: GoogleFonts.poppins(
// fontSize: 12,
// fontWeight: FontWeight.w600,
// color: Color(0xFF009195),
// ),
// ),
// )
// ],
// ),
// ],
// SizedBox(height: 20),
// Container(
// // decoration: BoxDecoration(
// // color: Colors.white,
// // borderRadius:
// // BorderRadius.circular(10), // 👈 set your desired radius
// // ),
// // height: 400,
// // margin: const EdgeInsets.only(left: 40.0, right: 40.0),
// // padding: const EdgeInsets.all(16.0),
// child: Column(
// children: [
// Row(
// children: [
// Expanded(
// child: Container(
// child: SingleChildScrollView(
// scrollDirection: Axis.vertical,
// child: _buildCDDataTable(context),
// ),
// ),
// )
// ],
// ),
// ],
// ),
// ),
// Positioned(
// bottom: 12,
// right: 16,
// child: Text(
// '(* Premium may vary subject to claims)',
// textAlign: TextAlign.right,
// style: GoogleFonts.poppins(
// fontSize: 11,
// color: Colors.red,
// fontStyle: FontStyle.italic,
// ),
// ),
// ),
// ],
// ),
// );
// }
Map<String, int> getPreStatusCounts() {
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 (_isEnrolledStatus(status)) {
enrolled++;
} else {
notEnrolled++;
}
if (_isSelfRow(item)) {
if (_isLoggedInRow(item)) {
loggedIn++;
} else {
notLoggedIn++;
}
}
if (status == 'draft') 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) {
case 'emp_count':
return const Color(0xFFE2FBCB);
case 'enrolled':
return const Color(0xFFBDF9D9);
case 'not_enrolled':
return const Color(0xFFFFE8AC);
case 'logged_in':
return const Color(0xFFC5F2F4);
case 'not_logged_in':
return const Color(0xFFE8EAF6);
case 'draft':
return const Color(0xFFF9EBBD);
default:
return const Color(0xFFB0BEC5);
}
}
Widget _buildCompactSearchField({double width = 180}) {
return SizedBox(
width: width,
height: 32,
child: Container(
decoration: BoxDecoration(
color: const Color(0xFFF0F0F0),
borderRadius: BorderRadius.circular(6),
),
child: TextField(
controller: searchController,
onChanged: search,
textAlign: TextAlign.left,
textAlignVertical: TextAlignVertical.center,
style: GoogleFonts.poppins(fontSize: 12),
decoration: InputDecoration(
hintText: 'Search',
hintStyle: GoogleFonts.poppins(fontSize: 12, color: Colors.black45),
prefixIcon: const Icon(Icons.search, size: 16),
prefixIconConstraints:
const BoxConstraints(minWidth: 28, minHeight: 32),
isDense: true,
border: InputBorder.none,
contentPadding:
const EdgeInsets.only(left: 0, right: 10, top: 0, bottom: 0),
),
),
),
);
}
Widget _buildPreStatusFilterChip({
required String filterKey,
required String label,
required int count,
}) {
final selected = _preStatusFilter == filterKey;
final color = _getPreFilterChipColor(filterKey);
return Material(
color: Colors.transparent,
child: InkWell(
onTap: () => _onPreStatusFilterTap(filterKey),
borderRadius: BorderRadius.circular(6),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
decoration: BoxDecoration(
color: color,
borderRadius: BorderRadius.circular(6),
border: Border.all(
color: selected ? const Color(0xFF009195) : Colors.transparent,
width: 2,
),
),
child: Text(
'$label - $count',
style: GoogleFonts.poppins(
fontSize: 12,
fontWeight: selected ? FontWeight.w700 : FontWeight.w600,
color: Colors.black,
),
),
),
),
);
}
Widget _buildStatusSummary() {
if (originalData.isEmpty) {
return const SizedBox();
}
final statusCounts = getPreStatusCounts();
const filters = <Map<String, String>>[
{'key': 'emp_count', 'label': 'Emp Count'},
{'key': 'enrolled', 'label': 'Submitted'},
{'key': 'logged_in', 'label': 'Logged-In'},
{'key': 'not_logged_in', 'label': 'Not Logged-In'},
{'key': 'draft', 'label': 'Draft'},
];
return SizedBox(
height: 38,
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Expanded(
child: ListView.separated(
scrollDirection: Axis.horizontal,
itemCount: filters.length + 1,
separatorBuilder: (_, __) => const SizedBox(width: 10),
itemBuilder: (context, index) {
if (index == filters.length) {
return Material(
color: Colors.transparent,
child: InkWell(
onTap:
_preStatusFilter == null ? null : _resetPreStatusFilter,
borderRadius: BorderRadius.circular(6),
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 8,
),
decoration: BoxDecoration(
color: const Color(0xFFF0F0F0),
borderRadius: BorderRadius.circular(6),
border: Border.all(
color: _preStatusFilter != null
? const Color(0xFF009195)
: const Color(0xFFD0D0D0),
),
),
child: Text(
'Reset',
style: GoogleFonts.poppins(
fontSize: 12,
fontWeight: FontWeight.w600,
color: _preStatusFilter != null
? const Color(0xFF009195)
: Colors.black54,
),
),
),
),
);
}
final filter = filters[index];
return _buildPreStatusFilterChip(
filterKey: filter['key']!,
label: filter['label']!,
count: statusCounts[filter['key']] ?? 0,
);
},
),
),
const SizedBox(width: 12),
_buildCompactSearchField(),
],
),
);
}
Widget _buildCDDataTable(BuildContext context) {
if (filteredData.isEmpty) {
return const Center(child: Text('No data available'));
}
return Expanded(
// ✅ VERY IMPORTANT
child: CustomScrollView(
slivers: [
/// 🔒 Sticky Header
SliverPersistentHeader(
pinned: true,
delegate: _CDHeaderDelegate(
showUHID: localPolicyTypeId != '6' && localPolicyTypeId != '7',
showLoggedIn: localTokenType == "pre",
showAction:
localTokenType != "pre" && (hasAnyEcardLink || hasModule),
),
),
/// 📄 Rows
SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) {
final item = _paginatedData[index];
return _buildCDRow(item);
},
childCount: _paginatedData.length,
),
),
/// 📌 Pagination
SliverToBoxAdapter(
child: _buildPagination(context),
),
],
),
);
}
Widget _buildCDRow(Map<String, dynamic> item) {
return Container(
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16),
margin: const EdgeInsets.only(top: 6),
decoration: const BoxDecoration(
border: Border(
bottom: BorderSide(color: Color(0xFFA9D9DE)),
),
),
child: Row(
children: [
/// NAME
Expanded(
flex: 3,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(item['name'] ?? '-', style: _dataBold),
Text(item['emp_code'] ?? '-', style: _dataSub),
],
),
),
/// UHID
if (localPolicyTypeId != '6' && localPolicyTypeId != '7')
Expanded(
flex: 2,
child: Text(item['uhid'] ?? '-', style: _dataBold),
),
Expanded(
flex: 2,
child: Text(item['relationship'] ?? '-', style: _dataBold),
),
Expanded(
flex: 2,
child: Text(item['formatted_dob']?.replaceAll("/", "-") ?? '-',
style: _dataBold),
),
Expanded(
flex: 2,
child: Text(item['gender'] ?? '-', style: _dataBold),
),
Expanded(
flex: 2,
child: Text(item['mobile'] ?? '-', style: _dataBold),
),
Expanded(
flex: 5,
child: Text(item['email_corporate'] ?? '-', style: _dataBold),
),
if (localTokenType == "pre")
Expanded(
flex: 2,
child: _isSelfRow(item)
? Container(
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 4,
),
decoration: BoxDecoration(
color: _getLoggedInChipColor(item),
borderRadius: BorderRadius.circular(10),
),
child: Text(
_getLoggedInDisplayLabel(item),
textAlign: TextAlign.center,
style: _dataBold,
),
)
: Text('-', style: _dataBold),
),
if (localTokenType == "pre") const SizedBox(width: 8),
/// STATUS
Expanded(
flex: 2,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: getStatusColor(_getRowStatusRaw(item) ?? ''),
borderRadius: BorderRadius.circular(10),
),
child: Text(
_getStatusDisplayLabel(_getRowStatusRaw(item)),
textAlign: TextAlign.center,
style: _dataBold,
),
),
),
/// ACTION
if (localTokenType != "pre" && (hasAnyEcardLink || hasModule))
Expanded(
flex: 3,
child: Builder(
builder: (context) {
final isSelf = item['relationship'] == 'Self';
final hasEcard = item['ecard_download_link'] != null;
final showEcard = isSelf && hasEcard;
final showClaim = localTokenType == "post" && hasModule;
if (!showEcard && !showClaim) {
return SizedBox(); // No icon to show
}
return SizedBox(
height: 40,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
/// --- eCard Menu (Fixed Space) ---
SizedBox(
width: 40,
height: 40,
child: Visibility(
visible: showEcard,
maintainSize: true,
maintainAnimation: true,
maintainState: true,
child: PopupMenuButton<String>(
tooltip: 'Ecard',
offset: const Offset(0, 40),
onSelected: (value) {
switch (value) {
case 'download':
getEcardDownload(
item['emp_code'],
item['employee_id'],
item['client_policy_id'],
item['policy_no'],
);
break;
case 'email':
sendEcardViaEmail(
item['id'],
item['client_policy_id'],
);
break;
case 'copy':
copyEcardLink(
item['ecard_download_link']?.toString(),
);
break;
}
},
itemBuilder: (context) => [
PopupMenuItem(
value: 'download',
child: Text(
'Download Ecard',
style: GoogleFonts.poppins(fontSize: 13),
),
),
if (isSelf)
PopupMenuItem(
value: 'email',
child: Text(
'Send E-card via email',
style: GoogleFonts.poppins(fontSize: 13),
),
),
PopupMenuItem(
value: 'copy',
child: Text(
'Copy link',
style: GoogleFonts.poppins(fontSize: 13),
),
),
],
child: MouseRegion(
cursor: SystemMouseCursors.click,
child: Container(
decoration: BoxDecoration(
color: const Color(0xFFE6F5F6),
borderRadius: BorderRadius.circular(8),
),
padding: const EdgeInsets.all(6),
child: Image.asset(
'assets/credit_card.png',
fit: BoxFit.contain,
),
),
),
),
),
),
const SizedBox(width: 8),
/// --- Claim Button (Fixed Space) ---
SizedBox(
width: 40,
height: 40,
child: Visibility(
visible: showClaim,
maintainSize: true,
maintainAnimation: true,
maintainState: true,
child: Tooltip(
message: 'View Claims',
child: MouseRegion(
cursor: SystemMouseCursors.click,
child: GestureDetector(
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => ClaimsPolicies(
empCode: item['emp_code']!,
),
),
);
},
child: Container(
decoration: BoxDecoration(
color: const Color(0xFFE6F5F6),
borderRadius: BorderRadius.circular(8),
),
padding: const EdgeInsets.all(6),
child: Image.asset(
'assets/claim.png',
fit: BoxFit.contain,
),
),
),
),
),
),
),
],
),
);
},
),
),
],
),
);
}
// Widget _buildCDDataTable(BuildContext context) {
// if (_paginatedData.isNotEmpty) {
// hasAnyEcardLink = _paginatedData.any(
// (item) => item['ecard_download_link'] != null,
// );
//
// if (localTokenType == "post" && hasModule) {
// logDebug("paginatTtt - $_paginatedData");
//
// logDebug("📥 Any e-card link present: $hasAnyEcardLink");
// }
// }
//
// logDebug('widgetpolicyTypeId');
// logDebug(localPolicyTypeId);
//
// if (filteredData.isEmpty) {
// return SizedBox(
// // height: 50,
// child: Center(
// child: Text(
// 'No data is available for the selected policy',
// style: GoogleFonts.poppins(
// color: Colors.grey,
// fontWeight: FontWeight.w400,
// ),
// )),
// );
// }
//
// final currentPageIds = _paginatedData
// .map((e) => e['id']?.toString())
// .whereType<String>()
// .toList();
//
// return Container(
// child: Column(
// crossAxisAlignment: CrossAxisAlignment.start,
// children: [
// // Header row
// Container(
// decoration: BoxDecoration(
// color: Color(0xFFD7E9EB),
// borderRadius: BorderRadius.circular(6),
// ),
// padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16),
// child: Row(
// children: [
// if (localTokenType == 'post' &&
// localIsEcardBulkDownload == 1)
// SizedBox(
// width: 40,
// child: Checkbox(
// value: currentPageIds.isNotEmpty &&
// currentPageIds.every(selectedEmployeeIds.contains),
// onChanged: (checked) {
// setState(() {
// if (checked == true) {
// selectedEmployeeIds.addAll(currentPageIds);
// } else {
// selectedEmployeeIds.removeAll(currentPageIds);
// }
// });
// },
// ),
// ),
// Expanded(
// flex: 3,
// child: Text(
// 'Name',
// style: GoogleFonts.poppins(
// color: Colors.black,
// fontWeight: FontWeight.w600,
// ),
// ),
// ),
// if (localPolicyTypeId != '6' && localPolicyTypeId != '7')
// Expanded(
// flex: 2,
// child: Text(
// 'UHID',
// style: GoogleFonts.poppins(
// color: Colors.black,
// fontWeight: FontWeight.w600,
// ),
// ),
// ),
// Expanded(
// flex: 2,
// child: Text(
// 'Relationship',
// style: GoogleFonts.poppins(
// color: Colors.black,
// fontWeight: FontWeight.w600,
// ),
// ),
// ),
// Expanded(
// flex: 2,
// child: Text(
// 'Date Of Birth',
// style: GoogleFonts.poppins(
// color: Colors.black,
// fontWeight: FontWeight.w600,
// ),
// ),
// ),
// Expanded(
// flex: 2,
// child: Text(
// 'Gender',
// style: GoogleFonts.poppins(
// color: Colors.black,
// fontWeight: FontWeight.w600,
// ),
// ),
// ),
// Expanded(
// flex: 2,
// child: Text(
// 'Mobile',
// style: GoogleFonts.poppins(
// color: Colors.black,
// fontWeight: FontWeight.w600,
// ),
// ),
// ),
// Expanded(
// flex: 5,
// child: Text(
// 'Email',
// style: GoogleFonts.poppins(
// color: Colors.black,
// fontWeight: FontWeight.w600,
// ),
// ),
// ),
// Expanded(
// flex: 2,
// child: Text(
// 'Status',
// style: GoogleFonts.poppins(
// color: Colors.black,
// fontWeight: FontWeight.w600,
// ),
// ),
// ),
// if (localTokenType != "pre" && (hasAnyEcardLink || hasModule))
// Expanded(
// flex: 3,
// child: Text(
// 'Action',
// textAlign: TextAlign.center,
// style: GoogleFonts.poppins(
// color: Colors.black,
// fontWeight: FontWeight.w600,
// ),
// ),
// ),
// ],
// ),
// ),
//
// const SizedBox(height: 6),
//
// SingleChildScrollView(
// scrollDirection: Axis.vertical,
// child: Column(
// crossAxisAlignment: CrossAxisAlignment.start,
// children: _paginatedData.mapIndexed((index, item) {
// return Container(
// // margin: const EdgeInsets.only(bottom: 8),
// padding:
// const EdgeInsets.symmetric(vertical: 5, horizontal: 16),
// decoration: BoxDecoration(
// // color: Colors.white,
// border: Border(
// bottom: BorderSide(
// // color: Color(0xFFA1A1A1),
// color: Color(0xFFA9D9DE),
// width: 1,
// ),
// ),
// // color: index % 2 == 0 ? Color(0xFFE6FAFB) : Colors.white,
// // borderRadius: BorderRadius.circular(6),
// ),
// child: Row(
// children: [
// if (localTokenType == 'post' &&
// localIsEcardBulkDownload == 1)
// SizedBox(
// width: 40,
// child: Checkbox(
// value: selectedEmployeeIds
// .contains(item['id']?.toString()),
// onChanged: (checked) {
// setState(() {
// final id = item['id']?.toString();
// if (id == null) return;
//
// if (checked == true) {
// selectedEmployeeIds.add(id);
// } else {
// selectedEmployeeIds.remove(id);
// }
// });
// },
// ),
// ),
//
// Expanded(
// flex: 3,
// child: Column(
// mainAxisAlignment: MainAxisAlignment.start,
// crossAxisAlignment: CrossAxisAlignment.start,
// children: [
// Text(
// item['name'] ?? '-',
// style: GoogleFonts.poppins(
// color: Color(0xFF000000),
// fontWeight: FontWeight.w400,
// fontSize: 12),
// ),
// Text(
// item['emp_code'] ?? '-',
// style: GoogleFonts.poppins(
// color: Color(0xFF585757),
// fontWeight: FontWeight.w300,
// fontSize: 10),
// ),
// ],
// ),
// ),
// if (localPolicyTypeId != '6' &&
// localPolicyTypeId != '7')
// Expanded(
// flex: 2,
// child: Text(
// item['uhid'] ?? '-',
// style: GoogleFonts.poppins(
// color: Color(0xFF000000),
// fontWeight: FontWeight.w400,
// fontSize: 12),
// ),
// ),
// Expanded(
// flex: 2,
// child: Text(
// "${item['relationship'] ?? ''}",
// style: GoogleFonts.poppins(
// color: Color(0xFF000000),
// fontWeight: FontWeight.w400,
// fontSize: 12),
// ),
// ),
// Expanded(
// flex: 2,
// child: Text(
// "${item['formatted_dob'].replaceAll("/", "-") ?? ''}",
// style: GoogleFonts.poppins(
// color: Color(0xFF000000),
// fontWeight: FontWeight.w400,
// fontSize: 12),
// ),
// ),
// Expanded(
// flex: 2,
// child: Text(
// "${item['gender'] ?? ''}",
// style: GoogleFonts.poppins(
// color: Color(0xFF000000),
// fontWeight: FontWeight.w400,
// fontSize: 12),
// ),
// ),
// Expanded(
// flex: 2,
// child: Text(
// "${item['mobile'] ?? ''}",
// style: GoogleFonts.poppins(
// color: Color(0xFF000000),
// fontWeight: FontWeight.w400,
// fontSize: 12),
// ),
// ),
// Expanded(
// flex: 5,
// child: Text(
// "${item['email_corporate'] ?? ''}",
// style: GoogleFonts.poppins(
// color: Color(0xFF000000),
// fontWeight: FontWeight.w400,
// fontSize: 12),
// ),
// ),
// Expanded(
// flex: 2,
// child: Container(
// padding: const EdgeInsets.symmetric(
// horizontal: 4, vertical: 4),
// decoration: BoxDecoration(
// color: getStatusColor(item['status'] ?? ''),
// // color: (item['emp_is_active'] == "1")
// // ? Color(0xFF7BD9B6)
// // : Color(0xFFFFA6A6),
// borderRadius: BorderRadius.circular(10),
// ),
// child: Align(
// alignment: Alignment.center,
// child: Text(
// _capitalize(item['status']),
// style: GoogleFonts.poppins(
// color: Color(0xFF000000),
// fontWeight: FontWeight.w500,
// fontSize: 12,
// ),
// ),
// ),
// ),
// ),
//
// if (localTokenType != "pre" &&
// (hasAnyEcardLink || hasModule))
// Expanded(
// flex: 3,
// child: Builder(
// builder: (context) {
// final isSelf = item['relationship'] == 'Self';
// final hasEcard = item['ecard_download_link'] != null;
// final showEcard = isSelf && hasEcard;
// final showClaim = localTokenType == "post" && hasModule;
//
// if (!showEcard && !showClaim) {
// return SizedBox(); // No icon to show
// }
//
// return SizedBox(
// height: 40,
// child: Row(
// mainAxisAlignment: MainAxisAlignment.center,
// crossAxisAlignment: CrossAxisAlignment.center,
// children: [
//
// /// --- eCard Button (Fixed Space) ---
// SizedBox(
// width: 40,
// height: 40,
// child: Visibility(
// visible: showEcard,
// maintainSize: true,
// maintainAnimation: true,
// maintainState: true,
// child: Tooltip(
// message: 'Download e-Card',
// child: MouseRegion(
// cursor: SystemMouseCursors.click,
// child: GestureDetector(
// onTap: () {
// getEcardDownload(
// item['emp_code'],
// item['employee_id'],
// item['client_policy_id'],
// item['policy_no'],
// );
// },
// child: Container(
// decoration: BoxDecoration(
// color: const Color(0xFFE6F5F6),
// borderRadius: BorderRadius.circular(8),
// ),
// padding: const EdgeInsets.all(6),
// child: Image.asset(
// 'assets/credit_card.png',
// fit: BoxFit.contain,
// ),
// ),
// ),
// ),
// ),
// ),
// ),
//
// const SizedBox(width: 8),
//
// /// --- Claim Button (Fixed Space) ---
// SizedBox(
// width: 40,
// height: 40,
// child: Visibility(
// visible: showClaim,
// maintainSize: true,
// maintainAnimation: true,
// maintainState: true,
// child: Tooltip(
// message: 'View Claims',
// child: MouseRegion(
// cursor: SystemMouseCursors.click,
// child: GestureDetector(
// onTap: () {
// Navigator.push(
// context,
// MaterialPageRoute(
// builder: (context) => ClaimsPolicies(
// empCode: item['emp_code']!,
// ),
// ),
// );
// },
// child: Container(
// decoration: BoxDecoration(
// color: const Color(0xFFE6F5F6),
// borderRadius: BorderRadius.circular(8),
// ),
// padding: const EdgeInsets.all(6),
// child: Image.asset(
// 'assets/claim.png',
// fit: BoxFit.contain,
// ),
// ),
// ),
// ),
// ),
// ),
// ),
// ],
// ),
// );
// },
// ),
// ),
//
// // (hasAnyEcardLink || hasModule))
// // Expanded(
// // flex: 3,
// // child: Row(
// // mainAxisAlignment: MainAxisAlignment.center,
// // children: [
// // if (item['ecard_download_link'] != null)
// // GestureDetector(
// // onTap: () {
// // _launchURL(item['ecard_download_link']);
// // },
// // child: Container(
// // height: 35,
// // width: 35,
// // decoration: BoxDecoration(
// // color: Color(0xFFE6F5F6),
// // borderRadius: BorderRadius.circular(8),
// // ),
// // child: Padding(
// // padding: EdgeInsets.all(
// // 6), // You can adjust this value
// // child: Image.asset(
// // 'assets/credit_card.png',
// // fit: BoxFit.contain,
// // ),
// // ),
// // ),
// // ),
// // SizedBox(
// // width: 10,
// // ),
// // if (localTokenType == "post" && hasModule)
// // GestureDetector(
// // onTap: () {
// // setState(() {
// // logDebug(
// // "policytabdata - ${item['emp_code']!}");
// // Navigator.push(
// // context,
// // MaterialPageRoute(
// // builder: (context) => hrDashboard(
// // selectedIndex: 3,
// // empCodeFromHrPolicy:
// // item['emp_code']!,
// // isHrcode: 1,
// // ),
// // ),
// // );
// // });
// // },
// // child: Container(
// // height: 35,
// // width: 35,
// // decoration: BoxDecoration(
// // color: Color(0xFFE6F5F6),
// // borderRadius: BorderRadius.circular(8),
// // ),
// // child: Padding(
// // padding: EdgeInsets.all(
// // 6), // You can adjust this value
// // child: Image.asset(
// // 'assets/claim.png',
// // fit: BoxFit.contain,
// // ),
// // ),
// // ),
// // ),
// // ],
// // ),
// // ),
// ],
// ),
// );
// }).toList(),
// ),
// ),
// // ),
// _buildPagination(context)
// // Row(
// // mainAxisAlignment: MainAxisAlignment.end,
// // children: [
// // Padding(
// // padding: const EdgeInsets.symmetric(vertical: 12),
// // child: Row(
// // mainAxisAlignment: MainAxisAlignment.center,
// // children: [
// // DropdownButton<int>(
// // value: _rowsPerPage,
// // items: [5, 10, 15, 20, 50].map((int value) {
// // return DropdownMenuItem<int>(
// // value: value,
// // child: Text(
// // ' $value ',
// // style: GoogleFonts.poppins(fontSize: 15),
// // ),
// // );
// // }).toList(),
// // onChanged: (newValue) {
// // setState(() {
// // _rowsPerPage = newValue!;
// // _currentPage =
// // 1; // Reset to first page when rows per page changes
// // });
// // },
// // ),
// // IconButton(
// // onPressed: _currentPage > 1
// // ? () {
// // setState(() {
// // _currentPage--;
// // });
// // }
// // : null,
// // icon: Icon(Icons.chevron_left),
// // ),
// // for (int i = 1;
// // i <= (filteredData.length / _rowsPerPage).ceil();
// // i++)
// // Padding(
// // padding: const EdgeInsets.symmetric(horizontal: 4),
// // child: ElevatedButton(
// // style: ElevatedButton.styleFrom(
// // backgroundColor: _currentPage == i
// // ? Color(0xFF00A6A6)
// // : Colors.grey[300],
// // foregroundColor:
// // _currentPage == i ? Colors.white : Colors.black,
// // minimumSize: Size(36, 36),
// // padding: EdgeInsets.zero,
// // ),
// // onPressed: () {
// // setState(() {
// // _currentPage = i;
// // });
// // },
// // child: Text(i.toString()),
// // ),
// // ),
// // IconButton(
// // onPressed: _currentPage <
// // (filteredData.length / _rowsPerPage).ceil()
// // ? () {
// // setState(() {
// // _currentPage++;
// // });
// // }
// // : null,
// // icon: Icon(Icons.chevron_right),
// // ),
// // ],
// // ),
// // ),
// // ],
// // ),
// ],
// ),
// );
// }
Widget _buildPagination(BuildContext context) {
// 1. Calculate the range of entries being shown
final totalItems = filteredData.length;
final int startEntry =
totalItems == 0 ? 0 : ((_currentPage - 1) * _rowsPerPage) + 1;
int endEntry = _currentPage * _rowsPerPage;
if (endEntry > totalItems) endEntry = totalItems;
final totalPages = (filteredData.length / _rowsPerPage).ceil();
const visiblePageCount = 5;
List<int> getVisiblePages() {
if (totalPages <= visiblePageCount) {
return List.generate(totalPages, (i) => i + 1);
}
if (_currentPage <= 3) {
return [1, 2, 3, 4, 5];
}
if (_currentPage >= totalPages - 2) {
return [
totalPages - 4,
totalPages - 3,
totalPages - 2,
totalPages - 1,
totalPages
];
}
return [
_currentPage - 2,
_currentPage - 1,
_currentPage,
_currentPage + 1,
_currentPage + 2,
];
}
List<int> visiblePages = getVisiblePages();
return Padding(
// Match this horizontal padding (16) to your Table Header padding for perfect alignment
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
child: Row(
mainAxisAlignment: MainAxisAlignment
.spaceBetween, // Pushes text to left, buttons to right
children: [
// --- LEFT SIDE: Showing Text ---
Text(
"Showing $startEntry to $endEntry of $totalItems entries",
style: GoogleFonts.poppins(
fontSize: 13,
color: const Color(0xFF585757),
fontWeight: FontWeight.w400,
),
),
// --- RIGHT SIDE: Controls ---
Row(
children: [
// Dropdown for rows per page
DropdownButton<int>(
value: _rowsPerPage,
focusColor: Colors
.transparent, // Fix: Removes the grey/blue highlight on change
items: [5, 10, 15, 20, 50].map((int value) {
return DropdownMenuItem<int>(
value: value,
child: Text(' $value ',
style: GoogleFonts.poppins(fontSize: 15)),
);
}).toList(),
onChanged: (newValue) {
setState(() {
_rowsPerPage = newValue!;
_currentPage = 1;
});
},
),
const SizedBox(width: 8),
// Previous button
IconButton(
tooltip: 'Previous Page',
onPressed: _currentPage > 1
? () => setState(() => _currentPage--)
: null,
icon: const Icon(Icons.chevron_left),
),
// First page + left ellipsis
if (!visiblePages.contains(1))
Row(children: [
_buildPageButton(1),
const Padding(
padding: EdgeInsets.symmetric(horizontal: 4),
child: Text("..."),
),
]),
// Visible page buttons
for (int page in visiblePages) _buildPageButton(page),
// Right ellipsis + last page
if (!visiblePages.contains(totalPages) && totalPages > 0)
Row(children: [
const Padding(
padding: EdgeInsets.symmetric(horizontal: 4),
child: Text("..."),
),
_buildPageButton(totalPages),
]),
// Next button
IconButton(
onPressed: _currentPage < totalPages
? () => setState(() => _currentPage++)
: null,
icon: const Icon(Icons.chevron_right),
),
],
),
],
),
);
}
Widget _buildPageButton(int page) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 4),
child: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor:
_currentPage == page ? const Color(0xFF00A6A6) : Colors.grey[300],
foregroundColor: _currentPage == page ? Colors.white : Colors.black,
minimumSize: const Size(36, 36),
padding: EdgeInsets.zero,
),
onPressed: () {
setState(() {
_currentPage = page;
});
},
child: Text(page.toString()),
),
);
}
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 bool showLoggedIn;
final bool showAction;
_CDHeaderDelegate({
required this.showUHID,
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(0xFFD7E9EB),
padding: const EdgeInsets.symmetric(horizontal: 16),
alignment: Alignment.centerLeft,
child: Row(
children: [
_headerCell('Name', 3),
if (showUHID) _headerCell('UHID', 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 _PolicyTermsDialog extends StatelessWidget {
final Map<String, String> terms;
const _PolicyTermsDialog({required this.terms});
@override
Widget build(BuildContext context) {
final entries = terms.entries.toList();
final maxHeight = MediaQuery.of(context).size.height * 0.65;
final cappedMaxHeight = maxHeight.clamp(320.0, 560.0);
final contentHeight =
(entries.length * 88.0).clamp(120.0, cappedMaxHeight);
return AlertDialog(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
title: Text(
'Policy Terms',
style: GoogleFonts.poppins(fontWeight: FontWeight.w600, fontSize: 18),
),
content: SizedBox(
width: 640,
height: contentHeight,
child: Scrollbar(
thumbVisibility: entries.length > 4,
child: ListView.separated(
itemCount: entries.length,
separatorBuilder: (_, __) => const SizedBox(height: 10),
itemBuilder: (context, index) {
final entry = entries[index];
return Container(
width: double.infinity,
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: const Color(0xFFF5F5F5),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: const Color(0xFFE8E8E8)),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
entry.key,
style: GoogleFonts.poppins(
fontSize: 13,
fontWeight: FontWeight.w600,
color: const Color(0xFF009195),
),
),
const SizedBox(height: 4),
Text(
entry.value,
style: GoogleFonts.poppins(
fontSize: 13,
fontWeight: FontWeight.w400,
color: Colors.black87,
),
),
],
),
);
},
),
),
),
actions: [
ElevatedButton(
onPressed: () => Navigator.pop(context),
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF009195),
),
child: Text('Close', style: GoogleFonts.poppins(color: Colors.white)),
),
],
);
}
}
class _ReminderMailConfigDialog extends StatefulWidget {
final String clientId;
final String clientPolicyId;
final String clientBranchId;
final String token;
final ApiService apiService;
final String defaultSubject;
final String defaultHtmlBody;
const _ReminderMailConfigDialog({
required this.clientId,
required this.clientPolicyId,
required this.clientBranchId,
required this.token,
required this.apiService,
required this.defaultSubject,
required this.defaultHtmlBody,
});
@override
State<_ReminderMailConfigDialog> createState() =>
_ReminderMailConfigDialogState();
}
class _ReminderMailConfigDialogState extends State<_ReminderMailConfigDialog> {
static const _frequencies = <String, String>{
'daily': 'Daily',
'weekly': 'Weekly',
'monthly': 'Monthly',
'custom': 'Custom',
'working_days': 'Working Days',
};
bool _isLoadingConfig = true;
bool _isSaving = false;
int? _configId;
String _frequency = 'daily';
bool _isEnabled = true;
String _reminderDaysText = '';
final Set<String> _selectedWorkingDays = {};
List<Map<String, dynamic>> _workingDayOptions = [];
late final TextEditingController _reminderDaysController;
@override
void initState() {
super.initState();
_reminderDaysController = TextEditingController();
_loadConfig();
}
@override
void dispose() {
_reminderDaysController.dispose();
super.dispose();
}
Future<void> _loadConfig() async {
setState(() => _isLoadingConfig = true);
try {
final response = await widget.apiService.getReminderMailConfigApi(
widget.clientPolicyId,
widget.token,
);
if (!mounted) return;
final ok = response['status'] == true || response['status'] == 'success';
if (ok) {
_workingDayOptions = _parseWorkingDayOptions(
response['working_day_options'],
);
_applyConfig(response['data']);
} else {
ToastHelper.showErrorToast(
context,
response['message']?.toString() ?? 'Failed to load reminder config',
);
}
} catch (e) {
logDebug('Reminder config load failed: $e');
if (mounted) {
ToastHelper.showErrorToast(
context,
'Failed to load reminder configuration',
);
}
} finally {
if (mounted) {
setState(() => _isLoadingConfig = false);
}
}
}
List<Map<String, dynamic>> _parseWorkingDayOptions(dynamic raw) {
if (raw is! List) return _defaultWorkingDayOptions();
return raw
.whereType<Map>()
.map((item) => Map<String, dynamic>.from(item))
.toList();
}
List<Map<String, dynamic>> _defaultWorkingDayOptions() {
return [
{'value': 1, 'label': 'Mon', 'key': 'mon'},
{'value': 2, 'label': 'Tue', 'key': 'tue'},
{'value': 3, 'label': 'Wed', 'key': 'wed'},
{'value': 4, 'label': 'Thu', 'key': 'thu'},
{'value': 5, 'label': 'Fri', 'key': 'fri'},
{'value': 6, 'label': 'Sat', 'key': 'sat'},
{'value': 7, 'label': 'Sun', 'key': 'sun'},
];
}
void _applyConfig(dynamic data) {
_configId = null;
_frequency = 'daily';
_isEnabled = true;
_reminderDaysText = '';
_reminderDaysController.clear();
_selectedWorkingDays.clear();
if (data is! Map) return;
final config = Map<String, dynamic>.from(data);
_configId = int.tryParse(config['id']?.toString() ?? '');
_frequency = config['frequency']?.toString() ?? 'daily';
_isEnabled = config['is_enabled']?.toString() != '0';
if (_frequency == 'working_days') {
final labels = config['working_day_labels'];
if (labels is List && labels.isNotEmpty) {
_selectedWorkingDays.addAll(labels.map((e) => e.toString()));
} else {
_applyWorkingDaysFromReminderDays(
config['reminder_days']?.toString() ?? '',
);
}
} else if (_frequency != 'daily') {
_reminderDaysText = config['reminder_days']?.toString() ?? '';
_reminderDaysController.text = _reminderDaysText;
}
}
void _applyWorkingDaysFromReminderDays(String reminderDays) {
if (reminderDays.isEmpty) return;
final options = _workingDayOptions.isNotEmpty
? _workingDayOptions
: _defaultWorkingDayOptions();
final valueToLabel = <String, String>{
for (final option in options)
option['value'].toString(): option['label'].toString(),
};
for (final part in reminderDays.split(',')) {
final token = part.trim();
if (token.isEmpty) continue;
if (RegExp(r'^\d+$').hasMatch(token)) {
final normalized = token == '0' ? '7' : token;
final label = valueToLabel[normalized];
if (label != null) {
_selectedWorkingDays.add(label);
}
} else {
final match = options.firstWhere(
(option) =>
option['label'].toString().toLowerCase() == token.toLowerCase() ||
option['key'].toString().toLowerCase() == token.toLowerCase(),
orElse: () => {},
);
if (match.isNotEmpty) {
_selectedWorkingDays.add(match['label'].toString());
}
}
}
}
String? _buildReminderDays() {
switch (_frequency) {
case 'daily':
return null;
case 'working_days':
if (_selectedWorkingDays.isEmpty) return null;
return _selectedWorkingDays.join(',');
default:
final value = _reminderDaysText.trim();
return value.isEmpty ? null : value;
}
}
String? _validateBeforeSave() {
if (_frequency == 'working_days' && _selectedWorkingDays.isEmpty) {
return 'Select at least one working day';
}
if (_frequency != 'daily' &&
_frequency != 'working_days' &&
_reminderDaysText.trim().isEmpty) {
return 'Reminder days are required for $_frequency frequency';
}
return null;
}
Future<int?> _resolveHrId() async {
final tokenService = TokenStorageService();
final hrId = await tokenService.readValue('enrollmentHrId') ??
await tokenService.readValue('empHrId');
return int.tryParse(hrId?.toString() ?? '');
}
Future<void> _saveConfig() async {
final validationError = _validateBeforeSave();
if (validationError != null) {
ToastHelper.showWarningToast(context, validationError);
return;
}
setState(() => _isSaving = true);
try {
final policyId = int.tryParse(widget.clientPolicyId);
if (policyId == null) {
ToastHelper.showErrorToast(context, 'Invalid client policy id');
return;
}
final payload = <String, dynamic>{
'client_policy_id': policyId,
'frequency': _frequency,
'is_enabled': _isEnabled ? 1 : 0,
};
final reminderDays = _buildReminderDays();
if (reminderDays != null) {
payload['reminder_days'] = reminderDays;
}
final hrId = await _resolveHrId();
if (hrId != null) {
payload['hr_id'] = hrId;
}
if (_configId != null) {
payload['id'] = _configId;
}
final response = await widget.apiService.saveReminderMailConfigApi(
widget.token,
payload,
);
if (!mounted) return;
final ok = response['status'] == true || response['status'] == 'success';
if (ok) {
final data = response['data'];
if (data is Map) {
_applyConfig(data);
}
ToastHelper.showSuccessToast(
context,
response['message']?.toString() ??
'Reminder mail configuration saved successfully',
);
Navigator.pop(context);
} else {
ToastHelper.showErrorToast(
context,
response['message']?.toString() ?? 'Failed to save configuration',
);
}
} catch (e) {
logDebug('Reminder config save failed: $e');
if (mounted) {
ToastHelper.showErrorToast(
context,
'Failed to save reminder configuration',
);
}
} finally {
if (mounted) {
setState(() => _isSaving = false);
}
}
}
Widget _buildDaysField() {
if (_frequency == 'daily') {
return const SizedBox.shrink();
}
if (_frequency == 'working_days') {
final options = _workingDayOptions.isNotEmpty
? _workingDayOptions
: _defaultWorkingDayOptions();
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Working Days',
style: GoogleFonts.poppins(
fontSize: 13,
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 8),
Wrap(
spacing: 8,
runSpacing: 8,
children: options.map((option) {
final label = option['label'].toString();
final selected = _selectedWorkingDays.contains(label);
return FilterChip(
label: Text(label, style: GoogleFonts.poppins(fontSize: 12)),
selected: selected,
onSelected: (value) {
setState(() {
if (value) {
_selectedWorkingDays.add(label);
} else {
_selectedWorkingDays.remove(label);
}
});
},
selectedColor: const Color(0xFFC5F2F4),
checkmarkColor: const Color(0xFF009195),
);
}).toList(),
),
],
);
}
final helperText = switch (_frequency) {
'weekly' => 'Weekday numbers 0-6 (Sun-Sat), comma-separated. Example: 1,3,5',
'monthly' => 'Days of month 1-31, comma-separated. Example: 1,15,28',
'custom' => 'Custom days of month 1-31, comma-separated. Example: 5,10,20',
_ => '',
};
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Reminder Days',
style: GoogleFonts.poppins(
fontSize: 13,
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 6),
TextField(
controller: _reminderDaysController,
onChanged: (value) => _reminderDaysText = value,
style: GoogleFonts.poppins(fontSize: 14),
decoration: InputDecoration(
hintText: helperText,
filled: true,
fillColor: const Color(0xFFF5F5F5),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: BorderSide.none,
),
contentPadding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 10,
),
),
),
const SizedBox(height: 4),
Text(
helperText,
style: GoogleFonts.poppins(fontSize: 11, color: Colors.black45),
),
],
);
}
Future<void> _openTemplateEditor() async {
await showDialog<void>(
context: context,
barrierDismissible: false,
builder: (dialogContext) {
return ReminderEmailTemplateDialog(
clientId: widget.clientId,
clientPolicyId: widget.clientPolicyId,
clientBranchId: widget.clientBranchId,
token: widget.token,
apiService: widget.apiService,
defaultSubject: widget.defaultSubject,
defaultHtmlBody: widget.defaultHtmlBody,
mode: ReminderEmailTemplateMode.config,
);
},
);
}
@override
Widget build(BuildContext context) {
final isBusy = _isSaving;
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: _isLoadingConfig
? const SizedBox(
height: 180,
child: Center(child: CircularProgressIndicator()),
)
: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Configure the schedule for automated enrollment reminder emails.',
style: GoogleFonts.poppins(
fontSize: 13,
color: Colors.black54,
),
),
const SizedBox(height: 16),
SwitchListTile(
contentPadding: EdgeInsets.zero,
title: Text(
'Enable scheduled reminders',
style: GoogleFonts.poppins(
fontSize: 13,
fontWeight: FontWeight.w600,
),
),
value: _isEnabled,
activeColor: const Color(0xFF009195),
onChanged: isBusy
? null
: (value) => setState(() => _isEnabled = value),
),
const SizedBox(height: 8),
Text(
'Frequency',
style: GoogleFonts.poppins(
fontSize: 13,
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 6),
DropdownButtonFormField<String>(
value: _frequency,
isExpanded: 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: 10,
),
),
items: _frequencies.entries
.map(
(entry) => DropdownMenuItem(
value: entry.key,
child: Text(
entry.value,
style: GoogleFonts.poppins(fontSize: 14),
),
),
)
.toList(),
onChanged: isBusy
? null
: (value) {
if (value == null) return;
setState(() {
_frequency = value;
_reminderDaysText = '';
_reminderDaysController.clear();
_selectedWorkingDays.clear();
});
},
),
const SizedBox(height: 14),
_buildDaysField(),
const SizedBox(height: 20),
// OutlinedButton.icon(
// onPressed: isBusy ? null : _openTemplateEditor,
// icon: const Icon(Icons.edit_outlined, size: 18),
// label: Text(
// 'Edit Email Template',
// style: GoogleFonts.poppins(
// fontSize: 13,
// fontWeight: FontWeight.w600,
// ),
// ),
// style: OutlinedButton.styleFrom(
// foregroundColor: const Color(0xFF009195),
// side: const BorderSide(color: Color(0xFF009195)),
// padding: const EdgeInsets.symmetric(
// horizontal: 16,
// vertical: 12,
// ),
// ),
// ),
],
),
),
),
actions: [
TextButton(
onPressed: isBusy ? null : () => Navigator.pop(context),
child: Text('Cancel', style: GoogleFonts.poppins(color: Colors.black54)),
),
ElevatedButton(
onPressed: isBusy || _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)),
),
],
);
}
}