551 lines
16 KiB
Dart

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/employee_info.dart';
import 'package:marco/helpers/widgets/avatar.dart';
typedef LeftbarMenuFunction = void Function(String key);
class LeftbarObserver {
static Map<String, LeftbarMenuFunction> 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<LeftBar>
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('/home'),
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 Planing",
isCondensed: isCondensed,
route: '/dashboard/daily-task-planing'),
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: () {
Get.offNamed('/auth/login');
},
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<MenuItem> 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<MenuWidget>
with UIMixin, SingleTickerProviderStateMixin {
bool isHover = false;
bool isActive = false;
late Animation<double> _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<double>(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<MenuItem> 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<NavigationItem> 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,
),
)
],
),
),
),
);
}
}