hr feedbacks

This commit is contained in:
Surendiran 2026-08-03 10:20:03 +05:30
parent 1e97c41545
commit 5ab200618c
7 changed files with 717 additions and 399 deletions

View File

@ -7,7 +7,6 @@ import 'package:nhancepolicy/service/api_service.dart';
import 'package:nhancepolicy/service/svg_service.dart';
import 'package:nhancepolicy/service/token_storage_service.dart';
import 'package:nhancepolicy/logger.dart';
import 'package:url_launcher/url_launcher.dart';
class NhanceSideBar extends StatefulWidget {
const NhanceSideBar({super.key});
@ -102,8 +101,7 @@ class _NhanceSideBarState extends State<NhanceSideBar> {
}
// CLAIMS only when module 4 and at least one sub-menu is allowed
if (postModules.contains(4) &&
(claimsSubMenu.contains('EB') || claimsSubMenu.contains('Non-EB'))) {
if (postModules.contains(4) && _claimsSubMenuRoutes.isNotEmpty) {
items.add({
'route': 'ClaimsPolicies',
'label': 'Claims',
@ -116,6 +114,26 @@ class _NhanceSideBarState extends State<NhanceSideBar> {
});
}
/// Allowed Claims destinations based on claims_sub_menu.
List<({String label, String route})> get _claimsSubMenuRoutes {
final items = <({String label, String route})>[];
if (claimsSubMenu.contains('EB')) {
items.add((label: 'Employee Benefits', route: 'ClaimsPolicies'));
}
if (claimsSubMenu.contains('Non-EB')) {
items.add((label: 'Non-Employee Benefits', route: 'nonEBClaimsList'));
}
return items;
}
bool get _hasMultipleClaimsSubMenus => _claimsSubMenuRoutes.length > 1;
String? get _singleClaimsRoute {
final routes = _claimsSubMenuRoutes;
if (routes.length == 1) return routes.first.route;
return null;
}
Future<void> logout(BuildContext context) async {
// final prefs = await SharedPreferences.getInstance();
// final String? hrtoken = prefs.getString('_postToken');
@ -149,6 +167,8 @@ class _NhanceSideBarState extends State<NhanceSideBar> {
}
void _showClaimsSubMenu(GlobalKey key) {
if (!_hasMultipleClaimsSubMenus) return;
if (_claimsMenuSuppressUntil != null &&
DateTime.now().isBefore(_claimsMenuSuppressUntil!)) {
return;
@ -161,6 +181,7 @@ class _NhanceSideBarState extends State<NhanceSideBar> {
if (box == null) return;
final offset = box.localToGlobal(Offset.zero);
final subMenus = _claimsSubMenuRoutes;
_claimsOverlayEntry?.remove();
_claimsOverlayEntry = OverlayEntry(
builder: (context) => Positioned(
@ -177,7 +198,7 @@ class _NhanceSideBarState extends State<NhanceSideBar> {
child: Material(
color: Colors.transparent,
child: Container(
width: 120,
width: 200,
padding: const EdgeInsets.symmetric(vertical: 6),
decoration: BoxDecoration(
color: Colors.white,
@ -193,10 +214,8 @@ class _NhanceSideBarState extends State<NhanceSideBar> {
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
if (claimsSubMenu.contains('EB'))
_buildClaimsMenuItem('EB', 'ClaimsPolicies'),
if (claimsSubMenu.contains('Non-EB'))
_buildClaimsMenuItem('Non-EB', 'nonEBClaimsList'),
for (final item in subMenus)
_buildClaimsMenuItem(item.label, item.route),
],
),
),
@ -341,6 +360,9 @@ class _NhanceSideBarState extends State<NhanceSideBar> {
final isClaimsActive =
activeRoute == 'ClaimsPolicies' || activeRoute == 'nonEBClaimsList';
final itemKey = GlobalKey();
final showClaimsSubMenu = isClaims && _hasMultipleClaimsSubMenus;
final claimsDirectRoute =
isClaims ? (_singleClaimsRoute ?? 'ClaimsPolicies') : null;
return _SideItem(
key: itemKey,
@ -355,14 +377,18 @@ class _NhanceSideBarState extends State<NhanceSideBar> {
),
label: item['label'],
isActive: isClaims ? isClaimsActive : activeRoute == item['route'],
onTap: isClaims ? null : () => _navigate(item['route']),
onHoverEnter: isClaims
onTap: showClaimsSubMenu
? null
: () => _navigate(
claimsDirectRoute ?? item['route'] as String,
),
onHoverEnter: showClaimsSubMenu
? () {
_isHoveringClaimsItem = true;
_showClaimsSubMenu(itemKey);
}
: null,
onHoverExit: isClaims
onHoverExit: showClaimsSubMenu
? () {
_isHoveringClaimsItem = false;
_scheduleCloseClaimsMenu();
@ -443,7 +469,6 @@ class _HelpContactDialogState extends State<_HelpContactDialog> {
bool _isLoading = false;
List<Map<String, dynamic>> _level1Contacts = [];
List<Map<String, dynamic>> _level2Contacts = [];
Map<String, String> _hospitalLinks = {};
String? _errorMessage;
@override
@ -487,7 +512,6 @@ class _HelpContactDialogState extends State<_HelpContactDialog> {
setState(() {
_level1Contacts = _parseContacts(data['level_1']);
_level2Contacts = _parseContacts(data['level_2']);
_hospitalLinks = _parseHospitalLinks(data['tpa_network_hospitals']);
_isLoading = false;
});
} else {
@ -516,25 +540,6 @@ class _HelpContactDialogState extends State<_HelpContactDialog> {
.toList();
}
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> _openHospitalUrl(String url) async {
final uri = Uri.tryParse(url);
if (uri == null) return;
await launchUrl(uri, webOnlyWindowName: '_blank');
}
Widget _buildContactCard(Map<String, dynamic> contact) {
final name = contact['first_name']?.toString() ?? '-';
final email = contact['email']?.toString() ?? '-';
@ -649,151 +654,35 @@ class _HelpContactDialogState extends State<_HelpContactDialog> {
);
}
Widget _buildHospitalListContent() {
if (!widget.isPostUser) {
return Center(
child: Text(
'No Network Hospital List',
style: GoogleFonts.poppins(
fontSize: 15,
fontWeight: FontWeight.w500,
color: Colors.black54,
),
),
);
}
if (_isLoading) {
return const SizedBox(
height: 180,
child: Center(child: CircularProgressIndicator()),
);
}
if (_errorMessage != null) {
return Center(
child: Text(
_errorMessage!,
textAlign: TextAlign.center,
style: GoogleFonts.poppins(fontSize: 14, color: Colors.red),
),
);
}
if (_hospitalLinks.isEmpty) {
return Center(
child: Text(
'No Network Hospital List',
style: GoogleFonts.poppins(fontSize: 14, color: Colors.black54),
),
);
}
return 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),
),
],
),
),
);
},
);
}
@override
Widget build(BuildContext context) {
final maxHeight = MediaQuery.of(context).size.height * 0.65;
final cappedMaxHeight = maxHeight.clamp(280.0, 520.0);
return DefaultTabController(
length: 2,
child: AlertDialog(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
title: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(
'Help',
style:
GoogleFonts.poppins(fontWeight: FontWeight.w600, fontSize: 18),
),
const SizedBox(height: 12),
TabBar(
labelColor: const Color(0xFF009195),
unselectedLabelColor: Colors.black54,
indicatorColor: const Color(0xFF009195),
labelStyle: GoogleFonts.poppins(
fontSize: 13,
fontWeight: FontWeight.w600,
),
tabs: const [
Tab(text: 'Contacts'),
Tab(text: 'Network Hospital List'),
],
),
],
),
content: SizedBox(
width: 560,
child: ConstrainedBox(
constraints: BoxConstraints(maxHeight: cappedMaxHeight),
child: TabBarView(
children: [
Scrollbar(
child: SingleChildScrollView(child: _buildContent()),
),
Scrollbar(
child: SingleChildScrollView(
child: _buildHospitalListContent(),
),
),
],
),
),
),
actions: [
ElevatedButton(
onPressed: () => Navigator.pop(context),
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF009195),
),
child: Text('Close', style: GoogleFonts.poppins(color: Colors.white)),
),
],
return AlertDialog(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
title: Text(
'Help',
style: GoogleFonts.poppins(fontWeight: FontWeight.w600, fontSize: 18),
),
content: SizedBox(
width: 560,
child: ConstrainedBox(
constraints: BoxConstraints(maxHeight: cappedMaxHeight),
child: Scrollbar(
child: SingleChildScrollView(child: _buildContent()),
),
),
),
actions: [
ElevatedButton(
onPressed: () => Navigator.pop(context),
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF009195),
),
child: Text('Close', style: GoogleFonts.poppins(color: Colors.white)),
),
],
);
}
}

View File

@ -1,6 +1,9 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import '../config/environment.dart';
import '../service/token_storage_service.dart';
class NhanceTopBar extends StatefulWidget implements PreferredSizeWidget {
@ -19,6 +22,9 @@ class _NhanceTopBarState extends State<NhanceTopBar> {
List<Map<String, dynamic>> branches = [];
Map<String, dynamic>? selectedBranch;
/// client_id logo URL
final Map<String, String> _logoByClientId = {};
@override
void initState() {
super.initState();
@ -29,6 +35,98 @@ class _NhanceTopBarState extends State<NhanceTopBar> {
branches = tokenStorage.getCombinedBranches();
selectedBranch = tokenStorage.getSelectedBranch();
setState(() {});
_loadBranchLogos();
}
String _clientKey(Map<String, dynamic> branch) {
return (branch['client_id'] ?? branch['id'] ?? '').toString();
}
String? _logoFromBranch(Map<String, dynamic> branch) {
final candidates = [
branch['client_logo'],
branch['logo'],
branch['clientLogo'],
];
for (final value in candidates) {
final url = value?.toString().trim() ?? '';
if (url.isNotEmpty) return url;
}
return null;
}
Future<void> _loadBranchLogos() async {
final uniqueBranches = <String, Map<String, dynamic>>{};
for (final branch in branches) {
final key = _clientKey(branch);
if (key.isEmpty) continue;
uniqueBranches.putIfAbsent(key, () => branch);
}
for (final entry in uniqueBranches.entries) {
final key = entry.key;
if (_logoByClientId.containsKey(key)) continue;
final fromBranch = _logoFromBranch(entry.value);
if (fromBranch != null) {
if (!mounted) return;
setState(() => _logoByClientId[key] = fromBranch);
continue;
}
final logoUrl = await _fetchClientLogoUrl(entry.value);
if (!mounted) return;
if (logoUrl != null && logoUrl.isNotEmpty) {
setState(() => _logoByClientId[key] = logoUrl);
}
}
}
Future<String?> _fetchClientLogoUrl(Map<String, dynamic> branch) async {
try {
final token = branch['token']?.toString() ??
tokenStorage.getCurrentToken() ??
'';
if (token.isEmpty) return null;
final clientId = branch['client_id']?.toString() ?? '';
final branchId = branch['client_branch_id']?.toString() ?? '';
final preBranchId = branch['pre_branch_id']?.toString() ?? branchId;
if (clientId.isEmpty || branchId.isEmpty) return null;
final isPre = branch['enrollment_type']?.toString() == 'pre';
final postClientId = isPre ? '' : clientId;
final postBranchId = isPre ? '' : branchId;
final preClientId = isPre ? clientId : clientId;
final preBranch = isPre ? branchId : preBranchId;
final url = Uri.parse(
'${Environment.apiUrl}getClientDetails'
'?post_client_id=$postClientId'
'&post_branch_id=$postBranchId'
'&pre_client_id=$preClientId'
'&pre_branch_id=$preBranch',
);
final response = await http.get(
url,
headers: {
'Authorization': 'Bearer $token',
'APP-SIGNATURE':
'nhance-2025-signature-T8f6gV9k5fK2wf5V5c4vId3b0J8z6cAm2x8Y',
},
);
if (response.statusCode != 200) return null;
final data = jsonDecode(response.body);
if (data is! Map || data['status'] != 'success') return null;
final logo = data['data']?['client']?['client_logo']?.toString().trim();
if (logo == null || logo.isEmpty) return null;
return logo;
} catch (_) {
return null;
}
}
Future<String> _getBranchSwitchRoute() async {
@ -60,19 +158,26 @@ class _NhanceTopBarState extends State<NhanceTopBar> {
@override
Widget build(BuildContext context) {
final selectedLogo = selectedBranch == null
? null
: _logoByClientId[_clientKey(selectedBranch!)];
return AppBar(
automaticallyImplyLeading: false,
backgroundColor: const Color(0xFFBFEFEF),
elevation: 0,
title: Row(
children: [
Image.asset('assets/nhance_client_logo.png', height: 36),
Image.asset('assets/nhance_client_logo.png', height: 32),
const Spacer(),
if (selectedBranch != null)
_BranchPopup(
clientName: selectedBranch!['client_name'],
branchName: selectedBranch!['branch_name'],
clientName: selectedBranch!['client_name']?.toString() ?? '',
branchName: selectedBranch!['branch_name']?.toString() ?? '',
selectedLogoUrl: selectedLogo,
branches: branches,
logoByClientId: _logoByClientId,
clientKeyBuilder: _clientKey,
onSelected: _onBranchSelected,
),
],
@ -84,13 +189,19 @@ class _NhanceTopBarState extends State<NhanceTopBar> {
class _BranchPopup extends StatelessWidget {
final String clientName;
final String branchName;
final String? selectedLogoUrl;
final List<Map<String, dynamic>> branches;
final Map<String, String> logoByClientId;
final String Function(Map<String, dynamic>) clientKeyBuilder;
final Function(Map<String, dynamic>) onSelected;
const _BranchPopup({
required this.clientName,
required this.branchName,
required this.selectedLogoUrl,
required this.branches,
required this.logoByClientId,
required this.clientKeyBuilder,
required this.onSelected,
});
@ -102,13 +213,16 @@ class _BranchPopup extends StatelessWidget {
onSelected: onSelected,
itemBuilder: (context) {
return branches.map((branch) {
final logoUrl = logoByClientId[clientKeyBuilder(branch)];
return PopupMenuItem<Map<String, dynamic>>(
value: branch,
child: Row(
children: [
_BranchLogo(logoUrl: logoUrl, size: 24),
const SizedBox(width: 8),
Expanded(
child: Text(
branch['client_name'],
branch['client_name']?.toString() ?? '',
style: const TextStyle(fontSize: 13),
overflow: TextOverflow.ellipsis,
),
@ -116,11 +230,14 @@ class _BranchPopup extends StatelessWidget {
const SizedBox(width: 10),
const Icon(Icons.location_on, size: 14, color: Colors.grey),
const SizedBox(width: 4),
Text(
branch['branch_name'],
style: const TextStyle(
fontSize: 12,
color: Colors.grey,
Flexible(
child: Text(
branch['branch_name']?.toString() ?? '',
style: const TextStyle(
fontSize: 12,
color: Colors.grey,
),
overflow: TextOverflow.ellipsis,
),
),
],
@ -128,62 +245,119 @@ class _BranchPopup extends StatelessWidget {
);
}).toList();
},
child: Container(
height: 40,
padding: const EdgeInsets.symmetric(horizontal: 16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(22),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.08),
blurRadius: 8,
offset: const Offset(0, 2),
),
],
),
child: Row(
children: [
// CLIENT NAME
Text(
clientName,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
),
),
const SizedBox(width: 8),
// 📍 BRANCH NAME (SELECTED)
Row(
children: [
const Icon(
Icons.location_on,
size: 14,
color: Colors.grey,
),
const SizedBox(width: 4),
Text(
branchName,
style: const TextStyle(
fontSize: 12,
color: Colors.grey,
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
_BranchLogo(logoUrl: selectedLogoUrl, size: 32),
const SizedBox(width: 8),
Container(
height: 40,
padding: const EdgeInsets.symmetric(horizontal: 12),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(22),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.08),
blurRadius: 8,
offset: const Offset(0, 2),
),
],
),
const SizedBox(width: 6),
// DROPDOWN ARROW
const Icon(
Icons.keyboard_arrow_down,
size: 20,
color: Colors.orange,
child: Row(
children: [
Text(
clientName,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
),
),
const SizedBox(width: 8),
Row(
children: [
const Icon(
Icons.location_on,
size: 14,
color: Colors.grey,
),
const SizedBox(width: 4),
Text(
branchName,
style: const TextStyle(
fontSize: 12,
color: Colors.grey,
),
),
],
),
const SizedBox(width: 6),
const Icon(
Icons.keyboard_arrow_down,
size: 20,
color: Colors.orange,
),
],
),
],
),
),
],
),
);
}
}
class _BranchLogo extends StatelessWidget {
final String? logoUrl;
final double size;
const _BranchLogo({
required this.logoUrl,
required this.size,
});
@override
Widget build(BuildContext context) {
final url = logoUrl?.trim() ?? '';
if (url.isEmpty) {
return _placeholder();
}
return ClipRRect(
borderRadius: BorderRadius.circular(4),
child: Image.network(
url,
width: size,
height: size,
fit: BoxFit.contain,
errorBuilder: (_, __, ___) => _placeholder(),
loadingBuilder: (context, child, progress) {
if (progress == null) return child;
return SizedBox(
width: size,
height: size,
child: const Padding(
padding: EdgeInsets.all(4),
child: CircularProgressIndicator(strokeWidth: 1.5),
),
);
},
),
);
}
Widget _placeholder() {
return Container(
width: size,
height: size,
decoration: BoxDecoration(
color: const Color(0xFFE8F5F5),
borderRadius: BorderRadius.circular(4),
border: Border.all(color: const Color(0xFFBFEFEF)),
),
child: Icon(
Icons.business,
size: size * 0.6,
color: const Color(0xFF00999E),
),
);
}

View File

@ -6,7 +6,8 @@ class ClaimsOverviewCacheEntry {
final String policyId;
final Map<String, dynamic> claimsKpiBySlug;
final Map<String, dynamic> enrollmentKpiBySlug;
final DateTime generatedAt;
/// Raw `generated_at` from the claims API (e.g. `01-07-2026`), if present.
final String? generatedAt;
final String? claimsLoadError;
final String? enrollmentLoadError;
@ -14,19 +15,19 @@ class ClaimsOverviewCacheEntry {
required this.policyId,
required this.claimsKpiBySlug,
required this.enrollmentKpiBySlug,
required this.generatedAt,
this.generatedAt,
this.claimsLoadError,
this.enrollmentLoadError,
});
factory ClaimsOverviewCacheEntry.fromJson(Map<String, dynamic> json) {
final raw = json['generatedAt']?.toString().trim();
return ClaimsOverviewCacheEntry(
policyId: json['policyId']?.toString() ?? '',
claimsKpiBySlug: Map<String, dynamic>.from(json['claimsKpiBySlug'] ?? {}),
enrollmentKpiBySlug:
Map<String, dynamic>.from(json['enrollmentKpiBySlug'] ?? {}),
generatedAt: DateTime.tryParse(json['generatedAt']?.toString() ?? '') ??
DateTime.now(),
generatedAt: (raw != null && raw.isNotEmpty) ? raw : null,
claimsLoadError: json['claimsLoadError']?.toString(),
enrollmentLoadError: json['enrollmentLoadError']?.toString(),
);
@ -36,14 +37,14 @@ class ClaimsOverviewCacheEntry {
'policyId': policyId,
'claimsKpiBySlug': claimsKpiBySlug,
'enrollmentKpiBySlug': enrollmentKpiBySlug,
'generatedAt': generatedAt.toIso8601String(),
'generatedAt': generatedAt,
'claimsLoadError': claimsLoadError,
'enrollmentLoadError': enrollmentLoadError,
};
}
abstract final class ClaimsOverviewCache {
static const _keyPrefix = 'claims_overview_cache_v1_';
static const _keyPrefix = 'claims_overview_cache_v2_';
static String _key(String branchId, String policyId) =>
'$_keyPrefix${branchId}_$policyId';

View File

@ -7,6 +7,7 @@ import 'package:intl/intl.dart';
import '../../customAppBar/base_layout.dart';
import '../../customAppBar/toastHelper.dart';
import '../../logger.dart';
import '../../service/api_service.dart';
import '../../service/secure_pop_scope.dart';
import '../../service/token_storage_service.dart';
@ -43,6 +44,7 @@ class _ClaimsOverviewDashboardState extends State<ClaimsOverviewDashboard>
bool _isExportingPdf = false;
bool _sessionChecked = false;
int _replayToken = 0;
int _loadGeneration = 0;
int _currentTab = 0;
ClaimsOverviewViewData _viewData = ClaimsOverviewViewData.empty();
@ -53,12 +55,34 @@ class _ClaimsOverviewDashboardState extends State<ClaimsOverviewDashboard>
String? _selectedPolicyId;
List<Map<String, dynamic>> _activePolicies = [];
Uint8List? _clientLogoBytes;
DateTime? _dataGeneratedAt;
/// Raw `generated_at` from claims API (e.g. `01-07-2026`).
String? _apiGeneratedAt;
String? get _formattedGeneratedAt {
final generatedAt = _dataGeneratedAt;
if (generatedAt == null) return null;
return DateFormat('d MMM yyyy, h:mm a').format(generatedAt.toLocal());
final raw = _apiGeneratedAt?.trim();
if (raw == null || raw.isEmpty) return null;
return _formatApiGeneratedAt(raw);
}
/// Formats API `generated_at` for display. Returns null when empty.
static String? _formatApiGeneratedAt(String raw) {
final value = raw.trim();
if (value.isEmpty) return null;
for (final pattern in ['dd-MM-yyyy', 'yyyy-MM-dd', 'dd/MM/yyyy']) {
try {
final parsed = DateFormat(pattern).parseStrict(value);
return DateFormat('d MMM yyyy').format(parsed);
} catch (_) {}
}
final iso = DateTime.tryParse(value);
if (iso != null) return DateFormat('d MMM yyyy').format(iso);
return value;
}
static String? _parseApiGeneratedAt(Map<String, dynamic> response) {
final raw = response['generated_at']?.toString().trim();
if (raw == null || raw.isEmpty) return null;
return raw;
}
static const _tabs = [
@ -108,9 +132,29 @@ class _ClaimsOverviewDashboardState extends State<ClaimsOverviewDashboard>
if (!mounted) return;
setState(() => _sessionChecked = true);
_logHrActivity('opened_hr_dashboard');
await _loadDashboard();
}
Future<void> _logHrActivity(String activity) async {
try {
final postId = await _tokenService.readValue('empHrId');
final preId = await _tokenService.readValue('enrollmentEmpPrimaryId');
final token = await _tokenService.getCurrentToken();
if (postId == null ||
postId.isEmpty ||
preId == null ||
preId.isEmpty ||
token == null ||
token.isEmpty) {
return;
}
await _apiService.getPostLogHrActivity(postId, preId, token, activity);
} catch (e) {
logDebug('logHrActivity ($activity) failed: $e');
}
}
void _onTabChanged() {
if (_tabController.indexIsChanging) return;
final index = _tabController.index;
@ -133,10 +177,17 @@ class _ClaimsOverviewDashboardState extends State<ClaimsOverviewDashboard>
});
}
bool _isStaleLoad(int loadId, [String? policyId]) {
if (!mounted || loadId != _loadGeneration) return true;
if (policyId != null && policyId != _selectedPolicyId) return true;
return false;
}
Future<void> _loadDashboard({
bool reloadPolicies = false,
bool forceRefresh = false,
}) async {
final loadId = ++_loadGeneration;
final isInitialLoad = _replayToken == 0;
setState(() {
if (isInitialLoad) {
@ -151,28 +202,27 @@ class _ClaimsOverviewDashboardState extends State<ClaimsOverviewDashboard>
_clientBranchId = await _tokenService.readValue('empClientBranchId');
final hrId = await _tokenService.readValue('empHrId');
final token = await _tokenService.getCurrentToken();
if (_isStaleLoad(loadId)) return;
if (token != null && token.isNotEmpty) {
await _apiService.getTokenLoadAPI(token);
}
if (_isStaleLoad(loadId)) return;
if (isInitialLoad || reloadPolicies || _activePolicies.isEmpty) {
await _loadPolicyList(token ?? '', hrId ?? '');
if (isInitialLoad && _activePolicies.isNotEmpty) {
_selectedPolicyId =
_activePolicies.first['client_policy_id'].toString();
}
if (!mounted) return;
// Keep an existing user selection; only default to first when none set.
if (_isStaleLoad(loadId)) return;
setState(() {});
}
if (_selectedPolicyId == null) {
if (!mounted) return;
if (_isStaleLoad(loadId)) return;
setState(() {
_viewData = ClaimsOverviewViewData.empty(
error: 'No active policy found',
);
_enrollmentViewData = EnrollmentOverviewViewData.empty();
_dataGeneratedAt = null;
_apiGeneratedAt = null;
_isLoading = false;
_isRefreshing = false;
});
@ -185,12 +235,13 @@ class _ClaimsOverviewDashboardState extends State<ClaimsOverviewDashboard>
if (forceRefresh) {
await ClaimsOverviewCache.clear(branchId, policyId);
}
if (_isStaleLoad(loadId, policyId)) return;
if (!forceRefresh) {
final cached = await ClaimsOverviewCache.read(branchId, policyId);
if (cached != null) {
if (_isStaleLoad(loadId, policyId)) return;
_applyCacheEntry(cached);
if (!mounted) return;
setState(() {
_isLoading = false;
_isRefreshing = false;
@ -205,13 +256,15 @@ class _ClaimsOverviewDashboardState extends State<ClaimsOverviewDashboard>
final response = await _apiService.getClaimsCollectionV2All(
clientPolicyId: policyId,
);
if (_isStaleLoad(loadId, policyId)) return;
final enrollmentResponse =
await _apiService.getEnrollmentCollectionV1All(
clientPolicyId: policyId,
);
if (!mounted) return;
if (_isStaleLoad(loadId, policyId)) return;
final apiGeneratedAt = _parseApiGeneratedAt(response);
final claimsOk = ClaimsKpiParser.isSuccessResponse(response);
var claimsData = await ClaimsOverviewViewData.enrichFromApiResponse(
response,
@ -220,6 +273,7 @@ class _ClaimsOverviewDashboardState extends State<ClaimsOverviewDashboard>
clientPolicyId: policyId,
),
);
if (_isStaleLoad(loadId, policyId)) return;
var enrollmentData = _parseEnrollmentResponse(enrollmentResponse);
final selectedPolicy = _selectedPolicy();
@ -236,11 +290,11 @@ class _ClaimsOverviewDashboardState extends State<ClaimsOverviewDashboard>
loadError: message,
);
if (claimsData.kpiBySlug.isEmpty) {
if (!mounted) return;
if (_isStaleLoad(loadId, policyId)) return;
setState(() {
_viewData = claimsData;
_enrollmentViewData = enrollmentData;
_dataGeneratedAt = DateTime.now();
_apiGeneratedAt = apiGeneratedAt;
_isLoading = false;
_isRefreshing = false;
_replayToken++;
@ -250,35 +304,34 @@ class _ClaimsOverviewDashboardState extends State<ClaimsOverviewDashboard>
}
}
final generatedAt = DateTime.now();
await ClaimsOverviewCache.write(
branchId,
ClaimsOverviewCacheEntry(
policyId: policyId,
claimsKpiBySlug: claimsData.kpiBySlug,
enrollmentKpiBySlug: enrollmentData.kpiBySlug,
generatedAt: generatedAt,
generatedAt: apiGeneratedAt,
claimsLoadError: claimsData.loadError,
enrollmentLoadError: enrollmentData.loadError,
),
);
if (!mounted) return;
if (_isStaleLoad(loadId, policyId)) return;
setState(() {
_viewData = claimsData;
_enrollmentViewData = enrollmentData;
_dataGeneratedAt = generatedAt;
_apiGeneratedAt = apiGeneratedAt;
_isLoading = false;
_isRefreshing = false;
_replayToken++;
});
} catch (e) {
if (!mounted) return;
if (_isStaleLoad(loadId)) return;
setState(() {
_viewData = ClaimsOverviewViewData.empty(error: e.toString());
_enrollmentViewData =
EnrollmentOverviewViewData.empty(error: e.toString());
_dataGeneratedAt = null;
_apiGeneratedAt = null;
_isLoading = false;
_isRefreshing = false;
});
@ -295,7 +348,7 @@ class _ClaimsOverviewDashboardState extends State<ClaimsOverviewDashboard>
kpiBySlug: Map<String, dynamic>.from(entry.enrollmentKpiBySlug),
loadError: entry.enrollmentLoadError,
);
_dataGeneratedAt = entry.generatedAt;
_apiGeneratedAt = entry.generatedAt;
}
Future<void> _loadPolicyList(String token, String hrId) async {
@ -468,6 +521,8 @@ class _ClaimsOverviewDashboardState extends State<ClaimsOverviewDashboard>
policyId: _selectedPolicyId ?? '',
dashboardInfo: ClaimsPdfDashboardInfo(
policyLabel: _selectedPolicyLabel(),
clientName: _tokenService.getSelectedBranch()?['client_name']?.toString(),
branchName: _tokenService.getSelectedBranch()?['branch_name']?.toString(),
misCreationDate: _formattedGeneratedAt,
),
clientLogoBytes: _clientLogoBytes,
@ -477,6 +532,7 @@ class _ClaimsOverviewDashboardState extends State<ClaimsOverviewDashboard>
});
},
);
await _logHrActivity('export_hr_dashboard_data_pdf');
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
@ -636,7 +692,7 @@ class _ClaimsOverviewDashboardState extends State<ClaimsOverviewDashboard>
Row(
children: [
Text(
'Real-time policy & claims analytics',
'Policy & Claims Analytics',
style: GoogleFonts.poppins(
fontSize: 13,
color: ClaimsOverviewTheme.textSecondary,

View File

@ -50,15 +50,21 @@ const _minCaptureHeight = 360.0;
const _captureTimeout = Duration(seconds: 20);
const _pdfEmbedMaxWidth = 960;
const _logoAsset = 'assets/Nhance-Logo-Final 1.png';
const _pdfLogoHeight = 28.0;
const _pdfPageMargin = pw.EdgeInsets.fromLTRB(10, 14, 10, 14);
typedef ClaimsPdfExportProgress = void Function(int current, int total, String label);
class ClaimsPdfDashboardInfo {
final String policyLabel;
final String? clientName;
final String? branchName;
final String? misCreationDate;
const ClaimsPdfDashboardInfo({
required this.policyLabel,
this.clientName,
this.branchName,
this.misCreationDate,
});
}
@ -128,27 +134,30 @@ Future<Uint8List> _buildPdfBytesSync(_PdfBuildInput input) async {
final doc = pw.Document();
for (final capture in input.captures) {
for (var i = 0; i < input.captures.length; i++) {
final capture = input.captures[i];
final embed = _compressTabImageForPdf(capture.png);
final isFirstPage = i == 0;
doc.addPage(
pw.Page(
pageFormat: PdfPageFormat.a4.landscape,
margin: const pw.EdgeInsets.all(24),
margin: _pdfPageMargin,
build: (ctx) => pw.Column(
crossAxisAlignment: pw.CrossAxisAlignment.stretch,
children: [
_pdfDashboardHeader(
clientLogo: clientLogo,
logo: logo,
info: input.dashboardInfo,
),
pw.SizedBox(height: 8),
if (isFirstPage) ...[
_pdfDashboardHeader(
clientLogo: clientLogo,
logo: logo,
info: input.dashboardInfo,
),
pw.SizedBox(height: 8),
],
pw.Expanded(
child: pw.Center(
child: pw.Image(
pw.MemoryImage(embed),
fit: pw.BoxFit.contain,
),
child: pw.Image(
pw.MemoryImage(embed),
fit: pw.BoxFit.fitWidth,
alignment: pw.Alignment.topCenter,
),
),
],
@ -183,25 +192,27 @@ Future<Uint8List> _buildPdfBytesWithYields(
await Future<void>.delayed(Duration.zero);
final embed = _compressTabImageForPdf(capture.png);
final isFirstPage = i == 0;
doc.addPage(
pw.Page(
pageFormat: PdfPageFormat.a4.landscape,
margin: const pw.EdgeInsets.all(24),
margin: _pdfPageMargin,
build: (ctx) => pw.Column(
crossAxisAlignment: pw.CrossAxisAlignment.stretch,
children: [
_pdfDashboardHeader(
clientLogo: clientLogo,
logo: logo,
info: input.dashboardInfo,
),
pw.SizedBox(height: 8),
if (isFirstPage) ...[
_pdfDashboardHeader(
clientLogo: clientLogo,
logo: logo,
info: input.dashboardInfo,
),
pw.SizedBox(height: 8),
],
pw.Expanded(
child: pw.Center(
child: pw.Image(
pw.MemoryImage(embed),
fit: pw.BoxFit.contain,
),
child: pw.Image(
pw.MemoryImage(embed),
fit: pw.BoxFit.fitWidth,
alignment: pw.Alignment.topCenter,
),
),
],
@ -410,25 +421,60 @@ pw.Widget _pdfDashboardHeader({
}) {
final misDate = info.misCreationDate?.trim();
final hasMisDate = misDate != null && misDate.isNotEmpty;
final clientName = info.clientName?.trim() ?? '';
final branchName = info.branchName?.trim() ?? '';
final hasClientMeta = clientName.isNotEmpty || branchName.isNotEmpty;
return pw.Column(
crossAxisAlignment: pw.CrossAxisAlignment.stretch,
children: [
pw.Row(
crossAxisAlignment: pw.CrossAxisAlignment.center,
mainAxisAlignment: pw.MainAxisAlignment.spaceBetween,
children: [
if (clientLogo != null)
pw.Container(
height: 28,
constraints: const pw.BoxConstraints(maxWidth: 120),
height: _pdfLogoHeight,
alignment: pw.Alignment.centerLeft,
child: pw.Image(clientLogo, fit: pw.BoxFit.contain),
child: pw.Image(clientLogo, height: _pdfLogoHeight, fit: pw.BoxFit.contain),
)
else
pw.SizedBox.shrink(),
pw.SizedBox(height: _pdfLogoHeight),
if (hasClientMeta) ...[
pw.SizedBox(width: 4),
pw.Expanded(
child: pw.Column(
crossAxisAlignment: pw.CrossAxisAlignment.start,
mainAxisAlignment: pw.MainAxisAlignment.center,
children: [
if (clientName.isNotEmpty)
pw.Text(
clientName,
style: pw.TextStyle(
fontSize: 10,
fontWeight: pw.FontWeight.bold,
color: _pdfTextPrimary,
),
),
if (branchName.isNotEmpty)
pw.Text(
branchName,
style: const pw.TextStyle(
fontSize: 8,
color: _pdfTextSecondary,
),
),
],
),
),
] else
pw.Spacer(),
if (logo != null)
pw.Image(logo, height: 28, fit: pw.BoxFit.contain),
pw.Container(
height: _pdfLogoHeight,
width: _pdfLogoHeight * 3.2,
alignment: pw.Alignment.centerRight,
child: pw.Image(logo, fit: pw.BoxFit.contain),
),
],
),
pw.SizedBox(height: 10),
@ -451,7 +497,7 @@ pw.Widget _pdfDashboardHeader({
crossAxisAlignment: pw.WrapCrossAlignment.center,
children: [
pw.Text(
'Real-time policy & claims analytics',
'Policy & Claims Analytics',
style: const pw.TextStyle(
fontSize: 9,
color: _pdfTextSecondary,
@ -486,7 +532,7 @@ pw.Widget _pdfDashboardHeader({
),
pw.SizedBox(width: 12),
pw.Text(
'Branch : ${info.policyLabel}',
'Policy No : ${info.policyLabel}',
style: pw.TextStyle(
fontSize: 9,
fontWeight: pw.FontWeight.bold,

View File

@ -105,6 +105,7 @@ class _HrPolicyDetailsState extends State<hrPolicyDetails>
bool _isLoading = false;
bool _isSendingReminder = false;
bool _isLoadingPolicyTerms = false;
bool _isLoadingHospitalList = false;
// dynamic clintID;
late TabController _tabController;
// List<dynamic> dataPolicy = [];
@ -594,6 +595,7 @@ class _HrPolicyDetailsState extends State<hrPolicyDetails>
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']
@ -652,12 +654,14 @@ class _HrPolicyDetailsState extends State<hrPolicyDetails>
void exportToCsv(List<Map<String, dynamic>> data) {
List<List<String>> rows = [];
final isPost = localTokenType == 'post';
final idHeader = isPost ? 'TPA ID' : 'UHID';
// Header
rows.add([
'Emp Code',
'Name',
'UHID',
idHeader,
'Relationship',
'Date Of Birth',
'Gender',
@ -671,7 +675,7 @@ class _HrPolicyDetailsState extends State<hrPolicyDetails>
rows.add([
item['emp_code'] ?? '',
item['name'] ?? '',
item['uhid'] ?? '',
isPost ? (item['tpa_id'] ?? '') : (item['uhid'] ?? ''),
item['relationship'] ?? '',
item['formatted_dob'] ?? '',
item['gender'] ?? '',
@ -915,6 +919,76 @@ class _HrPolicyDetailsState extends State<hrPolicyDetails>
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);
@ -1329,7 +1403,7 @@ class _HrPolicyDetailsState extends State<hrPolicyDetails>
),
const SizedBox(width: 12),
SizedBox(
width: 142,
width: 210,
height: 37,
child: ElevatedButton(
onPressed: _isSendingReminder
@ -1355,7 +1429,7 @@ class _HrPolicyDetailsState extends State<hrPolicyDetails>
),
)
: Text(
'Reminder',
'Reminder Mail Template',
maxLines: 1,
softWrap: false,
style: GoogleFonts.poppins(
@ -1410,6 +1484,46 @@ class _HrPolicyDetailsState extends State<hrPolicyDetails>
),
),
const SizedBox(width: 12),
SizedBox(
width: 190,
height: 37,
child: ElevatedButton(
onPressed: _isLoadingHospitalList
? null
: _openNetworkHospitalListDialog,
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF009195),
elevation: 0,
padding: EdgeInsets.zero,
disabledBackgroundColor:
const Color(0xFF009195).withValues(alpha: 0.6),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
),
child: _isLoadingHospitalList
? const SizedBox(
width: 18,
height: 18,
child: CircularProgressIndicator(
strokeWidth: 2,
color: Colors.white,
),
)
: Text(
'Network Hospital List',
maxLines: 1,
softWrap: false,
overflow: TextOverflow.ellipsis,
style: GoogleFonts.poppins(
fontSize: 13,
fontWeight: FontWeight.w700,
color: Colors.white,
),
),
),
),
const SizedBox(width: 12),
],
SizedBox(
@ -1561,28 +1675,9 @@ class _HrPolicyDetailsState extends State<hrPolicyDetails>
if (localTokenType == "post")
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Container(
padding: const EdgeInsets.symmetric(
horizontal: 12, vertical: 8),
decoration: BoxDecoration(
color: const Color(0xFFF9EBBD),
borderRadius: BorderRadius.circular(6),
),
child: Text(
'Premium - ₹${localTotalPremium}',
style: GoogleFonts.poppins(
fontSize: 12,
fontWeight: FontWeight.w600,
color: const Color(0xFF009195),
),
),
),
const Spacer(),
_buildCompactSearchField(),
],
child: Align(
alignment: Alignment.centerRight,
child: _buildCompactSearchField(),
),
),
@ -1615,20 +1710,6 @@ class _HrPolicyDetailsState extends State<hrPolicyDetails>
const SizedBox(height: 48),
],
),
// ===================== FOOTER TEXT =====================
Positioned(
bottom: 12,
right: 16,
child: Text(
'(* Premium may vary subject to claims)',
style: GoogleFonts.poppins(
fontSize: 11,
color: Colors.red,
fontStyle: FontStyle.italic,
),
),
),
],
),
),
@ -2136,6 +2217,7 @@ class _HrPolicyDetailsState extends State<hrPolicyDetails>
pinned: true,
delegate: _CDHeaderDelegate(
showUHID: localPolicyTypeId != '6' && localPolicyTypeId != '7',
uhidHeaderLabel: localTokenType == 'post' ? 'TPA ID' : 'UHID',
showLoggedIn: localTokenType == "pre",
showAction:
localTokenType != "pre" && (hasAnyEcardLink || hasModule),
@ -2185,11 +2267,16 @@ class _HrPolicyDetailsState extends State<hrPolicyDetails>
),
),
/// UHID
/// UHID / TPA ID
if (localPolicyTypeId != '6' && localPolicyTypeId != '7')
Expanded(
flex: 2,
child: Text(item['uhid'] ?? '-', style: _dataBold),
child: Text(
localTokenType == 'post'
? (item['tpa_id'] ?? '-')
: (item['uhid'] ?? '-'),
style: _dataBold,
),
),
Expanded(
@ -3176,11 +3263,13 @@ class _HrPolicyDetailsState extends State<hrPolicyDetails>
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,
});
@ -3201,7 +3290,7 @@ class _CDHeaderDelegate extends SliverPersistentHeaderDelegate {
child: Row(
children: [
_headerCell('Name', 3),
if (showUHID) _headerCell('UHID', 2),
if (showUHID) _headerCell(uhidHeaderLabel, 2),
_headerCell('Relationship', 2),
_headerCell('Date Of Birth', 2),
_headerCell('Gender', 2),
@ -3234,6 +3323,101 @@ class _CDHeaderDelegate extends SliverPersistentHeaderDelegate {
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;

View File

@ -544,14 +544,6 @@ class _policiesState extends State<policies>
),
const SizedBox(height: 8),
Align(
alignment: Alignment.bottomRight,
child: Text(
'* Premium may vary subject to claims',
style: GoogleFonts.poppins(
fontSize: 10, color: Colors.red),
),
),
],
),
),
@ -579,7 +571,7 @@ class _PolicyGrid extends StatelessWidget {
final Function(String clientPolicyId)? onBulkDownload;
static const double _enrollmentCardHeight = 156;
static const double _activePolicyCardHeight = 170;
static const double _activePolicyCardHeight = 148;
const _PolicyGrid({
super.key,
@ -647,7 +639,7 @@ class _PolicyGrid extends StatelessWidget {
/// EB section
if (ebPolicies.isNotEmpty) ...[
Text(
'EB Policies',
'Employee Benefits',
style: GoogleFonts.poppins(
fontSize: 13,
fontWeight: FontWeight.w600,
@ -666,7 +658,7 @@ class _PolicyGrid extends StatelessWidget {
/// Non-EB section
if (nonEbPolicies.isNotEmpty) ...[
Text(
'Non-EB Policies',
'Non-Employee Benefits',
style: GoogleFonts.poppins(
fontSize: 13,
fontWeight: FontWeight.w600,
@ -1129,57 +1121,11 @@ class _ActivePolicyCardNew extends StatelessWidget {
: const Color(0xFFE9F6FB), // EB
borderRadius: BorderRadius.circular(12),
),
padding: const EdgeInsets.all(14),
padding: const EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
/// PREMIUM
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Text(
'Premium - ₹${data['total_premium'] ?? ''}*',
style: GoogleFonts.poppins(
fontSize: 12,
color: Color(0xFF009195),
fontWeight: FontWeight.w600,
),
),
),
// 🔥 ICON FLOATING ABOVE CARD
if (data['is_ecard_bulk_download'] == 1)
Positioned(
top: 10,
right: 10,
child: GestureDetector(
onTap: () {
logDebug(
'ICON CLICKED ${data['client_policy_id']}');
onBulkDownload?.call(
data['client_policy_id'].toString(),
);
},
child: Container(
padding: const EdgeInsets.all(6),
decoration: BoxDecoration(
color: const Color(0xFF009195),
borderRadius: BorderRadius.circular(6),
),
child: const Icon(
Icons.credit_card,
color: Colors.white,
size: 16,
),
),
),
),
],
),
const SizedBox(height: 4),
/// POLICY NO + ICON
/// POLICY NO + optional ecard bulk download
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
@ -1193,10 +1139,32 @@ class _ActivePolicyCardNew extends StatelessWidget {
overflow: TextOverflow.ellipsis,
),
),
if (data['is_ecard_bulk_download'] == 1)
GestureDetector(
onTap: () {
logDebug(
'ICON CLICKED ${data['client_policy_id']}');
onBulkDownload?.call(
data['client_policy_id'].toString(),
);
},
child: Container(
padding: const EdgeInsets.all(6),
decoration: BoxDecoration(
color: const Color(0xFF009195),
borderRadius: BorderRadius.circular(6),
),
child: const Icon(
Icons.credit_card,
color: Colors.white,
size: 16,
),
),
),
],
),
const SizedBox(height: 4),
const SizedBox(height: 2),
/// Insurer
Tooltip(
@ -1232,7 +1200,7 @@ class _ActivePolicyCardNew extends StatelessWidget {
),
],
const SizedBox(height: 4),
const SizedBox(height: 2),
/// DATE RANGE
Text(
@ -1243,7 +1211,7 @@ class _ActivePolicyCardNew extends StatelessWidget {
),
),
const SizedBox(height: 10),
const SizedBox(height: 8),
/// ACTIVE / INACTIVE
Row(