enrollment-app/lib/customAppBar/side_bar.dart

808 lines
23 KiB
Dart

import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart';
import 'package:google_fonts/google_fonts.dart';
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';
class NhanceSideBar extends StatefulWidget {
const NhanceSideBar({super.key});
@override
State<NhanceSideBar> createState() => _NhanceSideBarState();
}
class _NhanceSideBarState extends State<NhanceSideBar> {
String? activeRoute;
late ApiService apiService;
OverlayEntry? _claimsOverlayEntry;
bool _isHoveringClaimsItem = false;
bool _isHoveringClaimsMenu = false;
DateTime? _claimsMenuSuppressUntil;
// bool isLoading = true; // Add a loading state
// bool hideInactiveStatus = true;
final tokenService = TokenStorageService();
// dynamic enrollmentModules = [];
// dynamic postModules = [];
List<Map<String, dynamic>> sideMenuItems = [];
List<int> postModules = [];
List<int> enrollmentModules = [];
List<String> claimsSubMenu = [];
@override
void initState() {
super.initState();
apiService = ApiService(context);
_buildSideMenu();
// _checkTokens();
}
Future<void> _buildSideMenu() async {
final enrollmentRaw =
await tokenService.readValue('enrollmentAllowed_modules'); // "[1]"
final postRaw =
await tokenService.readValue('empAllowed_modules'); // "[2,3,4]"
logDebug('enrollmentRaw $enrollmentRaw');
logDebug('postRaw $postRaw');
// ✅ Decode safely
enrollmentModules = enrollmentRaw != null && enrollmentRaw.isNotEmpty
? List<int>.from(jsonDecode(enrollmentRaw))
: [];
postModules = postRaw != null && postRaw.isNotEmpty
? List<int>.from(jsonDecode(postRaw))
: [];
final claimsSubMenuRaw = await tokenService.readValue('claims_sub_menu');
if (claimsSubMenuRaw != null && claimsSubMenuRaw.isNotEmpty) {
claimsSubMenu = List<dynamic>.from(jsonDecode(claimsSubMenuRaw))
.map((e) => e.toString())
.toList();
} else {
// Fallback: decoded JWT / selected branch may still have the list.
final decoded = tokenService.getDecodedToken();
final branch = tokenService.getSelectedBranch();
final fromDecoded = decoded?['claims_sub_menu'];
final fromBranch = branch?['claims_sub_menu'];
final source = fromBranch is List
? fromBranch
: (fromDecoded is List ? fromDecoded : const []);
claimsSubMenu = source.map((e) => e.toString()).toList();
}
logDebug('enrollmentModules $enrollmentModules');
logDebug('postModules $postModules');
logDebug('claimsSubMenu $claimsSubMenu');
final List<Map<String, dynamic>> items = [];
// ✅ POLICIES (1 OR 2)
if (enrollmentModules.contains(1) || postModules.contains(2)) {
items.add({
'route': 'policies',
'label': 'Policies',
'icon': 'policies',
});
items.add({
'route': 'pendingDependentApproval',
'label': 'Approvals',
'icon': 'pending_approve',
'useMaterialIcon': true,
});
}
// CD
if (postModules.contains(3)) {
items.add({
'route': 'CdPoliciesList',
'label': 'CD',
'icon': 'cd',
});
}
// CLAIMS — only when module 4 and at least one sub-menu is allowed
if (postModules.contains(4) && _claimsSubMenuRoutes.isNotEmpty) {
items.add({
'route': 'ClaimsPolicies',
'label': 'Claims',
'icon': 'claims',
});
}
setState(() {
sideMenuItems = items;
});
}
/// Allowed Claims destinations based on claims_sub_menu.
List<({String label, String route})> get _claimsSubMenuRoutes {
final items = <({String label, String route})>[];
if (claimsSubMenu.contains('EB')) {
items.add((label: 'Employee Benefits', route: 'ClaimsPolicies'));
}
if (claimsSubMenu.contains('Non-EB')) {
items.add((label: 'Non-Employee Benefits', route: 'nonEBClaimsList'));
}
return items;
}
bool get _hasMultipleClaimsSubMenus => _claimsSubMenuRoutes.length > 1;
String? get _singleClaimsRoute {
final routes = _claimsSubMenuRoutes;
if (routes.length == 1) return routes.first.route;
return null;
}
Future<void> logout(BuildContext context) async {
// final prefs = await SharedPreferences.getInstance();
// final String? hrtoken = prefs.getString('_postToken');
// final String? token = prefs.getString('enrollToken');
// prefs.clear();
logDebug('LocalStorage Cleared');
apiService.logout();
// Navigator.pushNamed(context, 'hrLogin');
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
// Safely capture the current route name
final newRoute = ModalRoute.of(context)?.settings.name;
if (activeRoute != newRoute) {
setState(() {
activeRoute = newRoute;
});
_closeClaimsMenu();
// Re-verify the menu items if the route changes
_buildSideMenu();
}
}
void _navigate(String routeName) {
_closeClaimsMenu();
if (activeRoute == routeName) return;
// Navigation should happen first, didChangeDependencies will handle the state
Navigator.pushReplacementNamed(context, routeName);
}
void _showClaimsSubMenu(GlobalKey key) {
if (!_hasMultipleClaimsSubMenus) return;
if (_claimsMenuSuppressUntil != null &&
DateTime.now().isBefore(_claimsMenuSuppressUntil!)) {
return;
}
final targetContext = key.currentContext;
if (targetContext == null) return;
final box = targetContext.findRenderObject() as RenderBox?;
if (box == null) return;
final offset = box.localToGlobal(Offset.zero);
final subMenus = _claimsSubMenuRoutes;
_claimsOverlayEntry?.remove();
_claimsOverlayEntry = OverlayEntry(
builder: (context) => Positioned(
left: offset.dx + box.size.width + 6,
top: offset.dy,
child: MouseRegion(
onEnter: (_) {
_isHoveringClaimsMenu = true;
},
onExit: (_) {
_isHoveringClaimsMenu = false;
_scheduleCloseClaimsMenu();
},
child: Material(
color: Colors.transparent,
child: Container(
width: 200,
padding: const EdgeInsets.symmetric(vertical: 6),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(8),
boxShadow: const [
BoxShadow(
color: Colors.black26,
blurRadius: 8,
offset: Offset(0, 2),
),
],
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
for (final item in subMenus)
_buildClaimsMenuItem(item.label, item.route),
],
),
),
),
),
),
);
Overlay.of(context).insert(_claimsOverlayEntry!);
}
Widget _buildClaimsMenuItem(String label, String route) {
return _ClaimsSubMenuItem(
label: label,
onTap: () {
_claimsMenuSuppressUntil =
DateTime.now().add(const Duration(milliseconds: 700));
_isHoveringClaimsItem = false;
_isHoveringClaimsMenu = false;
_closeClaimsMenu();
_navigate(route);
},
);
}
void _scheduleCloseClaimsMenu() {
Future.delayed(const Duration(milliseconds: 120), () {
if (!_isHoveringClaimsItem && !_isHoveringClaimsMenu) {
_closeClaimsMenu();
}
});
}
void _closeClaimsMenu() {
_claimsOverlayEntry?.remove();
_claimsOverlayEntry = null;
_isHoveringClaimsMenu = false;
}
Future<void> _openHelpDialog() async {
final isPostUser = postModules.isNotEmpty;
await showDialog<void>(
context: context,
builder: (dialogContext) => _HelpContactDialog(
isPostUser: isPostUser,
apiService: apiService,
tokenService: tokenService,
),
);
}
@override
Widget build(BuildContext context) {
return Container(
width: 70,
color: const Color(0xFF009E9E),
child: Column(
children: [
// const SizedBox(height: 12),
//
// _SideItem(
// // icon: Icons.policy,
// icon: SvgPicture.string(
// SvgService.getSvg('policies'),
// width: 35,
// height: 35,
// colorFilter: const ColorFilter.mode(
// Colors.white,
// BlendMode.srcIn,
// ),
// ),
// label: "Policies",
// isActive: activeRoute == 'oldPolicy', // or correct policy route
// onTap: () => _navigate('oldPolicy'),
// ),
//
// _SideItem(
// // icon: Icons.credit_card,
// icon: SvgPicture.string(
// SvgService.getSvg('cd'),
// width: 35,
// height: 35,
// colorFilter: const ColorFilter.mode(
// Colors.white,
// BlendMode.srcIn,
// ),
// ),
// label: "CD",
// isActive: activeRoute == 'cdTransactionDetails',
// onTap: () => _navigate('cdTransactionDetails'),
// ),
//
// _SideItem(
// // icon: Icons.assignment,
// icon: SvgPicture.string(
// SvgService.getSvg('claims'),
// width: 35,
// height: 35,
// colorFilter: const ColorFilter.mode(
// Colors.white,
// BlendMode.srcIn,
// ),
// ),
// label: "Claims",
// isActive: activeRoute == 'claims', // claims tab inside dashboard
// onTap: () => _navigate('claims'),
// ),
//
// const Spacer(),
//
// _SideItem(
// // icon: Icons.logout,
// icon: SvgPicture.string(
// SvgService.getSvg('logout'),
// width: 35,
// height: 35,
// colorFilter: const ColorFilter.mode(
// Colors.white,
// BlendMode.srcIn,
// ),
// ),
// label: "Logout",
// onTap: () {
// logout(context);
// },
// ),
if (postModules.isNotEmpty && postModules.contains(5))
_SideItem(
icon: const Icon(
Icons.space_dashboard_outlined,
color: Colors.white,
size: 30,
),
label: "Dashboard",
isActive: activeRoute == 'claimsOverviewDashboard',
onTap: () => _navigate('claimsOverviewDashboard'),
),
...sideMenuItems.map((item) {
final isClaims = item['route'] == 'ClaimsPolicies';
final isClaimsActive =
activeRoute == 'ClaimsPolicies' || activeRoute == 'nonEBClaimsList';
final itemKey = GlobalKey();
final showClaimsSubMenu = isClaims && _hasMultipleClaimsSubMenus;
final claimsDirectRoute =
isClaims ? (_singleClaimsRoute ?? 'ClaimsPolicies') : null;
final useMaterialIcon = item['useMaterialIcon'] == true;
return _SideItem(
key: itemKey,
icon: useMaterialIcon
? const Icon(
Icons.how_to_reg_outlined,
color: Colors.white,
size: 30,
)
: SvgPicture.string(
SvgService.getSvg(item['icon']),
width: 35,
height: 35,
colorFilter: const ColorFilter.mode(
Colors.white,
BlendMode.srcIn,
),
),
label: item['label'],
isActive: isClaims ? isClaimsActive : activeRoute == item['route'],
onTap: showClaimsSubMenu
? null
: () => _navigate(
claimsDirectRoute ?? item['route'] as String,
),
onHoverEnter: showClaimsSubMenu
? () {
_isHoveringClaimsItem = true;
_showClaimsSubMenu(itemKey);
}
: null,
onHoverExit: showClaimsSubMenu
? () {
_isHoveringClaimsItem = false;
_scheduleCloseClaimsMenu();
}
: null,
);
}),
// if (postModules.isNotEmpty && postModules.contains(5))
// _SideItem(
// // icon: Icons.dashboard,
// icon: SvgPicture.string(
// SvgService.getSvg('dashboard'),
// width: 35,
// height: 35,
// colorFilter: const ColorFilter.mode(
// Colors.white,
// BlendMode.srcIn,
// ),
// ),
// label: "Insights",
// isActive: activeRoute == 'hrDashboard',
// onTap: () => _navigate('hrDashboard'),
// ),
const Spacer(),
if (enrollmentModules.isNotEmpty || postModules.isNotEmpty)
_SideItem(
icon: const Icon(
Icons.help_outline,
color: Colors.white,
size: 30,
),
label: 'Help',
onTap: _openHelpDialog,
),
_SideItem(
icon: SvgPicture.string(
SvgService.getSvg('logout'),
width: 35,
height: 35,
colorFilter: const ColorFilter.mode(
Colors.white,
BlendMode.srcIn,
),
),
label: "Logout",
onTap: () => logout(context),
),
// const SizedBox(height: 10),
],
),
);
}
}
class _HelpContactDialog extends StatefulWidget {
final bool isPostUser;
final ApiService apiService;
final TokenStorageService tokenService;
const _HelpContactDialog({
required this.isPostUser,
required this.apiService,
required this.tokenService,
});
@override
State<_HelpContactDialog> createState() => _HelpContactDialogState();
}
class _HelpContactDialogState extends State<_HelpContactDialog> {
bool _isLoading = false;
List<Map<String, dynamic>> _level1Contacts = [];
List<Map<String, dynamic>> _level2Contacts = [];
String? _errorMessage;
@override
void initState() {
super.initState();
if (widget.isPostUser) {
_loadClientRM();
}
}
Future<void> _loadClientRM() async {
setState(() {
_isLoading = true;
_errorMessage = null;
});
try {
final clientId = await widget.tokenService.readValue('empClientId');
final token = widget.tokenService.getCurrentToken();
if (clientId == null ||
clientId.toString().isEmpty ||
token == null ||
token.isEmpty) {
setState(() {
_errorMessage = 'Client details not found';
_isLoading = false;
});
return;
}
final response = await widget.apiService.getClientRMApi(
clientId.toString(),
token,
);
if (!mounted) return;
if (response['status'] == 'success' && response['data'] is Map) {
final data = Map<String, dynamic>.from(response['data'] as Map);
setState(() {
_level1Contacts = _parseContacts(data['level_1']);
_level2Contacts = _parseContacts(data['level_2']);
_isLoading = false;
});
} else {
setState(() {
_errorMessage =
response['message']?.toString() ?? 'Failed to load contacts';
_isLoading = false;
});
}
} catch (e) {
logDebug('getClientRM failed: $e');
if (mounted) {
setState(() {
_errorMessage = 'Failed to load contacts';
_isLoading = false;
});
}
}
}
List<Map<String, dynamic>> _parseContacts(dynamic raw) {
if (raw is! List) return [];
return raw
.whereType<Map>()
.map((item) => Map<String, dynamic>.from(item))
.toList();
}
Widget _buildContactCard(Map<String, dynamic> contact) {
final name = contact['first_name']?.toString() ?? '-';
final email = contact['email']?.toString() ?? '-';
final mobile = contact['mobile']?.toString() ?? '-';
return Container(
width: double.infinity,
margin: const EdgeInsets.only(bottom: 10),
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: const Color(0xFFF5F5F5),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: const Color(0xFFE8E8E8)),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
name,
style: GoogleFonts.poppins(
fontSize: 14,
fontWeight: FontWeight.w600,
color: const Color(0xFF009195),
),
),
const SizedBox(height: 6),
Text(
email,
style: GoogleFonts.poppins(fontSize: 13, color: Colors.black87),
),
const SizedBox(height: 2),
Text(
mobile,
style: GoogleFonts.poppins(fontSize: 13, color: Colors.black87),
),
],
),
);
}
Widget _buildLevelSection({
required String title,
required List<Map<String, dynamic>> contacts,
}) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: GoogleFonts.poppins(
fontSize: 14,
fontWeight: FontWeight.w600,
color: Colors.black87,
),
),
const SizedBox(height: 8),
if (contacts.isEmpty)
Text(
'No contact assigned',
style: GoogleFonts.poppins(fontSize: 13, color: Colors.black54),
)
else
...contacts.map(_buildContactCard),
const SizedBox(height: 12),
],
);
}
Widget _buildContent() {
if (!widget.isPostUser) {
return Center(
child: Text(
'No Level Contact',
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),
),
);
}
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildLevelSection(
title: 'Level 1 - Account Manager',
contacts: _level1Contacts,
),
_buildLevelSection(
title: 'Level 2 - Head',
contacts: _level2Contacts,
),
],
);
}
@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(
'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)),
),
],
);
}
}
class _SideItem extends StatelessWidget {
// final IconData icon;
final Widget icon; // 👈 changed
final String label;
final bool isActive;
final VoidCallback? onTap;
final VoidCallback? onHoverEnter;
final VoidCallback? onHoverExit;
const _SideItem({
super.key,
required this.icon,
required this.label,
this.isActive = false,
this.onTap,
this.onHoverEnter,
this.onHoverExit,
});
@override
Widget build(BuildContext context) {
return MouseRegion(
onEnter: (_) => onHoverEnter?.call(),
onExit: (_) => onHoverExit?.call(),
child: InkWell(
onTap: onTap,
child: Container(
width: double.infinity,
margin: const EdgeInsets.symmetric(vertical: 6),
padding: const EdgeInsets.symmetric(vertical: 10),
decoration: BoxDecoration(
color: isActive
? const Color(0xFF065D61) // ✅ ACTIVE like your screenshot
: Colors.transparent,
),
child: Column(
children: [
// 👇 SVG or Icon widget
icon,
// Icon(icon, color: Colors.white, size: 22),
const SizedBox(height: 6),
Text(
label,
textAlign: TextAlign.center,
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
color: Colors.white,
fontSize: 10,
fontWeight: FontWeight.w500,
height: 1.15,
),
),
],
),
),
),
);
}
}
class _ClaimsSubMenuItem extends StatefulWidget {
final String label;
final VoidCallback onTap;
const _ClaimsSubMenuItem({
required this.label,
required this.onTap,
});
@override
State<_ClaimsSubMenuItem> createState() => _ClaimsSubMenuItemState();
}
class _ClaimsSubMenuItemState extends State<_ClaimsSubMenuItem> {
bool isHovered = false;
@override
Widget build(BuildContext context) {
return MouseRegion(
onEnter: (_) => setState(() => isHovered = true),
onExit: (_) => setState(() => isHovered = false),
child: GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: widget.onTap,
child: Container(
width: double.infinity,
color: isHovered ? const Color(0xFFE5F6F6) : Colors.white,
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
child: Align(
alignment: Alignment.centerLeft,
child: Text(
widget.label,
style: TextStyle(
fontSize: 12,
color: isHovered ? const Color(0xFF006F73) : Colors.black87,
fontWeight: isHovered ? FontWeight.w600 : FontWeight.w400,
),
),
),
),
),
);
}
}