import 'package:marco/helpers/theme/theme_customizer.dart'; import 'package:marco/helpers/services/url_service.dart'; import 'package:marco/helpers/utils/mixins/ui_mixin.dart'; import 'package:marco/helpers/utils/my_shadow.dart'; import 'package:marco/helpers/widgets/my_card.dart'; import 'package:marco/helpers/widgets/my_container.dart'; import 'package:marco/helpers/widgets/my_spacing.dart'; import 'package:marco/helpers/widgets/my_text.dart'; import 'package:marco/images.dart'; import 'package:marco/widgets/custom_pop_menu.dart'; import 'package:flutter/material.dart'; import 'package:get/route_manager.dart'; import 'package:flutter_lucide/flutter_lucide.dart'; import 'package:marco/helpers/services/storage/local_storage.dart'; import 'package:marco/model/employees/employee_info.dart'; import 'package:marco/helpers/widgets/avatar.dart'; typedef LeftbarMenuFunction = void Function(String key); class LeftbarObserver { static Map observers = {}; static attachListener(String key, LeftbarMenuFunction fn) { observers[key] = fn; } static detachListener(String key) { observers.remove(key); } static notifyAll(String key) { for (var fn in observers.values) { fn(key); } } } class LeftBar extends StatefulWidget { final bool isCondensed; const LeftBar({super.key, this.isCondensed = false}); @override _LeftBarState createState() => _LeftBarState(); } class _LeftBarState extends State with SingleTickerProviderStateMixin, UIMixin { final ThemeCustomizer customizer = ThemeCustomizer.instance; bool isCondensed = false; String path = UrlService.getCurrentUrl(); EmployeeInfo? employeeInfo; @override void initState() { super.initState(); _loadEmployeeInfo(); } void _loadEmployeeInfo() { setState(() { employeeInfo = LocalStorage.getEmployeeInfo(); }); } @override Widget build(BuildContext context) { isCondensed = widget.isCondensed; return MyCard( paddingAll: 0, shadow: MyShadow(position: MyShadowPosition.centerRight, elevation: 0.2), child: AnimatedContainer( color: leftBarTheme.background, width: isCondensed ? 70 : 250, curve: Curves.easeInOut, duration: Duration(milliseconds: 200), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Center( child: Padding( padding: EdgeInsets.only(top: 50), child: InkWell( onTap: () => Get.toNamed('/dashboard'), child: Image.asset( (ThemeCustomizer.instance.theme == ThemeMode.light ? (widget.isCondensed ? Images.logoLightSmall : Images.logoLight) : (widget.isCondensed ? Images.logoDarkSmall : Images.logoDark)), height: 60, ), ), ), ), Expanded( child: ScrollConfiguration( behavior: ScrollConfiguration.of(context).copyWith(scrollbars: false), child: ListView( shrinkWrap: true, controller: ScrollController(), physics: BouncingScrollPhysics(), clipBehavior: Clip.antiAliasWithSaveLayer, children: [ labelWidget("Dashboard"), NavigationItem( iconData: LucideIcons.layout_dashboard, title: "Dashboard", isCondensed: isCondensed, route: '/dashboard'), NavigationItem( iconData: LucideIcons.scan_face, title: "Attendance", isCondensed: isCondensed, route: '/dashboard/attendance'), NavigationItem( iconData: LucideIcons.users, title: "Employees", isCondensed: isCondensed, route: '/dashboard/employees'), NavigationItem( iconData: LucideIcons.logs, title: "Daily Task Planning", isCondensed: isCondensed, route: '/dashboard/daily-task-Planning'), NavigationItem( iconData: LucideIcons.list_todo, title: "Daily Progress Report", isCondensed: isCondensed, route: '/dashboard/daily-task-progress'), ], ), ), ), Divider(), if (!isCondensed) userInfoSection(), ], ), ), ); } Widget userInfoSection() { if (employeeInfo == null) { return Center(child: CircularProgressIndicator()); } return Padding( padding: MySpacing.fromLTRB(16, 8, 16, 8), child: Row( children: [ Avatar( firstName: employeeInfo?.firstName ?? 'First', lastName: employeeInfo?.lastName ?? 'Name', ), MySpacing.width(16), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.center, children: [ MyText.labelSmall( "${employeeInfo?.firstName ?? 'First Name'} ${employeeInfo?.lastName ?? 'Last Name'}", fontWeight: 600, muted: true, ), ], ), ), MyContainer( onTap: () async { await LocalStorage.logout(); }, color: leftBarTheme.activeItemBackground, paddingAll: 8, child: Icon( LucideIcons.log_out, size: 16, color: leftBarTheme.activeItemColor, ), ) ], ), ); } Widget labelWidget(String label) { return isCondensed ? MySpacing.empty() : Container( padding: MySpacing.xy(24, 8), child: MyText.labelSmall( label.toUpperCase(), color: leftBarTheme.labelColor, muted: true, maxLines: 1, overflow: TextOverflow.clip, fontWeight: 700, ), ); } } class MenuWidget extends StatefulWidget { final IconData iconData; final String title; final bool isCondensed; final bool active; final List children; const MenuWidget( {super.key, required this.iconData, required this.title, this.isCondensed = false, this.active = false, this.children = const []}); @override _MenuWidgetState createState() => _MenuWidgetState(); } class _MenuWidgetState extends State with UIMixin, SingleTickerProviderStateMixin { bool isHover = false; bool isActive = false; late Animation _iconTurns; late AnimationController _controller; bool popupShowing = true; Function? hideFn; @override void initState() { super.initState(); _controller = AnimationController(duration: Duration(milliseconds: 200), vsync: this); _iconTurns = _controller.drive(Tween(begin: 0.0, end: 0.5) .chain(CurveTween(curve: Curves.easeIn))); LeftbarObserver.attachListener(widget.title, onChangeMenuActive); } void onChangeMenuActive(String key) { if (key != widget.title) {} } void onChangeExpansion(value) { isActive = value; if (isActive) { _controller.forward(); } else { _controller.reverse(); } if (mounted) { setState(() {}); } } @override void didChangeDependencies() { super.didChangeDependencies(); var route = UrlService.getCurrentUrl(); isActive = widget.children.any((element) => element.route == route); onChangeExpansion(isActive); if (hideFn != null) { hideFn!(); } } @override Widget build(BuildContext context) { if (widget.isCondensed) { return CustomPopupMenu( backdrop: true, show: popupShowing, hideFn: (hide) => hideFn = hide, onChange: (_) {}, placement: CustomPopupMenuPlacement.right, menu: MouseRegion( cursor: SystemMouseCursors.click, onHover: (event) { setState(() { isHover = true; }); }, onExit: (event) { setState(() { isHover = false; }); }, child: MyContainer.transparent( margin: MySpacing.fromLTRB(16, 0, 16, 8), color: isActive || isHover ? leftBarTheme.activeItemBackground : Colors.transparent, padding: MySpacing.xy(8, 8), child: Center( child: Icon( widget.iconData, color: (isHover || isActive) ? leftBarTheme.activeItemColor : leftBarTheme.onBackground, size: 20, ), ), ), ), menuBuilder: (_) => MyContainer.bordered( paddingAll: 8, width: 190, child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, mainAxisSize: MainAxisSize.min, children: widget.children, ), ), ); } else { return MouseRegion( cursor: SystemMouseCursors.click, onHover: (event) { setState(() { isHover = true; }); }, onExit: (event) { setState(() { isHover = false; }); }, child: MyContainer.transparent( margin: MySpacing.fromLTRB(24, 0, 16, 0), paddingAll: 0, child: ListTileTheme( contentPadding: EdgeInsets.all(0), dense: true, horizontalTitleGap: 0.0, minLeadingWidth: 0, child: ExpansionTile( tilePadding: MySpacing.zero, initiallyExpanded: isActive, maintainState: true, onExpansionChanged: (context) { LeftbarObserver.notifyAll(widget.title); onChangeExpansion(context); }, trailing: RotationTransition( turns: _iconTurns, child: Icon( LucideIcons.chevron_down, size: 18, color: leftBarTheme.onBackground, ), ), iconColor: leftBarTheme.activeItemColor, childrenPadding: MySpacing.x(12), title: Row( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.center, children: [ Icon( widget.iconData, size: 20, color: isHover || isActive ? leftBarTheme.activeItemColor : leftBarTheme.onBackground, ), MySpacing.width(18), Expanded( child: MyText.labelLarge( widget.title, maxLines: 1, overflow: TextOverflow.ellipsis, textAlign: TextAlign.start, color: isHover || isActive ? leftBarTheme.activeItemColor : leftBarTheme.onBackground, ), ), ], ), collapsedBackgroundColor: Colors.transparent, shape: RoundedRectangleBorder( side: BorderSide(color: Colors.transparent), ), backgroundColor: Colors.transparent, children: widget.children), ), ), ); } } @override void dispose() { _controller.dispose(); super.dispose(); } } class MenuItem extends StatefulWidget { final IconData? iconData; final String title; final bool isCondensed; final String? route; const MenuItem({ super.key, this.iconData, required this.title, this.isCondensed = false, this.route, }); @override _MenuItemState createState() => _MenuItemState(); } class _MenuItemState extends State with UIMixin { bool isHover = false; @override Widget build(BuildContext context) { bool isActive = UrlService.getCurrentUrl() == widget.route; return GestureDetector( onTap: () { if (widget.route != null) { Get.toNamed(widget.route!); } }, child: MouseRegion( cursor: SystemMouseCursors.click, onHover: (event) { setState(() { isHover = true; }); }, onExit: (event) { setState(() { isHover = false; }); }, child: MyContainer.transparent( margin: MySpacing.fromLTRB(4, 0, 8, 4), color: isActive || isHover ? leftBarTheme.activeItemBackground : Colors.transparent, width: MediaQuery.of(context).size.width, padding: MySpacing.xy(18, 7), child: MyText.bodySmall( "${widget.isCondensed ? "" : "- "} ${widget.title}", overflow: TextOverflow.clip, maxLines: 1, textAlign: TextAlign.left, fontSize: 12.5, color: isActive || isHover ? leftBarTheme.activeItemColor : leftBarTheme.onBackground, fontWeight: isActive || isHover ? 600 : 500, ), ), ), ); } } class NavigationItem extends StatefulWidget { final IconData? iconData; final String title; final bool isCondensed; final String? route; const NavigationItem( {super.key, this.iconData, required this.title, this.isCondensed = false, this.route}); @override _NavigationItemState createState() => _NavigationItemState(); } class _NavigationItemState extends State with UIMixin { bool isHover = false; @override Widget build(BuildContext context) { bool isActive = UrlService.getCurrentUrl() == widget.route; return GestureDetector( onTap: () { if (widget.route != null) { Get.toNamed(widget.route!); } }, child: MouseRegion( cursor: SystemMouseCursors.click, onHover: (event) { setState(() { isHover = true; }); }, onExit: (event) { setState(() { isHover = false; }); }, child: MyContainer.transparent( margin: MySpacing.fromLTRB(16, 0, 16, 8), color: isActive || isHover ? leftBarTheme.activeItemBackground : Colors.transparent, padding: MySpacing.xy(8, 8), child: Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ if (widget.iconData != null) Center( child: Icon( widget.iconData, color: (isHover || isActive) ? leftBarTheme.activeItemColor : leftBarTheme.onBackground, size: 20, ), ), if (!widget.isCondensed) Flexible( fit: FlexFit.loose, child: MySpacing.width(16), ), if (!widget.isCondensed) Expanded( flex: 3, child: MyText.labelLarge( widget.title, overflow: TextOverflow.clip, maxLines: 1, color: isActive || isHover ? leftBarTheme.activeItemColor : leftBarTheme.onBackground, ), ) ], ), ), ), ); } }