808 lines
23 KiB
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,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|