From 5ab200618cbab071cdf105aed3f80f223a5ef219 Mon Sep 17 00:00:00 2001 From: SurendarSuri30 Date: Mon, 3 Aug 2026 10:20:03 +0530 Subject: [PATCH] hr feedbacks --- lib/customAppBar/side_bar.dart | 231 ++++---------- lib/customAppBar/top_app_bar.dart | 296 ++++++++++++++---- .../claims_overview_cache.dart | 13 +- .../claims_overview_dashboard.dart | 102 ++++-- .../claims_overview_pdf_export.dart | 112 +++++-- lib/presentation/hrPolicyDetails.dart | 270 +++++++++++++--- lib/presentation/policies.dart | 92 ++---- 7 files changed, 717 insertions(+), 399 deletions(-) diff --git a/lib/customAppBar/side_bar.dart b/lib/customAppBar/side_bar.dart index 1742aa7..d582e33 100644 --- a/lib/customAppBar/side_bar.dart +++ b/lib/customAppBar/side_bar.dart @@ -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 { } // 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 { }); } + /// 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 logout(BuildContext context) async { // final prefs = await SharedPreferences.getInstance(); // final String? hrtoken = prefs.getString('_postToken'); @@ -149,6 +167,8 @@ class _NhanceSideBarState extends State { } void _showClaimsSubMenu(GlobalKey key) { + if (!_hasMultipleClaimsSubMenus) return; + if (_claimsMenuSuppressUntil != null && DateTime.now().isBefore(_claimsMenuSuppressUntil!)) { return; @@ -161,6 +181,7 @@ class _NhanceSideBarState extends State { 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 { 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 { 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 { 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 { ), 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> _level1Contacts = []; List> _level2Contacts = []; - Map _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 _parseHospitalLinks(dynamic raw) { - if (raw is! Map) return {}; - final parsed = {}; - raw.forEach((key, value) { - final name = key.toString().trim(); - final url = value?.toString().trim() ?? ''; - if (name.isNotEmpty && url.isNotEmpty) { - parsed[name] = url; - } - }); - return parsed; - } - - Future _openHospitalUrl(String url) async { - final uri = Uri.tryParse(url); - if (uri == null) return; - await launchUrl(uri, webOnlyWindowName: '_blank'); - } - Widget _buildContactCard(Map 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)), + ), + ], ); } } diff --git a/lib/customAppBar/top_app_bar.dart b/lib/customAppBar/top_app_bar.dart index 1bf6527..e37f21a 100644 --- a/lib/customAppBar/top_app_bar.dart +++ b/lib/customAppBar/top_app_bar.dart @@ -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 { List> branches = []; Map? selectedBranch; + /// client_id → logo URL + final Map _logoByClientId = {}; + @override void initState() { super.initState(); @@ -29,6 +35,98 @@ class _NhanceTopBarState extends State { branches = tokenStorage.getCombinedBranches(); selectedBranch = tokenStorage.getSelectedBranch(); setState(() {}); + _loadBranchLogos(); + } + + String _clientKey(Map branch) { + return (branch['client_id'] ?? branch['id'] ?? '').toString(); + } + + String? _logoFromBranch(Map 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 _loadBranchLogos() async { + final uniqueBranches = >{}; + 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 _fetchClientLogoUrl(Map 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 _getBranchSwitchRoute() async { @@ -60,19 +158,26 @@ class _NhanceTopBarState extends State { @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 { class _BranchPopup extends StatelessWidget { final String clientName; final String branchName; + final String? selectedLogoUrl; final List> branches; + final Map logoByClientId; + final String Function(Map) clientKeyBuilder; final Function(Map) 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>( 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), ), ); } diff --git a/lib/presentation/claims_overview/claims_overview_cache.dart b/lib/presentation/claims_overview/claims_overview_cache.dart index cf84f23..db0db68 100644 --- a/lib/presentation/claims_overview/claims_overview_cache.dart +++ b/lib/presentation/claims_overview/claims_overview_cache.dart @@ -6,7 +6,8 @@ class ClaimsOverviewCacheEntry { final String policyId; final Map claimsKpiBySlug; final Map 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 json) { + final raw = json['generatedAt']?.toString().trim(); return ClaimsOverviewCacheEntry( policyId: json['policyId']?.toString() ?? '', claimsKpiBySlug: Map.from(json['claimsKpiBySlug'] ?? {}), enrollmentKpiBySlug: Map.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'; diff --git a/lib/presentation/claims_overview/claims_overview_dashboard.dart b/lib/presentation/claims_overview/claims_overview_dashboard.dart index a17524c..c0671bc 100644 --- a/lib/presentation/claims_overview/claims_overview_dashboard.dart +++ b/lib/presentation/claims_overview/claims_overview_dashboard.dart @@ -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 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 String? _selectedPolicyId; List> _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 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 if (!mounted) return; setState(() => _sessionChecked = true); + _logHrActivity('opened_hr_dashboard'); await _loadDashboard(); } + Future _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 }); } + bool _isStaleLoad(int loadId, [String? policyId]) { + if (!mounted || loadId != _loadGeneration) return true; + if (policyId != null && policyId != _selectedPolicyId) return true; + return false; + } + Future _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 _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 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 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 clientPolicyId: policyId, ), ); + if (_isStaleLoad(loadId, policyId)) return; var enrollmentData = _parseEnrollmentResponse(enrollmentResponse); final selectedPolicy = _selectedPolicy(); @@ -236,11 +290,11 @@ class _ClaimsOverviewDashboardState extends State 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 } } - 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 kpiBySlug: Map.from(entry.enrollmentKpiBySlug), loadError: entry.enrollmentLoadError, ); - _dataGeneratedAt = entry.generatedAt; + _apiGeneratedAt = entry.generatedAt; } Future _loadPolicyList(String token, String hrId) async { @@ -468,6 +521,8 @@ class _ClaimsOverviewDashboardState extends State 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 }); }, ); + await _logHrActivity('export_hr_dashboard_data_pdf'); if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( SnackBar( @@ -636,7 +692,7 @@ class _ClaimsOverviewDashboardState extends State Row( children: [ Text( - 'Real-time policy & claims analytics', + 'Policy & Claims Analytics', style: GoogleFonts.poppins( fontSize: 13, color: ClaimsOverviewTheme.textSecondary, diff --git a/lib/presentation/claims_overview/claims_overview_pdf_export.dart b/lib/presentation/claims_overview/claims_overview_pdf_export.dart index 6ec24e5..859a860 100644 --- a/lib/presentation/claims_overview/claims_overview_pdf_export.dart +++ b/lib/presentation/claims_overview/claims_overview_pdf_export.dart @@ -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 _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 _buildPdfBytesWithYields( await Future.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, diff --git a/lib/presentation/hrPolicyDetails.dart b/lib/presentation/hrPolicyDetails.dart index 283c716..dfab720 100755 --- a/lib/presentation/hrPolicyDetails.dart +++ b/lib/presentation/hrPolicyDetails.dart @@ -105,6 +105,7 @@ class _HrPolicyDetailsState extends State bool _isLoading = false; bool _isSendingReminder = false; bool _isLoadingPolicyTerms = false; + bool _isLoadingHospitalList = false; // dynamic clintID; late TabController _tabController; // List dataPolicy = []; @@ -594,6 +595,7 @@ class _HrPolicyDetailsState extends State 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 void exportToCsv(List> data) { List> 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 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 return _capitalize(status); } + Future _openNetworkHospitalListDialog() async { + setState(() => _isLoadingHospitalList = true); + + try { + final clientId = await tokenService.readValue('empClientId'); + final token = localToken ?? + widget.Token ?? + tokenService.getCurrentToken() ?? + ''; + + if (clientId == null || + clientId.toString().isEmpty || + token.toString().isEmpty) { + if (mounted) { + ToastHelper.showErrorToast(context, 'Client details not found'); + } + return; + } + + final response = await apiService.getClientRMApi( + clientId.toString(), + token.toString(), + ); + + if (!mounted) return; + + if (response['status'] != 'success' || response['data'] is! Map) { + ToastHelper.showErrorToast( + context, + response['message']?.toString() ?? + 'Failed to load network hospital list', + ); + return; + } + + final data = Map.from(response['data'] as Map); + final hospitalLinks = _parseHospitalLinks(data['tpa_network_hospitals']); + + await showDialog( + context: context, + builder: (dialogContext) => _NetworkHospitalListDialog( + hospitalLinks: hospitalLinks, + ), + ); + } catch (e) { + logDebug('Network hospital list failed: $e'); + if (mounted) { + ToastHelper.showErrorToast( + context, + 'Failed to load network hospital list', + ); + } + } finally { + if (mounted) setState(() => _isLoadingHospitalList = false); + } + } + + Map _parseHospitalLinks(dynamic raw) { + if (raw is! Map) return {}; + final parsed = {}; + raw.forEach((key, value) { + final name = key.toString().trim(); + final url = value?.toString().trim() ?? ''; + if (name.isNotEmpty && url.isNotEmpty) { + parsed[name] = url; + } + }); + return parsed; + } + Future _openPolicyTermsDialog() async { setState(() => _isLoadingPolicyTerms = true); @@ -1329,7 +1403,7 @@ class _HrPolicyDetailsState extends State ), const SizedBox(width: 12), SizedBox( - width: 142, + width: 210, height: 37, child: ElevatedButton( onPressed: _isSendingReminder @@ -1355,7 +1429,7 @@ class _HrPolicyDetailsState extends State ), ) : Text( - 'Reminder', + 'Reminder Mail Template', maxLines: 1, softWrap: false, style: GoogleFonts.poppins( @@ -1410,6 +1484,46 @@ class _HrPolicyDetailsState extends State ), ), 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 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 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 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 ), ), - /// 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 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 hospitalLinks; + + const _NetworkHospitalListDialog({required this.hospitalLinks}); + + Future _openHospitalUrl(String url) async { + final uri = Uri.tryParse(url); + if (uri == null) return; + await launchUrl(uri, webOnlyWindowName: '_blank'); + } + + @override + Widget build(BuildContext context) { + final maxHeight = MediaQuery.of(context).size.height * 0.65; + final cappedMaxHeight = maxHeight.clamp(280.0, 520.0); + + return AlertDialog( + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), + title: Text( + 'Network Hospital List', + style: GoogleFonts.poppins(fontWeight: FontWeight.w600, fontSize: 18), + ), + content: SizedBox( + width: 520, + child: ConstrainedBox( + constraints: BoxConstraints(maxHeight: cappedMaxHeight), + child: hospitalLinks.isEmpty + ? Center( + child: Text( + 'No Network Hospital List', + style: GoogleFonts.poppins( + fontSize: 14, + color: Colors.black54, + ), + ), + ) + : ListView.separated( + shrinkWrap: true, + itemCount: hospitalLinks.length, + separatorBuilder: (_, __) => const SizedBox(height: 10), + itemBuilder: (context, index) { + final entry = hospitalLinks.entries.elementAt(index); + return InkWell( + onTap: () => _openHospitalUrl(entry.value), + borderRadius: BorderRadius.circular(8), + child: Container( + width: double.infinity, + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: const Color(0xFFF5F5F5), + borderRadius: BorderRadius.circular(8), + border: Border.all(color: const Color(0xFFE8E8E8)), + ), + child: Row( + children: [ + Expanded( + child: Text( + entry.key, + style: GoogleFonts.poppins( + fontSize: 13, + fontWeight: FontWeight.w600, + color: const Color(0xFF009195), + ), + ), + ), + const SizedBox(width: 8), + const Icon( + Icons.open_in_new, + size: 16, + color: Color(0xFF009195), + ), + ], + ), + ), + ); + }, + ), + ), + ), + actions: [ + ElevatedButton( + onPressed: () => Navigator.pop(context), + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFF009195), + ), + child: Text( + 'Close', + style: GoogleFonts.poppins(color: Colors.white), + ), + ), + ], + ); + } +} + class _PolicyTermsDialog extends StatelessWidget { final Map terms; diff --git a/lib/presentation/policies.dart b/lib/presentation/policies.dart index b04db5a..687efd1 100644 --- a/lib/presentation/policies.dart +++ b/lib/presentation/policies.dart @@ -544,14 +544,6 @@ class _policiesState extends State ), 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(