hr feedbacks
This commit is contained in:
parent
1e97c41545
commit
5ab200618c
@ -7,7 +7,6 @@ import 'package:nhancepolicy/service/api_service.dart';
|
|||||||
import 'package:nhancepolicy/service/svg_service.dart';
|
import 'package:nhancepolicy/service/svg_service.dart';
|
||||||
import 'package:nhancepolicy/service/token_storage_service.dart';
|
import 'package:nhancepolicy/service/token_storage_service.dart';
|
||||||
import 'package:nhancepolicy/logger.dart';
|
import 'package:nhancepolicy/logger.dart';
|
||||||
import 'package:url_launcher/url_launcher.dart';
|
|
||||||
|
|
||||||
class NhanceSideBar extends StatefulWidget {
|
class NhanceSideBar extends StatefulWidget {
|
||||||
const NhanceSideBar({super.key});
|
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
|
// CLAIMS — only when module 4 and at least one sub-menu is allowed
|
||||||
if (postModules.contains(4) &&
|
if (postModules.contains(4) && _claimsSubMenuRoutes.isNotEmpty) {
|
||||||
(claimsSubMenu.contains('EB') || claimsSubMenu.contains('Non-EB'))) {
|
|
||||||
items.add({
|
items.add({
|
||||||
'route': 'ClaimsPolicies',
|
'route': 'ClaimsPolicies',
|
||||||
'label': 'Claims',
|
'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 {
|
Future<void> logout(BuildContext context) async {
|
||||||
// final prefs = await SharedPreferences.getInstance();
|
// final prefs = await SharedPreferences.getInstance();
|
||||||
// final String? hrtoken = prefs.getString('_postToken');
|
// final String? hrtoken = prefs.getString('_postToken');
|
||||||
@ -149,6 +167,8 @@ class _NhanceSideBarState extends State<NhanceSideBar> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _showClaimsSubMenu(GlobalKey key) {
|
void _showClaimsSubMenu(GlobalKey key) {
|
||||||
|
if (!_hasMultipleClaimsSubMenus) return;
|
||||||
|
|
||||||
if (_claimsMenuSuppressUntil != null &&
|
if (_claimsMenuSuppressUntil != null &&
|
||||||
DateTime.now().isBefore(_claimsMenuSuppressUntil!)) {
|
DateTime.now().isBefore(_claimsMenuSuppressUntil!)) {
|
||||||
return;
|
return;
|
||||||
@ -161,6 +181,7 @@ class _NhanceSideBarState extends State<NhanceSideBar> {
|
|||||||
if (box == null) return;
|
if (box == null) return;
|
||||||
|
|
||||||
final offset = box.localToGlobal(Offset.zero);
|
final offset = box.localToGlobal(Offset.zero);
|
||||||
|
final subMenus = _claimsSubMenuRoutes;
|
||||||
_claimsOverlayEntry?.remove();
|
_claimsOverlayEntry?.remove();
|
||||||
_claimsOverlayEntry = OverlayEntry(
|
_claimsOverlayEntry = OverlayEntry(
|
||||||
builder: (context) => Positioned(
|
builder: (context) => Positioned(
|
||||||
@ -177,7 +198,7 @@ class _NhanceSideBarState extends State<NhanceSideBar> {
|
|||||||
child: Material(
|
child: Material(
|
||||||
color: Colors.transparent,
|
color: Colors.transparent,
|
||||||
child: Container(
|
child: Container(
|
||||||
width: 120,
|
width: 200,
|
||||||
padding: const EdgeInsets.symmetric(vertical: 6),
|
padding: const EdgeInsets.symmetric(vertical: 6),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
@ -193,10 +214,8 @@ class _NhanceSideBarState extends State<NhanceSideBar> {
|
|||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
if (claimsSubMenu.contains('EB'))
|
for (final item in subMenus)
|
||||||
_buildClaimsMenuItem('EB', 'ClaimsPolicies'),
|
_buildClaimsMenuItem(item.label, item.route),
|
||||||
if (claimsSubMenu.contains('Non-EB'))
|
|
||||||
_buildClaimsMenuItem('Non-EB', 'nonEBClaimsList'),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -341,6 +360,9 @@ class _NhanceSideBarState extends State<NhanceSideBar> {
|
|||||||
final isClaimsActive =
|
final isClaimsActive =
|
||||||
activeRoute == 'ClaimsPolicies' || activeRoute == 'nonEBClaimsList';
|
activeRoute == 'ClaimsPolicies' || activeRoute == 'nonEBClaimsList';
|
||||||
final itemKey = GlobalKey();
|
final itemKey = GlobalKey();
|
||||||
|
final showClaimsSubMenu = isClaims && _hasMultipleClaimsSubMenus;
|
||||||
|
final claimsDirectRoute =
|
||||||
|
isClaims ? (_singleClaimsRoute ?? 'ClaimsPolicies') : null;
|
||||||
|
|
||||||
return _SideItem(
|
return _SideItem(
|
||||||
key: itemKey,
|
key: itemKey,
|
||||||
@ -355,14 +377,18 @@ class _NhanceSideBarState extends State<NhanceSideBar> {
|
|||||||
),
|
),
|
||||||
label: item['label'],
|
label: item['label'],
|
||||||
isActive: isClaims ? isClaimsActive : activeRoute == item['route'],
|
isActive: isClaims ? isClaimsActive : activeRoute == item['route'],
|
||||||
onTap: isClaims ? null : () => _navigate(item['route']),
|
onTap: showClaimsSubMenu
|
||||||
onHoverEnter: isClaims
|
? null
|
||||||
|
: () => _navigate(
|
||||||
|
claimsDirectRoute ?? item['route'] as String,
|
||||||
|
),
|
||||||
|
onHoverEnter: showClaimsSubMenu
|
||||||
? () {
|
? () {
|
||||||
_isHoveringClaimsItem = true;
|
_isHoveringClaimsItem = true;
|
||||||
_showClaimsSubMenu(itemKey);
|
_showClaimsSubMenu(itemKey);
|
||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
onHoverExit: isClaims
|
onHoverExit: showClaimsSubMenu
|
||||||
? () {
|
? () {
|
||||||
_isHoveringClaimsItem = false;
|
_isHoveringClaimsItem = false;
|
||||||
_scheduleCloseClaimsMenu();
|
_scheduleCloseClaimsMenu();
|
||||||
@ -443,7 +469,6 @@ class _HelpContactDialogState extends State<_HelpContactDialog> {
|
|||||||
bool _isLoading = false;
|
bool _isLoading = false;
|
||||||
List<Map<String, dynamic>> _level1Contacts = [];
|
List<Map<String, dynamic>> _level1Contacts = [];
|
||||||
List<Map<String, dynamic>> _level2Contacts = [];
|
List<Map<String, dynamic>> _level2Contacts = [];
|
||||||
Map<String, String> _hospitalLinks = {};
|
|
||||||
String? _errorMessage;
|
String? _errorMessage;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -487,7 +512,6 @@ class _HelpContactDialogState extends State<_HelpContactDialog> {
|
|||||||
setState(() {
|
setState(() {
|
||||||
_level1Contacts = _parseContacts(data['level_1']);
|
_level1Contacts = _parseContacts(data['level_1']);
|
||||||
_level2Contacts = _parseContacts(data['level_2']);
|
_level2Contacts = _parseContacts(data['level_2']);
|
||||||
_hospitalLinks = _parseHospitalLinks(data['tpa_network_hospitals']);
|
|
||||||
_isLoading = false;
|
_isLoading = false;
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
@ -516,25 +540,6 @@ class _HelpContactDialogState extends State<_HelpContactDialog> {
|
|||||||
.toList();
|
.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) {
|
Widget _buildContactCard(Map<String, dynamic> contact) {
|
||||||
final name = contact['first_name']?.toString() ?? '-';
|
final name = contact['first_name']?.toString() ?? '-';
|
||||||
final email = contact['email']?.toString() ?? '-';
|
final email = contact['email']?.toString() ?? '-';
|
||||||
@ -649,139 +654,24 @@ 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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final maxHeight = MediaQuery.of(context).size.height * 0.65;
|
final maxHeight = MediaQuery.of(context).size.height * 0.65;
|
||||||
final cappedMaxHeight = maxHeight.clamp(280.0, 520.0);
|
final cappedMaxHeight = maxHeight.clamp(280.0, 520.0);
|
||||||
|
|
||||||
return DefaultTabController(
|
return AlertDialog(
|
||||||
length: 2,
|
|
||||||
child: AlertDialog(
|
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
||||||
title: Column(
|
title: Text(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
'Help',
|
'Help',
|
||||||
style:
|
style: GoogleFonts.poppins(fontWeight: FontWeight.w600, fontSize: 18),
|
||||||
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(
|
content: SizedBox(
|
||||||
width: 560,
|
width: 560,
|
||||||
child: ConstrainedBox(
|
child: ConstrainedBox(
|
||||||
constraints: BoxConstraints(maxHeight: cappedMaxHeight),
|
constraints: BoxConstraints(maxHeight: cappedMaxHeight),
|
||||||
child: TabBarView(
|
child: Scrollbar(
|
||||||
children: [
|
|
||||||
Scrollbar(
|
|
||||||
child: SingleChildScrollView(child: _buildContent()),
|
child: SingleChildScrollView(child: _buildContent()),
|
||||||
),
|
),
|
||||||
Scrollbar(
|
|
||||||
child: SingleChildScrollView(
|
|
||||||
child: _buildHospitalListContent(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
actions: [
|
actions: [
|
||||||
@ -793,7 +683,6 @@ class _HelpContactDialogState extends State<_HelpContactDialog> {
|
|||||||
child: Text('Close', style: GoogleFonts.poppins(color: Colors.white)),
|
child: Text('Close', style: GoogleFonts.poppins(color: Colors.white)),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,9 @@
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:http/http.dart' as http;
|
||||||
|
|
||||||
|
import '../config/environment.dart';
|
||||||
import '../service/token_storage_service.dart';
|
import '../service/token_storage_service.dart';
|
||||||
|
|
||||||
class NhanceTopBar extends StatefulWidget implements PreferredSizeWidget {
|
class NhanceTopBar extends StatefulWidget implements PreferredSizeWidget {
|
||||||
@ -19,6 +22,9 @@ class _NhanceTopBarState extends State<NhanceTopBar> {
|
|||||||
List<Map<String, dynamic>> branches = [];
|
List<Map<String, dynamic>> branches = [];
|
||||||
Map<String, dynamic>? selectedBranch;
|
Map<String, dynamic>? selectedBranch;
|
||||||
|
|
||||||
|
/// client_id → logo URL
|
||||||
|
final Map<String, String> _logoByClientId = {};
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
@ -29,6 +35,98 @@ class _NhanceTopBarState extends State<NhanceTopBar> {
|
|||||||
branches = tokenStorage.getCombinedBranches();
|
branches = tokenStorage.getCombinedBranches();
|
||||||
selectedBranch = tokenStorage.getSelectedBranch();
|
selectedBranch = tokenStorage.getSelectedBranch();
|
||||||
setState(() {});
|
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 {
|
Future<String> _getBranchSwitchRoute() async {
|
||||||
@ -60,19 +158,26 @@ class _NhanceTopBarState extends State<NhanceTopBar> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final selectedLogo = selectedBranch == null
|
||||||
|
? null
|
||||||
|
: _logoByClientId[_clientKey(selectedBranch!)];
|
||||||
|
|
||||||
return AppBar(
|
return AppBar(
|
||||||
automaticallyImplyLeading: false,
|
automaticallyImplyLeading: false,
|
||||||
backgroundColor: const Color(0xFFBFEFEF),
|
backgroundColor: const Color(0xFFBFEFEF),
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
title: Row(
|
title: Row(
|
||||||
children: [
|
children: [
|
||||||
Image.asset('assets/nhance_client_logo.png', height: 36),
|
Image.asset('assets/nhance_client_logo.png', height: 32),
|
||||||
const Spacer(),
|
const Spacer(),
|
||||||
if (selectedBranch != null)
|
if (selectedBranch != null)
|
||||||
_BranchPopup(
|
_BranchPopup(
|
||||||
clientName: selectedBranch!['client_name'],
|
clientName: selectedBranch!['client_name']?.toString() ?? '',
|
||||||
branchName: selectedBranch!['branch_name'],
|
branchName: selectedBranch!['branch_name']?.toString() ?? '',
|
||||||
|
selectedLogoUrl: selectedLogo,
|
||||||
branches: branches,
|
branches: branches,
|
||||||
|
logoByClientId: _logoByClientId,
|
||||||
|
clientKeyBuilder: _clientKey,
|
||||||
onSelected: _onBranchSelected,
|
onSelected: _onBranchSelected,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@ -84,13 +189,19 @@ class _NhanceTopBarState extends State<NhanceTopBar> {
|
|||||||
class _BranchPopup extends StatelessWidget {
|
class _BranchPopup extends StatelessWidget {
|
||||||
final String clientName;
|
final String clientName;
|
||||||
final String branchName;
|
final String branchName;
|
||||||
|
final String? selectedLogoUrl;
|
||||||
final List<Map<String, dynamic>> branches;
|
final List<Map<String, dynamic>> branches;
|
||||||
|
final Map<String, String> logoByClientId;
|
||||||
|
final String Function(Map<String, dynamic>) clientKeyBuilder;
|
||||||
final Function(Map<String, dynamic>) onSelected;
|
final Function(Map<String, dynamic>) onSelected;
|
||||||
|
|
||||||
const _BranchPopup({
|
const _BranchPopup({
|
||||||
required this.clientName,
|
required this.clientName,
|
||||||
required this.branchName,
|
required this.branchName,
|
||||||
|
required this.selectedLogoUrl,
|
||||||
required this.branches,
|
required this.branches,
|
||||||
|
required this.logoByClientId,
|
||||||
|
required this.clientKeyBuilder,
|
||||||
required this.onSelected,
|
required this.onSelected,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -102,13 +213,16 @@ class _BranchPopup extends StatelessWidget {
|
|||||||
onSelected: onSelected,
|
onSelected: onSelected,
|
||||||
itemBuilder: (context) {
|
itemBuilder: (context) {
|
||||||
return branches.map((branch) {
|
return branches.map((branch) {
|
||||||
|
final logoUrl = logoByClientId[clientKeyBuilder(branch)];
|
||||||
return PopupMenuItem<Map<String, dynamic>>(
|
return PopupMenuItem<Map<String, dynamic>>(
|
||||||
value: branch,
|
value: branch,
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
|
_BranchLogo(logoUrl: logoUrl, size: 24),
|
||||||
|
const SizedBox(width: 8),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Text(
|
child: Text(
|
||||||
branch['client_name'],
|
branch['client_name']?.toString() ?? '',
|
||||||
style: const TextStyle(fontSize: 13),
|
style: const TextStyle(fontSize: 13),
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
),
|
),
|
||||||
@ -116,21 +230,29 @@ class _BranchPopup extends StatelessWidget {
|
|||||||
const SizedBox(width: 10),
|
const SizedBox(width: 10),
|
||||||
const Icon(Icons.location_on, size: 14, color: Colors.grey),
|
const Icon(Icons.location_on, size: 14, color: Colors.grey),
|
||||||
const SizedBox(width: 4),
|
const SizedBox(width: 4),
|
||||||
Text(
|
Flexible(
|
||||||
branch['branch_name'],
|
child: Text(
|
||||||
|
branch['branch_name']?.toString() ?? '',
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
color: Colors.grey,
|
color: Colors.grey,
|
||||||
),
|
),
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}).toList();
|
}).toList();
|
||||||
},
|
},
|
||||||
child: Container(
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
_BranchLogo(logoUrl: selectedLogoUrl, size: 32),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Container(
|
||||||
height: 40,
|
height: 40,
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
borderRadius: BorderRadius.circular(22),
|
borderRadius: BorderRadius.circular(22),
|
||||||
@ -144,7 +266,6 @@ class _BranchPopup extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
// CLIENT NAME
|
|
||||||
Text(
|
Text(
|
||||||
clientName,
|
clientName,
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
@ -152,10 +273,7 @@ class _BranchPopup extends StatelessWidget {
|
|||||||
fontWeight: FontWeight.w500,
|
fontWeight: FontWeight.w500,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
|
|
||||||
// 📍 BRANCH NAME (SELECTED)
|
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
const Icon(
|
const Icon(
|
||||||
@ -173,10 +291,7 @@ class _BranchPopup extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
||||||
const SizedBox(width: 6),
|
const SizedBox(width: 6),
|
||||||
|
|
||||||
// DROPDOWN ARROW
|
|
||||||
const Icon(
|
const Icon(
|
||||||
Icons.keyboard_arrow_down,
|
Icons.keyboard_arrow_down,
|
||||||
size: 20,
|
size: 20,
|
||||||
@ -185,6 +300,65 @@ class _BranchPopup extends StatelessWidget {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,7 +6,8 @@ class ClaimsOverviewCacheEntry {
|
|||||||
final String policyId;
|
final String policyId;
|
||||||
final Map<String, dynamic> claimsKpiBySlug;
|
final Map<String, dynamic> claimsKpiBySlug;
|
||||||
final Map<String, dynamic> enrollmentKpiBySlug;
|
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? claimsLoadError;
|
||||||
final String? enrollmentLoadError;
|
final String? enrollmentLoadError;
|
||||||
|
|
||||||
@ -14,19 +15,19 @@ class ClaimsOverviewCacheEntry {
|
|||||||
required this.policyId,
|
required this.policyId,
|
||||||
required this.claimsKpiBySlug,
|
required this.claimsKpiBySlug,
|
||||||
required this.enrollmentKpiBySlug,
|
required this.enrollmentKpiBySlug,
|
||||||
required this.generatedAt,
|
this.generatedAt,
|
||||||
this.claimsLoadError,
|
this.claimsLoadError,
|
||||||
this.enrollmentLoadError,
|
this.enrollmentLoadError,
|
||||||
});
|
});
|
||||||
|
|
||||||
factory ClaimsOverviewCacheEntry.fromJson(Map<String, dynamic> json) {
|
factory ClaimsOverviewCacheEntry.fromJson(Map<String, dynamic> json) {
|
||||||
|
final raw = json['generatedAt']?.toString().trim();
|
||||||
return ClaimsOverviewCacheEntry(
|
return ClaimsOverviewCacheEntry(
|
||||||
policyId: json['policyId']?.toString() ?? '',
|
policyId: json['policyId']?.toString() ?? '',
|
||||||
claimsKpiBySlug: Map<String, dynamic>.from(json['claimsKpiBySlug'] ?? {}),
|
claimsKpiBySlug: Map<String, dynamic>.from(json['claimsKpiBySlug'] ?? {}),
|
||||||
enrollmentKpiBySlug:
|
enrollmentKpiBySlug:
|
||||||
Map<String, dynamic>.from(json['enrollmentKpiBySlug'] ?? {}),
|
Map<String, dynamic>.from(json['enrollmentKpiBySlug'] ?? {}),
|
||||||
generatedAt: DateTime.tryParse(json['generatedAt']?.toString() ?? '') ??
|
generatedAt: (raw != null && raw.isNotEmpty) ? raw : null,
|
||||||
DateTime.now(),
|
|
||||||
claimsLoadError: json['claimsLoadError']?.toString(),
|
claimsLoadError: json['claimsLoadError']?.toString(),
|
||||||
enrollmentLoadError: json['enrollmentLoadError']?.toString(),
|
enrollmentLoadError: json['enrollmentLoadError']?.toString(),
|
||||||
);
|
);
|
||||||
@ -36,14 +37,14 @@ class ClaimsOverviewCacheEntry {
|
|||||||
'policyId': policyId,
|
'policyId': policyId,
|
||||||
'claimsKpiBySlug': claimsKpiBySlug,
|
'claimsKpiBySlug': claimsKpiBySlug,
|
||||||
'enrollmentKpiBySlug': enrollmentKpiBySlug,
|
'enrollmentKpiBySlug': enrollmentKpiBySlug,
|
||||||
'generatedAt': generatedAt.toIso8601String(),
|
'generatedAt': generatedAt,
|
||||||
'claimsLoadError': claimsLoadError,
|
'claimsLoadError': claimsLoadError,
|
||||||
'enrollmentLoadError': enrollmentLoadError,
|
'enrollmentLoadError': enrollmentLoadError,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract final class ClaimsOverviewCache {
|
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) =>
|
static String _key(String branchId, String policyId) =>
|
||||||
'$_keyPrefix${branchId}_$policyId';
|
'$_keyPrefix${branchId}_$policyId';
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import 'package:intl/intl.dart';
|
|||||||
|
|
||||||
import '../../customAppBar/base_layout.dart';
|
import '../../customAppBar/base_layout.dart';
|
||||||
import '../../customAppBar/toastHelper.dart';
|
import '../../customAppBar/toastHelper.dart';
|
||||||
|
import '../../logger.dart';
|
||||||
import '../../service/api_service.dart';
|
import '../../service/api_service.dart';
|
||||||
import '../../service/secure_pop_scope.dart';
|
import '../../service/secure_pop_scope.dart';
|
||||||
import '../../service/token_storage_service.dart';
|
import '../../service/token_storage_service.dart';
|
||||||
@ -43,6 +44,7 @@ class _ClaimsOverviewDashboardState extends State<ClaimsOverviewDashboard>
|
|||||||
bool _isExportingPdf = false;
|
bool _isExportingPdf = false;
|
||||||
bool _sessionChecked = false;
|
bool _sessionChecked = false;
|
||||||
int _replayToken = 0;
|
int _replayToken = 0;
|
||||||
|
int _loadGeneration = 0;
|
||||||
int _currentTab = 0;
|
int _currentTab = 0;
|
||||||
|
|
||||||
ClaimsOverviewViewData _viewData = ClaimsOverviewViewData.empty();
|
ClaimsOverviewViewData _viewData = ClaimsOverviewViewData.empty();
|
||||||
@ -53,12 +55,34 @@ class _ClaimsOverviewDashboardState extends State<ClaimsOverviewDashboard>
|
|||||||
String? _selectedPolicyId;
|
String? _selectedPolicyId;
|
||||||
List<Map<String, dynamic>> _activePolicies = [];
|
List<Map<String, dynamic>> _activePolicies = [];
|
||||||
Uint8List? _clientLogoBytes;
|
Uint8List? _clientLogoBytes;
|
||||||
DateTime? _dataGeneratedAt;
|
/// Raw `generated_at` from claims API (e.g. `01-07-2026`).
|
||||||
|
String? _apiGeneratedAt;
|
||||||
|
|
||||||
String? get _formattedGeneratedAt {
|
String? get _formattedGeneratedAt {
|
||||||
final generatedAt = _dataGeneratedAt;
|
final raw = _apiGeneratedAt?.trim();
|
||||||
if (generatedAt == null) return null;
|
if (raw == null || raw.isEmpty) return null;
|
||||||
return DateFormat('d MMM yyyy, h:mm a').format(generatedAt.toLocal());
|
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 = [
|
static const _tabs = [
|
||||||
@ -108,9 +132,29 @@ class _ClaimsOverviewDashboardState extends State<ClaimsOverviewDashboard>
|
|||||||
|
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
setState(() => _sessionChecked = true);
|
setState(() => _sessionChecked = true);
|
||||||
|
_logHrActivity('opened_hr_dashboard');
|
||||||
await _loadDashboard();
|
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() {
|
void _onTabChanged() {
|
||||||
if (_tabController.indexIsChanging) return;
|
if (_tabController.indexIsChanging) return;
|
||||||
final index = _tabController.index;
|
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({
|
Future<void> _loadDashboard({
|
||||||
bool reloadPolicies = false,
|
bool reloadPolicies = false,
|
||||||
bool forceRefresh = false,
|
bool forceRefresh = false,
|
||||||
}) async {
|
}) async {
|
||||||
|
final loadId = ++_loadGeneration;
|
||||||
final isInitialLoad = _replayToken == 0;
|
final isInitialLoad = _replayToken == 0;
|
||||||
setState(() {
|
setState(() {
|
||||||
if (isInitialLoad) {
|
if (isInitialLoad) {
|
||||||
@ -151,28 +202,27 @@ class _ClaimsOverviewDashboardState extends State<ClaimsOverviewDashboard>
|
|||||||
_clientBranchId = await _tokenService.readValue('empClientBranchId');
|
_clientBranchId = await _tokenService.readValue('empClientBranchId');
|
||||||
final hrId = await _tokenService.readValue('empHrId');
|
final hrId = await _tokenService.readValue('empHrId');
|
||||||
final token = await _tokenService.getCurrentToken();
|
final token = await _tokenService.getCurrentToken();
|
||||||
|
if (_isStaleLoad(loadId)) return;
|
||||||
if (token != null && token.isNotEmpty) {
|
if (token != null && token.isNotEmpty) {
|
||||||
await _apiService.getTokenLoadAPI(token);
|
await _apiService.getTokenLoadAPI(token);
|
||||||
}
|
}
|
||||||
|
if (_isStaleLoad(loadId)) return;
|
||||||
|
|
||||||
if (isInitialLoad || reloadPolicies || _activePolicies.isEmpty) {
|
if (isInitialLoad || reloadPolicies || _activePolicies.isEmpty) {
|
||||||
await _loadPolicyList(token ?? '', hrId ?? '');
|
await _loadPolicyList(token ?? '', hrId ?? '');
|
||||||
if (isInitialLoad && _activePolicies.isNotEmpty) {
|
// Keep an existing user selection; only default to first when none set.
|
||||||
_selectedPolicyId =
|
if (_isStaleLoad(loadId)) return;
|
||||||
_activePolicies.first['client_policy_id'].toString();
|
|
||||||
}
|
|
||||||
if (!mounted) return;
|
|
||||||
setState(() {});
|
setState(() {});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_selectedPolicyId == null) {
|
if (_selectedPolicyId == null) {
|
||||||
if (!mounted) return;
|
if (_isStaleLoad(loadId)) return;
|
||||||
setState(() {
|
setState(() {
|
||||||
_viewData = ClaimsOverviewViewData.empty(
|
_viewData = ClaimsOverviewViewData.empty(
|
||||||
error: 'No active policy found',
|
error: 'No active policy found',
|
||||||
);
|
);
|
||||||
_enrollmentViewData = EnrollmentOverviewViewData.empty();
|
_enrollmentViewData = EnrollmentOverviewViewData.empty();
|
||||||
_dataGeneratedAt = null;
|
_apiGeneratedAt = null;
|
||||||
_isLoading = false;
|
_isLoading = false;
|
||||||
_isRefreshing = false;
|
_isRefreshing = false;
|
||||||
});
|
});
|
||||||
@ -185,12 +235,13 @@ class _ClaimsOverviewDashboardState extends State<ClaimsOverviewDashboard>
|
|||||||
if (forceRefresh) {
|
if (forceRefresh) {
|
||||||
await ClaimsOverviewCache.clear(branchId, policyId);
|
await ClaimsOverviewCache.clear(branchId, policyId);
|
||||||
}
|
}
|
||||||
|
if (_isStaleLoad(loadId, policyId)) return;
|
||||||
|
|
||||||
if (!forceRefresh) {
|
if (!forceRefresh) {
|
||||||
final cached = await ClaimsOverviewCache.read(branchId, policyId);
|
final cached = await ClaimsOverviewCache.read(branchId, policyId);
|
||||||
if (cached != null) {
|
if (cached != null) {
|
||||||
|
if (_isStaleLoad(loadId, policyId)) return;
|
||||||
_applyCacheEntry(cached);
|
_applyCacheEntry(cached);
|
||||||
if (!mounted) return;
|
|
||||||
setState(() {
|
setState(() {
|
||||||
_isLoading = false;
|
_isLoading = false;
|
||||||
_isRefreshing = false;
|
_isRefreshing = false;
|
||||||
@ -205,13 +256,15 @@ class _ClaimsOverviewDashboardState extends State<ClaimsOverviewDashboard>
|
|||||||
final response = await _apiService.getClaimsCollectionV2All(
|
final response = await _apiService.getClaimsCollectionV2All(
|
||||||
clientPolicyId: policyId,
|
clientPolicyId: policyId,
|
||||||
);
|
);
|
||||||
|
if (_isStaleLoad(loadId, policyId)) return;
|
||||||
final enrollmentResponse =
|
final enrollmentResponse =
|
||||||
await _apiService.getEnrollmentCollectionV1All(
|
await _apiService.getEnrollmentCollectionV1All(
|
||||||
clientPolicyId: policyId,
|
clientPolicyId: policyId,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!mounted) return;
|
if (_isStaleLoad(loadId, policyId)) return;
|
||||||
|
|
||||||
|
final apiGeneratedAt = _parseApiGeneratedAt(response);
|
||||||
final claimsOk = ClaimsKpiParser.isSuccessResponse(response);
|
final claimsOk = ClaimsKpiParser.isSuccessResponse(response);
|
||||||
var claimsData = await ClaimsOverviewViewData.enrichFromApiResponse(
|
var claimsData = await ClaimsOverviewViewData.enrichFromApiResponse(
|
||||||
response,
|
response,
|
||||||
@ -220,6 +273,7 @@ class _ClaimsOverviewDashboardState extends State<ClaimsOverviewDashboard>
|
|||||||
clientPolicyId: policyId,
|
clientPolicyId: policyId,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
if (_isStaleLoad(loadId, policyId)) return;
|
||||||
var enrollmentData = _parseEnrollmentResponse(enrollmentResponse);
|
var enrollmentData = _parseEnrollmentResponse(enrollmentResponse);
|
||||||
|
|
||||||
final selectedPolicy = _selectedPolicy();
|
final selectedPolicy = _selectedPolicy();
|
||||||
@ -236,11 +290,11 @@ class _ClaimsOverviewDashboardState extends State<ClaimsOverviewDashboard>
|
|||||||
loadError: message,
|
loadError: message,
|
||||||
);
|
);
|
||||||
if (claimsData.kpiBySlug.isEmpty) {
|
if (claimsData.kpiBySlug.isEmpty) {
|
||||||
if (!mounted) return;
|
if (_isStaleLoad(loadId, policyId)) return;
|
||||||
setState(() {
|
setState(() {
|
||||||
_viewData = claimsData;
|
_viewData = claimsData;
|
||||||
_enrollmentViewData = enrollmentData;
|
_enrollmentViewData = enrollmentData;
|
||||||
_dataGeneratedAt = DateTime.now();
|
_apiGeneratedAt = apiGeneratedAt;
|
||||||
_isLoading = false;
|
_isLoading = false;
|
||||||
_isRefreshing = false;
|
_isRefreshing = false;
|
||||||
_replayToken++;
|
_replayToken++;
|
||||||
@ -250,35 +304,34 @@ class _ClaimsOverviewDashboardState extends State<ClaimsOverviewDashboard>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final generatedAt = DateTime.now();
|
|
||||||
await ClaimsOverviewCache.write(
|
await ClaimsOverviewCache.write(
|
||||||
branchId,
|
branchId,
|
||||||
ClaimsOverviewCacheEntry(
|
ClaimsOverviewCacheEntry(
|
||||||
policyId: policyId,
|
policyId: policyId,
|
||||||
claimsKpiBySlug: claimsData.kpiBySlug,
|
claimsKpiBySlug: claimsData.kpiBySlug,
|
||||||
enrollmentKpiBySlug: enrollmentData.kpiBySlug,
|
enrollmentKpiBySlug: enrollmentData.kpiBySlug,
|
||||||
generatedAt: generatedAt,
|
generatedAt: apiGeneratedAt,
|
||||||
claimsLoadError: claimsData.loadError,
|
claimsLoadError: claimsData.loadError,
|
||||||
enrollmentLoadError: enrollmentData.loadError,
|
enrollmentLoadError: enrollmentData.loadError,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!mounted) return;
|
if (_isStaleLoad(loadId, policyId)) return;
|
||||||
setState(() {
|
setState(() {
|
||||||
_viewData = claimsData;
|
_viewData = claimsData;
|
||||||
_enrollmentViewData = enrollmentData;
|
_enrollmentViewData = enrollmentData;
|
||||||
_dataGeneratedAt = generatedAt;
|
_apiGeneratedAt = apiGeneratedAt;
|
||||||
_isLoading = false;
|
_isLoading = false;
|
||||||
_isRefreshing = false;
|
_isRefreshing = false;
|
||||||
_replayToken++;
|
_replayToken++;
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (!mounted) return;
|
if (_isStaleLoad(loadId)) return;
|
||||||
setState(() {
|
setState(() {
|
||||||
_viewData = ClaimsOverviewViewData.empty(error: e.toString());
|
_viewData = ClaimsOverviewViewData.empty(error: e.toString());
|
||||||
_enrollmentViewData =
|
_enrollmentViewData =
|
||||||
EnrollmentOverviewViewData.empty(error: e.toString());
|
EnrollmentOverviewViewData.empty(error: e.toString());
|
||||||
_dataGeneratedAt = null;
|
_apiGeneratedAt = null;
|
||||||
_isLoading = false;
|
_isLoading = false;
|
||||||
_isRefreshing = false;
|
_isRefreshing = false;
|
||||||
});
|
});
|
||||||
@ -295,7 +348,7 @@ class _ClaimsOverviewDashboardState extends State<ClaimsOverviewDashboard>
|
|||||||
kpiBySlug: Map<String, dynamic>.from(entry.enrollmentKpiBySlug),
|
kpiBySlug: Map<String, dynamic>.from(entry.enrollmentKpiBySlug),
|
||||||
loadError: entry.enrollmentLoadError,
|
loadError: entry.enrollmentLoadError,
|
||||||
);
|
);
|
||||||
_dataGeneratedAt = entry.generatedAt;
|
_apiGeneratedAt = entry.generatedAt;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _loadPolicyList(String token, String hrId) async {
|
Future<void> _loadPolicyList(String token, String hrId) async {
|
||||||
@ -468,6 +521,8 @@ class _ClaimsOverviewDashboardState extends State<ClaimsOverviewDashboard>
|
|||||||
policyId: _selectedPolicyId ?? '',
|
policyId: _selectedPolicyId ?? '',
|
||||||
dashboardInfo: ClaimsPdfDashboardInfo(
|
dashboardInfo: ClaimsPdfDashboardInfo(
|
||||||
policyLabel: _selectedPolicyLabel(),
|
policyLabel: _selectedPolicyLabel(),
|
||||||
|
clientName: _tokenService.getSelectedBranch()?['client_name']?.toString(),
|
||||||
|
branchName: _tokenService.getSelectedBranch()?['branch_name']?.toString(),
|
||||||
misCreationDate: _formattedGeneratedAt,
|
misCreationDate: _formattedGeneratedAt,
|
||||||
),
|
),
|
||||||
clientLogoBytes: _clientLogoBytes,
|
clientLogoBytes: _clientLogoBytes,
|
||||||
@ -477,6 +532,7 @@ class _ClaimsOverviewDashboardState extends State<ClaimsOverviewDashboard>
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
await _logHrActivity('export_hr_dashboard_data_pdf');
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(
|
SnackBar(
|
||||||
@ -636,7 +692,7 @@ class _ClaimsOverviewDashboardState extends State<ClaimsOverviewDashboard>
|
|||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
'Real-time policy & claims analytics',
|
'Policy & Claims Analytics',
|
||||||
style: GoogleFonts.poppins(
|
style: GoogleFonts.poppins(
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
color: ClaimsOverviewTheme.textSecondary,
|
color: ClaimsOverviewTheme.textSecondary,
|
||||||
|
|||||||
@ -50,15 +50,21 @@ const _minCaptureHeight = 360.0;
|
|||||||
const _captureTimeout = Duration(seconds: 20);
|
const _captureTimeout = Duration(seconds: 20);
|
||||||
const _pdfEmbedMaxWidth = 960;
|
const _pdfEmbedMaxWidth = 960;
|
||||||
const _logoAsset = 'assets/Nhance-Logo-Final 1.png';
|
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);
|
typedef ClaimsPdfExportProgress = void Function(int current, int total, String label);
|
||||||
|
|
||||||
class ClaimsPdfDashboardInfo {
|
class ClaimsPdfDashboardInfo {
|
||||||
final String policyLabel;
|
final String policyLabel;
|
||||||
|
final String? clientName;
|
||||||
|
final String? branchName;
|
||||||
final String? misCreationDate;
|
final String? misCreationDate;
|
||||||
|
|
||||||
const ClaimsPdfDashboardInfo({
|
const ClaimsPdfDashboardInfo({
|
||||||
required this.policyLabel,
|
required this.policyLabel,
|
||||||
|
this.clientName,
|
||||||
|
this.branchName,
|
||||||
this.misCreationDate,
|
this.misCreationDate,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -128,27 +134,30 @@ Future<Uint8List> _buildPdfBytesSync(_PdfBuildInput input) async {
|
|||||||
|
|
||||||
final doc = pw.Document();
|
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 embed = _compressTabImageForPdf(capture.png);
|
||||||
|
final isFirstPage = i == 0;
|
||||||
doc.addPage(
|
doc.addPage(
|
||||||
pw.Page(
|
pw.Page(
|
||||||
pageFormat: PdfPageFormat.a4.landscape,
|
pageFormat: PdfPageFormat.a4.landscape,
|
||||||
margin: const pw.EdgeInsets.all(24),
|
margin: _pdfPageMargin,
|
||||||
build: (ctx) => pw.Column(
|
build: (ctx) => pw.Column(
|
||||||
crossAxisAlignment: pw.CrossAxisAlignment.stretch,
|
crossAxisAlignment: pw.CrossAxisAlignment.stretch,
|
||||||
children: [
|
children: [
|
||||||
|
if (isFirstPage) ...[
|
||||||
_pdfDashboardHeader(
|
_pdfDashboardHeader(
|
||||||
clientLogo: clientLogo,
|
clientLogo: clientLogo,
|
||||||
logo: logo,
|
logo: logo,
|
||||||
info: input.dashboardInfo,
|
info: input.dashboardInfo,
|
||||||
),
|
),
|
||||||
pw.SizedBox(height: 8),
|
pw.SizedBox(height: 8),
|
||||||
|
],
|
||||||
pw.Expanded(
|
pw.Expanded(
|
||||||
child: pw.Center(
|
|
||||||
child: pw.Image(
|
child: pw.Image(
|
||||||
pw.MemoryImage(embed),
|
pw.MemoryImage(embed),
|
||||||
fit: pw.BoxFit.contain,
|
fit: pw.BoxFit.fitWidth,
|
||||||
),
|
alignment: pw.Alignment.topCenter,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@ -183,25 +192,27 @@ Future<Uint8List> _buildPdfBytesWithYields(
|
|||||||
await Future<void>.delayed(Duration.zero);
|
await Future<void>.delayed(Duration.zero);
|
||||||
|
|
||||||
final embed = _compressTabImageForPdf(capture.png);
|
final embed = _compressTabImageForPdf(capture.png);
|
||||||
|
final isFirstPage = i == 0;
|
||||||
doc.addPage(
|
doc.addPage(
|
||||||
pw.Page(
|
pw.Page(
|
||||||
pageFormat: PdfPageFormat.a4.landscape,
|
pageFormat: PdfPageFormat.a4.landscape,
|
||||||
margin: const pw.EdgeInsets.all(24),
|
margin: _pdfPageMargin,
|
||||||
build: (ctx) => pw.Column(
|
build: (ctx) => pw.Column(
|
||||||
crossAxisAlignment: pw.CrossAxisAlignment.stretch,
|
crossAxisAlignment: pw.CrossAxisAlignment.stretch,
|
||||||
children: [
|
children: [
|
||||||
|
if (isFirstPage) ...[
|
||||||
_pdfDashboardHeader(
|
_pdfDashboardHeader(
|
||||||
clientLogo: clientLogo,
|
clientLogo: clientLogo,
|
||||||
logo: logo,
|
logo: logo,
|
||||||
info: input.dashboardInfo,
|
info: input.dashboardInfo,
|
||||||
),
|
),
|
||||||
pw.SizedBox(height: 8),
|
pw.SizedBox(height: 8),
|
||||||
|
],
|
||||||
pw.Expanded(
|
pw.Expanded(
|
||||||
child: pw.Center(
|
|
||||||
child: pw.Image(
|
child: pw.Image(
|
||||||
pw.MemoryImage(embed),
|
pw.MemoryImage(embed),
|
||||||
fit: pw.BoxFit.contain,
|
fit: pw.BoxFit.fitWidth,
|
||||||
),
|
alignment: pw.Alignment.topCenter,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@ -410,25 +421,60 @@ pw.Widget _pdfDashboardHeader({
|
|||||||
}) {
|
}) {
|
||||||
final misDate = info.misCreationDate?.trim();
|
final misDate = info.misCreationDate?.trim();
|
||||||
final hasMisDate = misDate != null && misDate.isNotEmpty;
|
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(
|
return pw.Column(
|
||||||
crossAxisAlignment: pw.CrossAxisAlignment.stretch,
|
crossAxisAlignment: pw.CrossAxisAlignment.stretch,
|
||||||
children: [
|
children: [
|
||||||
pw.Row(
|
pw.Row(
|
||||||
crossAxisAlignment: pw.CrossAxisAlignment.center,
|
crossAxisAlignment: pw.CrossAxisAlignment.center,
|
||||||
mainAxisAlignment: pw.MainAxisAlignment.spaceBetween,
|
|
||||||
children: [
|
children: [
|
||||||
if (clientLogo != null)
|
if (clientLogo != null)
|
||||||
pw.Container(
|
pw.Container(
|
||||||
height: 28,
|
height: _pdfLogoHeight,
|
||||||
constraints: const pw.BoxConstraints(maxWidth: 120),
|
|
||||||
alignment: pw.Alignment.centerLeft,
|
alignment: pw.Alignment.centerLeft,
|
||||||
child: pw.Image(clientLogo, fit: pw.BoxFit.contain),
|
child: pw.Image(clientLogo, height: _pdfLogoHeight, fit: pw.BoxFit.contain),
|
||||||
)
|
)
|
||||||
else
|
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)
|
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),
|
pw.SizedBox(height: 10),
|
||||||
@ -451,7 +497,7 @@ pw.Widget _pdfDashboardHeader({
|
|||||||
crossAxisAlignment: pw.WrapCrossAlignment.center,
|
crossAxisAlignment: pw.WrapCrossAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
pw.Text(
|
pw.Text(
|
||||||
'Real-time policy & claims analytics',
|
'Policy & Claims Analytics',
|
||||||
style: const pw.TextStyle(
|
style: const pw.TextStyle(
|
||||||
fontSize: 9,
|
fontSize: 9,
|
||||||
color: _pdfTextSecondary,
|
color: _pdfTextSecondary,
|
||||||
@ -486,7 +532,7 @@ pw.Widget _pdfDashboardHeader({
|
|||||||
),
|
),
|
||||||
pw.SizedBox(width: 12),
|
pw.SizedBox(width: 12),
|
||||||
pw.Text(
|
pw.Text(
|
||||||
'Branch : ${info.policyLabel}',
|
'Policy No : ${info.policyLabel}',
|
||||||
style: pw.TextStyle(
|
style: pw.TextStyle(
|
||||||
fontSize: 9,
|
fontSize: 9,
|
||||||
fontWeight: pw.FontWeight.bold,
|
fontWeight: pw.FontWeight.bold,
|
||||||
|
|||||||
@ -105,6 +105,7 @@ class _HrPolicyDetailsState extends State<hrPolicyDetails>
|
|||||||
bool _isLoading = false;
|
bool _isLoading = false;
|
||||||
bool _isSendingReminder = false;
|
bool _isSendingReminder = false;
|
||||||
bool _isLoadingPolicyTerms = false;
|
bool _isLoadingPolicyTerms = false;
|
||||||
|
bool _isLoadingHospitalList = false;
|
||||||
// dynamic clintID;
|
// dynamic clintID;
|
||||||
late TabController _tabController;
|
late TabController _tabController;
|
||||||
// List<dynamic> dataPolicy = [];
|
// List<dynamic> dataPolicy = [];
|
||||||
@ -594,6 +595,7 @@ class _HrPolicyDetailsState extends State<hrPolicyDetails>
|
|||||||
row['emp_code']?.toString().toLowerCase().contains(lowerQuery) ==
|
row['emp_code']?.toString().toLowerCase().contains(lowerQuery) ==
|
||||||
true ||
|
true ||
|
||||||
row['uhid']?.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) ==
|
row['relationship']?.toString().toLowerCase().contains(lowerQuery) ==
|
||||||
true ||
|
true ||
|
||||||
row['formatted_dob']
|
row['formatted_dob']
|
||||||
@ -652,12 +654,14 @@ class _HrPolicyDetailsState extends State<hrPolicyDetails>
|
|||||||
|
|
||||||
void exportToCsv(List<Map<String, dynamic>> data) {
|
void exportToCsv(List<Map<String, dynamic>> data) {
|
||||||
List<List<String>> rows = [];
|
List<List<String>> rows = [];
|
||||||
|
final isPost = localTokenType == 'post';
|
||||||
|
final idHeader = isPost ? 'TPA ID' : 'UHID';
|
||||||
|
|
||||||
// Header
|
// Header
|
||||||
rows.add([
|
rows.add([
|
||||||
'Emp Code',
|
'Emp Code',
|
||||||
'Name',
|
'Name',
|
||||||
'UHID',
|
idHeader,
|
||||||
'Relationship',
|
'Relationship',
|
||||||
'Date Of Birth',
|
'Date Of Birth',
|
||||||
'Gender',
|
'Gender',
|
||||||
@ -671,7 +675,7 @@ class _HrPolicyDetailsState extends State<hrPolicyDetails>
|
|||||||
rows.add([
|
rows.add([
|
||||||
item['emp_code'] ?? '',
|
item['emp_code'] ?? '',
|
||||||
item['name'] ?? '',
|
item['name'] ?? '',
|
||||||
item['uhid'] ?? '',
|
isPost ? (item['tpa_id'] ?? '') : (item['uhid'] ?? ''),
|
||||||
item['relationship'] ?? '',
|
item['relationship'] ?? '',
|
||||||
item['formatted_dob'] ?? '',
|
item['formatted_dob'] ?? '',
|
||||||
item['gender'] ?? '',
|
item['gender'] ?? '',
|
||||||
@ -915,6 +919,76 @@ class _HrPolicyDetailsState extends State<hrPolicyDetails>
|
|||||||
return _capitalize(status);
|
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 {
|
Future<void> _openPolicyTermsDialog() async {
|
||||||
setState(() => _isLoadingPolicyTerms = true);
|
setState(() => _isLoadingPolicyTerms = true);
|
||||||
|
|
||||||
@ -1329,7 +1403,7 @@ class _HrPolicyDetailsState extends State<hrPolicyDetails>
|
|||||||
),
|
),
|
||||||
const SizedBox(width: 12),
|
const SizedBox(width: 12),
|
||||||
SizedBox(
|
SizedBox(
|
||||||
width: 142,
|
width: 210,
|
||||||
height: 37,
|
height: 37,
|
||||||
child: ElevatedButton(
|
child: ElevatedButton(
|
||||||
onPressed: _isSendingReminder
|
onPressed: _isSendingReminder
|
||||||
@ -1355,7 +1429,7 @@ class _HrPolicyDetailsState extends State<hrPolicyDetails>
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
: Text(
|
: Text(
|
||||||
'Reminder',
|
'Reminder Mail Template',
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
softWrap: false,
|
softWrap: false,
|
||||||
style: GoogleFonts.poppins(
|
style: GoogleFonts.poppins(
|
||||||
@ -1410,6 +1484,46 @@ class _HrPolicyDetailsState extends State<hrPolicyDetails>
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 12),
|
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(
|
SizedBox(
|
||||||
@ -1561,28 +1675,9 @@ class _HrPolicyDetailsState extends State<hrPolicyDetails>
|
|||||||
if (localTokenType == "post")
|
if (localTokenType == "post")
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
child: Row(
|
child: Align(
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
alignment: Alignment.centerRight,
|
||||||
children: [
|
child: _buildCompactSearchField(),
|
||||||
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(),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
@ -1615,20 +1710,6 @@ class _HrPolicyDetailsState extends State<hrPolicyDetails>
|
|||||||
const SizedBox(height: 48),
|
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,
|
pinned: true,
|
||||||
delegate: _CDHeaderDelegate(
|
delegate: _CDHeaderDelegate(
|
||||||
showUHID: localPolicyTypeId != '6' && localPolicyTypeId != '7',
|
showUHID: localPolicyTypeId != '6' && localPolicyTypeId != '7',
|
||||||
|
uhidHeaderLabel: localTokenType == 'post' ? 'TPA ID' : 'UHID',
|
||||||
showLoggedIn: localTokenType == "pre",
|
showLoggedIn: localTokenType == "pre",
|
||||||
showAction:
|
showAction:
|
||||||
localTokenType != "pre" && (hasAnyEcardLink || hasModule),
|
localTokenType != "pre" && (hasAnyEcardLink || hasModule),
|
||||||
@ -2185,11 +2267,16 @@ class _HrPolicyDetailsState extends State<hrPolicyDetails>
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
/// UHID
|
/// UHID / TPA ID
|
||||||
if (localPolicyTypeId != '6' && localPolicyTypeId != '7')
|
if (localPolicyTypeId != '6' && localPolicyTypeId != '7')
|
||||||
Expanded(
|
Expanded(
|
||||||
flex: 2,
|
flex: 2,
|
||||||
child: Text(item['uhid'] ?? '-', style: _dataBold),
|
child: Text(
|
||||||
|
localTokenType == 'post'
|
||||||
|
? (item['tpa_id'] ?? '-')
|
||||||
|
: (item['uhid'] ?? '-'),
|
||||||
|
style: _dataBold,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
Expanded(
|
Expanded(
|
||||||
@ -3176,11 +3263,13 @@ class _HrPolicyDetailsState extends State<hrPolicyDetails>
|
|||||||
|
|
||||||
class _CDHeaderDelegate extends SliverPersistentHeaderDelegate {
|
class _CDHeaderDelegate extends SliverPersistentHeaderDelegate {
|
||||||
final bool showUHID;
|
final bool showUHID;
|
||||||
|
final String uhidHeaderLabel;
|
||||||
final bool showLoggedIn;
|
final bool showLoggedIn;
|
||||||
final bool showAction;
|
final bool showAction;
|
||||||
|
|
||||||
_CDHeaderDelegate({
|
_CDHeaderDelegate({
|
||||||
required this.showUHID,
|
required this.showUHID,
|
||||||
|
this.uhidHeaderLabel = 'UHID',
|
||||||
required this.showLoggedIn,
|
required this.showLoggedIn,
|
||||||
required this.showAction,
|
required this.showAction,
|
||||||
});
|
});
|
||||||
@ -3201,7 +3290,7 @@ class _CDHeaderDelegate extends SliverPersistentHeaderDelegate {
|
|||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
_headerCell('Name', 3),
|
_headerCell('Name', 3),
|
||||||
if (showUHID) _headerCell('UHID', 2),
|
if (showUHID) _headerCell(uhidHeaderLabel, 2),
|
||||||
_headerCell('Relationship', 2),
|
_headerCell('Relationship', 2),
|
||||||
_headerCell('Date Of Birth', 2),
|
_headerCell('Date Of Birth', 2),
|
||||||
_headerCell('Gender', 2),
|
_headerCell('Gender', 2),
|
||||||
@ -3234,6 +3323,101 @@ class _CDHeaderDelegate extends SliverPersistentHeaderDelegate {
|
|||||||
true;
|
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 {
|
class _PolicyTermsDialog extends StatelessWidget {
|
||||||
final Map<String, String> terms;
|
final Map<String, String> terms;
|
||||||
|
|
||||||
|
|||||||
@ -544,14 +544,6 @@ class _policiesState extends State<policies>
|
|||||||
),
|
),
|
||||||
|
|
||||||
const SizedBox(height: 8),
|
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;
|
final Function(String clientPolicyId)? onBulkDownload;
|
||||||
|
|
||||||
static const double _enrollmentCardHeight = 156;
|
static const double _enrollmentCardHeight = 156;
|
||||||
static const double _activePolicyCardHeight = 170;
|
static const double _activePolicyCardHeight = 148;
|
||||||
|
|
||||||
const _PolicyGrid({
|
const _PolicyGrid({
|
||||||
super.key,
|
super.key,
|
||||||
@ -647,7 +639,7 @@ class _PolicyGrid extends StatelessWidget {
|
|||||||
/// ✅ EB section
|
/// ✅ EB section
|
||||||
if (ebPolicies.isNotEmpty) ...[
|
if (ebPolicies.isNotEmpty) ...[
|
||||||
Text(
|
Text(
|
||||||
'EB Policies',
|
'Employee Benefits',
|
||||||
style: GoogleFonts.poppins(
|
style: GoogleFonts.poppins(
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
@ -666,7 +658,7 @@ class _PolicyGrid extends StatelessWidget {
|
|||||||
/// ✅ Non-EB section
|
/// ✅ Non-EB section
|
||||||
if (nonEbPolicies.isNotEmpty) ...[
|
if (nonEbPolicies.isNotEmpty) ...[
|
||||||
Text(
|
Text(
|
||||||
'Non-EB Policies',
|
'Non-Employee Benefits',
|
||||||
style: GoogleFonts.poppins(
|
style: GoogleFonts.poppins(
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
@ -1129,30 +1121,26 @@ class _ActivePolicyCardNew extends StatelessWidget {
|
|||||||
: const Color(0xFFE9F6FB), // EB
|
: const Color(0xFFE9F6FB), // EB
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
),
|
),
|
||||||
padding: const EdgeInsets.all(14),
|
padding: const EdgeInsets.all(12),
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
/// PREMIUM
|
/// POLICY NO + optional ecard bulk download
|
||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Text(
|
child: Text(
|
||||||
'Premium - ₹${data['total_premium'] ?? ''}*',
|
'${data['type']} - ${data['policy_no']}',
|
||||||
style: GoogleFonts.poppins(
|
style: GoogleFonts.poppins(
|
||||||
fontSize: 12,
|
fontSize: 14,
|
||||||
color: Color(0xFF009195),
|
fontWeight: FontWeight.w500,
|
||||||
fontWeight: FontWeight.w600,
|
),
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
|
||||||
// 🔥 ICON FLOATING ABOVE CARD
|
|
||||||
if (data['is_ecard_bulk_download'] == 1)
|
if (data['is_ecard_bulk_download'] == 1)
|
||||||
Positioned(
|
GestureDetector(
|
||||||
top: 10,
|
|
||||||
right: 10,
|
|
||||||
child: GestureDetector(
|
|
||||||
onTap: () {
|
onTap: () {
|
||||||
logDebug(
|
logDebug(
|
||||||
'ICON CLICKED ${data['client_policy_id']}');
|
'ICON CLICKED ${data['client_policy_id']}');
|
||||||
@ -1173,30 +1161,10 @@ class _ActivePolicyCardNew extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
||||||
const SizedBox(height: 4),
|
const SizedBox(height: 2),
|
||||||
|
|
||||||
/// POLICY NO + ICON
|
|
||||||
Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
children: [
|
|
||||||
Expanded(
|
|
||||||
child: Text(
|
|
||||||
'${data['type']} - ${data['policy_no']}',
|
|
||||||
style: GoogleFonts.poppins(
|
|
||||||
fontSize: 14,
|
|
||||||
fontWeight: FontWeight.w500,
|
|
||||||
),
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
|
|
||||||
const SizedBox(height: 4),
|
|
||||||
|
|
||||||
/// Insurer
|
/// Insurer
|
||||||
Tooltip(
|
Tooltip(
|
||||||
@ -1232,7 +1200,7 @@ class _ActivePolicyCardNew extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
||||||
const SizedBox(height: 4),
|
const SizedBox(height: 2),
|
||||||
|
|
||||||
/// DATE RANGE
|
/// DATE RANGE
|
||||||
Text(
|
Text(
|
||||||
@ -1243,7 +1211,7 @@ class _ActivePolicyCardNew extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
const SizedBox(height: 10),
|
const SizedBox(height: 8),
|
||||||
|
|
||||||
/// ACTIVE / INACTIVE
|
/// ACTIVE / INACTIVE
|
||||||
Row(
|
Row(
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user