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 createState() => _NhanceSideBarState(); } class _NhanceSideBarState extends State { 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> sideMenuItems = []; List postModules = []; List enrollmentModules = []; List claimsSubMenu = []; @override void initState() { super.initState(); apiService = ApiService(context); _buildSideMenu(); // _checkTokens(); } Future _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.from(jsonDecode(enrollmentRaw)) : []; postModules = postRaw != null && postRaw.isNotEmpty ? List.from(jsonDecode(postRaw)) : []; final claimsSubMenuRaw = await tokenService.readValue('claims_sub_menu'); if (claimsSubMenuRaw != null && claimsSubMenuRaw.isNotEmpty) { claimsSubMenu = List.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> 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 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 _openHelpDialog() async { final isPostUser = postModules.isNotEmpty; await showDialog( 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> _level1Contacts = []; List> _level2Contacts = []; String? _errorMessage; @override void initState() { super.initState(); if (widget.isPostUser) { _loadClientRM(); } } Future _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.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> _parseContacts(dynamic raw) { if (raw is! List) return []; return raw .whereType() .map((item) => Map.from(item)) .toList(); } Widget _buildContactCard(Map 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> 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, ), ), ), ), ), ); } }