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