diff --git a/lib/controller/permission_controller.dart b/lib/controller/permission_controller.dart index 71dd740..d0eef19 100644 --- a/lib/controller/permission_controller.dart +++ b/lib/controller/permission_controller.dart @@ -4,28 +4,30 @@ import 'package:get/get.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:marco/helpers/services/permission_service.dart'; import 'package:marco/model/user_permission.dart'; +import 'package:marco/model/employee_info.dart'; // Import the EmployeeInfo model class PermissionController extends GetxController { var permissions = [].obs; + var employeeInfo = Rxn(); // Observable for employee info Timer? _refreshTimer; @override void onInit() { super.onInit(); - _loadStoredPermissions(); // Try to load from local storage first - _startAutoRefresh(); // Schedule auto-refresh every 15 minutes + _loadStoredData(); // Load both permissions and employee info + _startAutoRefresh(); // Schedule auto-refresh every 30 minutes } - // Load permissions from SharedPreferences - Future _loadStoredPermissions() async { + // Load permissions and employee info from SharedPreferences + Future _loadStoredData() async { final prefs = await SharedPreferences.getInstance(); - final storedJson = prefs.getString('user_permissions'); - if (storedJson != null) { - print("Loaded Permissions from SharedPreferences: $storedJson"); + // Load stored permissions + final storedPermissionsJson = prefs.getString('user_permissions'); + if (storedPermissionsJson != null) { + print("Loaded Permissions from SharedPreferences: $storedPermissionsJson"); try { - final List parsedList = jsonDecode(storedJson); - + final List parsedList = jsonDecode(storedPermissionsJson); permissions.assignAll( parsedList .map((e) => UserPermission.fromJson(e as Map)) @@ -34,59 +36,93 @@ class PermissionController extends GetxController { } catch (e) { print("Error decoding stored permissions: $e"); await prefs.remove('user_permissions'); - await _loadPermissionsFromAPI(); // fallback to API load } - } else { - // If no permissions stored, fallback to API - await _loadPermissionsFromAPI(); + } + + // Load stored employee info + final storedEmployeeInfoJson = prefs.getString('employee_info'); + if (storedEmployeeInfoJson != null) { + print("Loaded Employee Info from SharedPreferences: $storedEmployeeInfoJson"); + try { + final decodedEmployeeInfo = jsonDecode(storedEmployeeInfoJson); + employeeInfo.value = EmployeeInfo.fromJson(decodedEmployeeInfo); + } catch (e) { + print("Error decoding stored employee info: $e"); + await prefs.remove('employee_info'); + } + } + + // If permissions or employee info are missing, fallback to API + if (storedPermissionsJson == null || storedEmployeeInfoJson == null) { + await _loadDataFromAPI(); } } - // Save permissions to SharedPreferences - Future _storePermissions() async { + // Save permissions and employee info to SharedPreferences + Future _storeData() async { final prefs = await SharedPreferences.getInstance(); - final jsonList = permissions.map((e) => e.toJson()).toList(); - print("Storing Permissions: $jsonList"); - await prefs.setString('user_permissions', jsonEncode(jsonList)); + + // Store permissions + final permissionsJson = permissions.map((e) => e.toJson()).toList(); + print("Storing Permissions: $permissionsJson"); + await prefs.setString('user_permissions', jsonEncode(permissionsJson)); + + // Store employee info + if (employeeInfo.value != null) { + final employeeInfoJson = employeeInfo.value!.toJson(); + print("Storing Employee Info: $employeeInfoJson"); + await prefs.setString('employee_info', jsonEncode(employeeInfoJson)); + } } - // Public method to load permissions (usually called from outside) - Future loadPermissions(String token) async { + // Public method to load permissions and employee info (usually called from outside) + Future loadData(String token) async { try { final result = await PermissionService.fetchPermissions(token); print("Fetched Permissions from API: $result"); permissions.assignAll(result); // Update observable list - await _storePermissions(); // Cache locally + await _storeData(); // Cache locally + + // Also fetch employee info from the API (you can extend the service if needed) + await _loadEmployeeInfoFromAPI(token); } catch (e) { - print('Error loading permissions from API: $e'); + print('Error loading data from API: $e'); } } - // Internal helper to load token and fetch permissions from API - Future _loadPermissionsFromAPI() async { + // Internal helper to load token and fetch permissions and employee info from API + Future _loadDataFromAPI() async { final token = await _getAuthToken(); if (token != null && token.isNotEmpty) { - await loadPermissions(token); + await loadData(token); } else { - print("No token available for fetching permissions."); + print("No token available for fetching data."); } } // Retrieve token from SharedPreferences Future _getAuthToken() async { final prefs = await SharedPreferences.getInstance(); - return prefs.getString( - 'jwt_token'); // Or 'auth_token' if that’s the key you're using + return prefs.getString('jwt_token'); // Or 'auth_token' if that’s the key you're using } - // Auto-refresh every 15 minutes + // Auto-refresh every 30 minutes void _startAutoRefresh() { _refreshTimer = Timer.periodic(Duration(minutes: 30), (timer) async { - await _loadPermissionsFromAPI(); + await _loadDataFromAPI(); }); } + // Load employee info from the API + Future _loadEmployeeInfoFromAPI(String token) async { + final employeeInfoResponse = await PermissionService.fetchEmployeeInfo(token); + print("Fetched Employee Info from API: $employeeInfoResponse"); + + employeeInfo.value = employeeInfoResponse; // Update observable employee info + await _storeData(); // Cache employee info locally + } + // Check for specific permission bool hasPermission(String permissionId) { return permissions.any((p) => p.id == permissionId); diff --git a/lib/helpers/services/permission_service.dart b/lib/helpers/services/permission_service.dart index cb37e3f..620d2bf 100644 --- a/lib/helpers/services/permission_service.dart +++ b/lib/helpers/services/permission_service.dart @@ -1,6 +1,7 @@ import 'package:http/http.dart' as http; import 'dart:convert'; import 'package:marco/model/user_permission.dart'; +import 'package:marco/model/employee_info.dart'; class PermissionService { static Future> fetchPermissions(String token) async { @@ -10,27 +11,12 @@ class PermissionService { headers: {'Authorization': 'Bearer $token'}, ); - // Print the full response for debugging - print('Status Code: ${response.statusCode}'); - print('Response Body: ${response.body}'); - if (response.statusCode == 200) { final decoded = json.decode(response.body); + final List featurePermissions = decoded['data']['featurePermissions']; - // Debug the decoded data - print('Decoded Data: $decoded'); - - // Extract featurePermissions - final List featurePermissions = - decoded['data']['featurePermissions']; - - // Check if the featurePermissions are indeed a List of Strings - print('FeaturePermissions Type: ${featurePermissions.runtimeType}'); - - // Map the featurePermissions to UserPermission objects return featurePermissions - .map( - (permissionId) => UserPermission.fromJson({'id': permissionId})) + .map((permissionId) => UserPermission.fromJson({'id': permissionId})) .toList(); } else { final errorData = json.decode(response.body); @@ -41,4 +27,27 @@ class PermissionService { throw Exception('Error fetching permissions: $e'); } } + + // New method to fetch employee info + static Future fetchEmployeeInfo(String token) async { + try { + final response = await http.get( + Uri.parse('https://stageapi.marcoaiot.com/api/user/profile'), + headers: {'Authorization': 'Bearer $token'}, + ); + + if (response.statusCode == 200) { + final decoded = json.decode(response.body); + final employeeData = decoded['data']['employeeInfo']; + + return EmployeeInfo.fromJson(employeeData); + } else { + final errorData = json.decode(response.body); + throw Exception('Failed to load employee info: ${errorData['message']}'); + } + } catch (e) { + print('Error fetching employee info: $e'); + throw Exception('Error fetching employee info: $e'); + } + } } diff --git a/lib/helpers/services/storage/local_storage.dart b/lib/helpers/services/storage/local_storage.dart index 7837298..1b3b827 100644 --- a/lib/helpers/services/storage/local_storage.dart +++ b/lib/helpers/services/storage/local_storage.dart @@ -3,6 +3,7 @@ import 'package:marco/helpers/services/localizations/language.dart'; import 'package:marco/helpers/theme/theme_customizer.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:marco/model/user_permission.dart'; +import 'package:marco/model/employee_info.dart'; // Import the EmployeeInfo model import 'dart:convert'; class LocalStorage { static const String _loggedInUserKey = "user"; @@ -10,6 +11,8 @@ class LocalStorage { static const String _languageKey = "lang_code"; static const String _jwtTokenKey = "jwt_token"; static const String _refreshTokenKey = "refresh_token"; + static const String _userPermissionsKey = "user_permissions"; + static const String _employeeInfoKey = "employee_info"; static SharedPreferences? _preferencesInstance; @@ -20,7 +23,6 @@ class LocalStorage { return _preferencesInstance!; } // In LocalStorage class - static const String _userPermissionsKey = "user_permissions"; static Future setUserPermissions( List permissions) async { @@ -48,6 +50,26 @@ class LocalStorage { return preferences.remove(_userPermissionsKey); } + // Store EmployeeInfo + static Future setEmployeeInfo(EmployeeInfo employeeInfo) async { + final jsonData = employeeInfo.toJson(); + return preferences.setString(_employeeInfoKey, jsonEncode(jsonData)); + } + + static EmployeeInfo? getEmployeeInfo() { + final storedJson = preferences.getString(_employeeInfoKey); + if (storedJson != null) { + final Map json = jsonDecode(storedJson); + return EmployeeInfo.fromJson(json); + } + return null; // Return null if no employee info is found + } + + static Future removeEmployeeInfo() async { + return preferences.remove(_employeeInfoKey); + } + + // Other methods for handling JWT, refresh token, etc. static Future init() async { _preferencesInstance = await SharedPreferences.getInstance(); await initData(); diff --git a/lib/helpers/widgets/avatar.dart b/lib/helpers/widgets/avatar.dart new file mode 100644 index 0000000..2bef33f --- /dev/null +++ b/lib/helpers/widgets/avatar.dart @@ -0,0 +1,40 @@ +import 'package:flutter/material.dart'; +import 'package:marco/helpers/widgets/my_container.dart'; +import 'package:marco/helpers/widgets/my_text.dart'; +class Avatar extends StatelessWidget { + final String firstName; + final String lastName; + final double size; + final Color backgroundColor; + final Color textColor; + + // Constructor + const Avatar({ + super.key, + required this.firstName, + required this.lastName, + this.size = 46.0, // Default size + this.backgroundColor = Colors.blue, // Default background color + this.textColor = Colors.white, // Default text color + }); + + @override + Widget build(BuildContext context) { + // Extract first letters of firstName and lastName + String initials = "${firstName.isNotEmpty ? firstName[0] : ''}${lastName.isNotEmpty ? lastName[0] : ''}".toUpperCase(); + + return MyContainer.rounded( + height: size, + width: size, + paddingAll: 0, + color: backgroundColor, // Background color of the avatar + child: Center( + child: MyText.labelSmall( + initials, + fontWeight: 600, + color: textColor, // Text color of the initials + ), + ), + ); + } +} diff --git a/lib/model/employee_info.dart b/lib/model/employee_info.dart new file mode 100644 index 0000000..e5b09ab --- /dev/null +++ b/lib/model/employee_info.dart @@ -0,0 +1,77 @@ +class EmployeeInfo { + final int id; + final String firstName; + final String lastName; + final String gender; + final String birthDate; + final String joiningDate; + final String currentAddress; + final String phoneNumber; + final String emergencyPhoneNumber; + final String emergencyContactPerson; + final String aadharNumber; + final bool isActive; + final String? photo; // Nullable photo + final String applicationUserId; + final int jobRoleId; + + EmployeeInfo({ + required this.id, + required this.firstName, + required this.lastName, + required this.gender, + required this.birthDate, + required this.joiningDate, + required this.currentAddress, + required this.phoneNumber, + required this.emergencyPhoneNumber, + required this.emergencyContactPerson, + required this.aadharNumber, + required this.isActive, + this.photo, + required this.applicationUserId, + required this.jobRoleId, + }); + + // Factory constructor to create an instance from JSON + factory EmployeeInfo.fromJson(Map json) { + return EmployeeInfo( + id: json['id'], + firstName: json['firstName'] ?? '', + lastName: json['lastName'] ?? '', + gender: json['gender'] ?? '', + birthDate: json['birthDate'] ?? '', + joiningDate: json['joiningDate'] ?? '', + currentAddress: json['currentAddress'] ?? '', + phoneNumber: json['phoneNumber'] ?? '', + emergencyPhoneNumber: json['emergencyPhoneNumber'] ?? '', + emergencyContactPerson: json['emergencyContactPerson'] ?? '', + aadharNumber: json['aadharNumber'] ?? '', + isActive: json['isActive'] ?? false, + photo: json['photo'], // Photo can be null + applicationUserId: json['applicationUserId'] ?? '', + jobRoleId: json['jobRoleId'] ?? 0, + ); + } + + // Convert the EmployeeInfo instance to a Map (for storage or API) + Map toJson() { + return { + 'id': id, + 'firstName': firstName, + 'lastName': lastName, + 'gender': gender, + 'birthDate': birthDate, + 'joiningDate': joiningDate, + 'currentAddress': currentAddress, + 'phoneNumber': phoneNumber, + 'emergencyPhoneNumber': emergencyPhoneNumber, + 'emergencyContactPerson': emergencyContactPerson, + 'aadharNumber': aadharNumber, + 'isActive': isActive, + 'photo': photo, + 'applicationUserId': applicationUserId, + 'jobRoleId': jobRoleId, + }; + } +} diff --git a/lib/view/dashboard/dashboard_screen.dart b/lib/view/dashboard/dashboard_screen.dart index 5f26dce..86535f0 100644 --- a/lib/view/dashboard/dashboard_screen.dart +++ b/lib/view/dashboard/dashboard_screen.dart @@ -11,9 +11,12 @@ 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/view/layouts/layout.dart'; + class DashboardScreen extends StatelessWidget with UIMixin { DashboardScreen({super.key}); + static const String dashboardRoute = "/dashboard/attendance"; + @override Widget build(BuildContext context) { return Layout( @@ -34,12 +37,7 @@ class DashboardScreen extends StatelessWidget with UIMixin { Padding( padding: MySpacing.x(flexSpacing / 2), child: Column( - children: _dashboardStats().map((rowStats) { - return Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: rowStats, - ); - }).toList(), + children: _buildDashboardStats(), ), ), ], @@ -47,67 +45,30 @@ class DashboardScreen extends StatelessWidget with UIMixin { ); } - List> _dashboardStats() { + List _buildDashboardStats() { final stats = [ - { - "icon": LucideIcons.layout_dashboard, - "title": "Dashboard", - "route": "/dashboard/attendance", - "color": contentTheme.primary - }, - { - "icon": LucideIcons.folder, - "title": "Projects", - "route": "/dashboard/attendance", - "color": contentTheme.secondary - }, - { - "icon": LucideIcons.check, - "title": "Attendence", - "route": "/dashboard/attendance", - "color": contentTheme.success - }, - { - "icon": LucideIcons.users, - "title": "Task", - "route": "/dashboard/attendance", - "color": contentTheme.info - }, + _StatItem(LucideIcons.layout_dashboard, "Dashboard", contentTheme.primary), + _StatItem(LucideIcons.folder, "Projects", contentTheme.secondary), + _StatItem(LucideIcons.check, "Attendance", contentTheme.success), + _StatItem(LucideIcons.users, "Task", contentTheme.info), ]; - // Group the stats into pairs for rows - List> rows = []; - for (int i = 0; i < stats.length; i += 2) { - rows.add([ - _buildStatCard( - icon: stats[i]['icon'] as IconData, - title: stats[i]['title'] as String, - route: stats[i]['route'] as String, - color: stats[i]['color'] as Color, - ), - if (i + 1 < stats.length) _buildStatCard( - icon: stats[i + 1]['icon'] as IconData, - title: stats[i + 1]['title'] as String, - route: stats[i + 1]['route'] as String, - color: stats[i + 1]['color'] as Color, - ), - ]); - } - - return rows; + return List.generate( + (stats.length / 2).ceil(), + (index) => Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + _buildStatCard(stats[index * 2]), + if (index * 2 + 1 < stats.length) _buildStatCard(stats[index * 2 + 1]), + ], + ), + ); } - Widget _buildStatCard({ - required IconData icon, - required String title, - required String route, - required Color color, - String count = "", - String change = "", - }) { + Widget _buildStatCard(_StatItem statItem) { return Expanded( child: InkWell( - onTap: () => Get.toNamed(route), + onTap: () => Get.toNamed(dashboardRoute), child: MyCard.bordered( borderRadiusAll: 10, border: Border.all(color: Colors.grey.withOpacity(0.2)), @@ -117,39 +78,33 @@ class DashboardScreen extends StatelessWidget with UIMixin { child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - MyContainer( - paddingAll: 16, - color: color.withOpacity(0.2), - child: MyContainer( - paddingAll: 8, - color: color, - child: Icon(icon, size: 16, color: contentTheme.light), - ), - ), + _buildStatCardIcon(statItem), MySpacing.height(12), - MyText.labelSmall(title, maxLines: 1), - if (count.isNotEmpty) - MyText.bodyMedium(count, fontWeight: 600, maxLines: 1), - if (change.isNotEmpty) - Padding( - padding: MySpacing.top(8), - child: MyContainer( - padding: MySpacing.xy(6, 4), - color: change.startsWith('+') - ? Colors.green.withOpacity(0.2) - : theme.colorScheme.error.withOpacity(0.2), - child: MyText.labelSmall( - change, - color: change.startsWith('+') - ? Colors.green - : theme.colorScheme.error, - ), - ), - ), + MyText.labelSmall(statItem.title, maxLines: 1), ], ), ), ), ); } + + Widget _buildStatCardIcon(_StatItem statItem) { + return MyContainer( + paddingAll: 16, + color: statItem.color.withOpacity(0.2), + child: MyContainer( + paddingAll: 8, + color: statItem.color, + child: Icon(statItem.icon, size: 16, color: contentTheme.light), + ), + ); + } +} + +class _StatItem { + final IconData icon; + final String title; + final Color color; + + _StatItem(this.icon, this.title, this.color); } diff --git a/lib/view/layouts/layout.dart b/lib/view/layouts/layout.dart index f03c354..e59d55d 100644 --- a/lib/view/layouts/layout.dart +++ b/lib/view/layouts/layout.dart @@ -9,13 +9,15 @@ import 'package:marco/helpers/widgets/my_dashed_divider.dart'; import 'package:marco/helpers/widgets/my_responsive.dart'; import 'package:marco/helpers/widgets/my_spacing.dart'; import 'package:marco/helpers/widgets/my_text.dart'; -import 'package:marco/images.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:marco/view/layouts/left_bar.dart'; import 'package:marco/view/layouts/right_bar.dart'; import 'package:marco/view/layouts/top_bar.dart'; import 'package:marco/widgets/custom_pop_menu.dart'; +import 'package:marco/helpers/services/storage/local_storage.dart'; +import 'package:marco/model/employee_info.dart'; +import 'package:marco/helpers/widgets/avatar.dart'; class Layout extends StatelessWidget { final Widget? child; @@ -23,6 +25,7 @@ class Layout extends StatelessWidget { final LayoutController controller = LayoutController(); final topBarTheme = AdminTheme.theme.topBarTheme; final contentTheme = AdminTheme.theme.contentTheme; + final EmployeeInfo? employeeInfo = LocalStorage.getEmployeeInfo(); Layout({super.key, this.child}); @@ -30,13 +33,13 @@ class Layout extends StatelessWidget { Widget build(BuildContext context) { return MyResponsive(builder: (BuildContext context, _, screenMT) { return GetBuilder( - init: controller, - builder: (controller) { - if (screenMT.isMobile || screenMT.isTablet) { - return mobileScreen(); - } else { - return largeScreen(); - } + init: controller, + builder: (controller) { + if (screenMT.isMobile || screenMT.isTablet) { + return mobileScreen(); + } else { + return largeScreen(); + } }); }); } @@ -47,16 +50,6 @@ class Layout extends StatelessWidget { appBar: AppBar( elevation: 0, actions: [ - InkWell( - onTap: () { - ThemeCustomizer.setTheme(ThemeCustomizer.instance.theme == ThemeMode.dark ? ThemeMode.light : ThemeMode.dark); - }, - child: Icon( - ThemeCustomizer.instance.theme == ThemeMode.dark ? LucideIcons.sun : LucideIcons.moon, - size: 18, - color: topBarTheme.onBackground, - ), - ), MySpacing.width(8), CustomPopupMenu( backdrop: true, @@ -82,13 +75,12 @@ class Layout extends StatelessWidget { menu: Padding( padding: MySpacing.xy(8, 8), child: MyContainer.rounded( - paddingAll: 0, - child: Image.asset( - Images.avatars[0], - height: 28, - width: 28, - fit: BoxFit.cover, - )), + paddingAll: 0, + child: Avatar( + firstName: employeeInfo?.firstName ?? 'First', + lastName: employeeInfo?.lastName ?? 'Name', + ), + ), ), menuBuilder: (_) => buildAccountMenu(), ), @@ -111,21 +103,21 @@ class Layout extends StatelessWidget { children: [ LeftBar(isCondensed: ThemeCustomizer.instance.leftBarCondensed), Expanded( - child: Stack( - children: [ - Positioned( - top: 0, - right: 0, - left: 0, - bottom: 0, - child: SingleChildScrollView( - padding: MySpacing.fromLTRB(0, 58 + flexSpacing, 0, flexSpacing), - key: controller.scrollKey, - child: child, + child: Stack( + children: [ + Positioned( + top: 0, + right: 0, + left: 0, + bottom: 0, + child: SingleChildScrollView( + padding: MySpacing.fromLTRB(0, 58 + flexSpacing, 0, flexSpacing), + key: controller.scrollKey, + child: child, + ), ), - ), - Positioned(top: 0, left: 0, right: 0, child: TopBar()), - ], + Positioned(top: 0, left: 0, right: 0, child: TopBar()), + ], )), ], ), @@ -156,9 +148,9 @@ class Layout extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - buildNotification("Your order is received", "Order #1232 is ready to deliver"), + buildNotification("Welcome to Marco", "Welcome to Marco, we are glad to have you here"), MySpacing.height(12), - buildNotification("Account Security ", "Your account password changed 1 hour ago"), + buildNotification("New update available", "There is a new update available for your app"), ], ), ), @@ -256,32 +248,6 @@ class Layout extends StatelessWidget { height: 1, thickness: 1, ), - Padding( - padding: MySpacing.xy(8, 8), - child: MyButton( - tapTargetSize: MaterialTapTargetSize.shrinkWrap, - onPressed: () => {}, - borderRadiusAll: AppStyle.buttonRadius.medium, - padding: MySpacing.xy(8, 4), - splashColor: contentTheme.danger.withAlpha(28), - backgroundColor: Colors.transparent, - child: Row( - children: [ - Icon( - LucideIcons.log_out, - size: 14, - color: contentTheme.danger, - ), - MySpacing.width(8), - MyText.labelMedium( - "Log out", - fontWeight: 600, - color: contentTheme.danger, - ) - ], - ), - ), - ) ], ), ); diff --git a/lib/view/layouts/left_bar.dart b/lib/view/layouts/left_bar.dart index 110ec72..67006e3 100644 --- a/lib/view/layouts/left_bar.dart +++ b/lib/view/layouts/left_bar.dart @@ -1,3 +1,4 @@ +// All import statements remain unchanged 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'; @@ -11,6 +12,9 @@ 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); @@ -41,15 +45,18 @@ class LeftBar extends StatefulWidget { _LeftBarState createState() => _LeftBarState(); } -class _LeftBarState extends State with SingleTickerProviderStateMixin, UIMixin { +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(); + employeeInfo = LocalStorage.getEmployeeInfo(); } @override @@ -68,75 +75,93 @@ class _LeftBarState extends State with SingleTickerProviderStateMixin, children: [ Center( child: Padding( - padding: MySpacing.y(13), + padding: MySpacing.y(12), 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: 28, + ? (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: [ - Divider(), - labelWidget("Dashboard"), - NavigationItem(iconData: LucideIcons.layout_dashboard, title: "Dashboard", isCondensed: isCondensed, route: '/dashboard'), - NavigationItem(iconData: LucideIcons.layout_template, title: "Attendance", isCondensed: isCondensed, route: '/dashboard/attendance'), - ], - ), - )), - if (!isCondensed) Divider(), - if (!isCondensed) - MyContainer.transparent( - paddingAll: 12, - child: Row( + child: ScrollConfiguration( + behavior: + ScrollConfiguration.of(context).copyWith(scrollbars: false), + child: ListView( + shrinkWrap: true, + controller: ScrollController(), + physics: BouncingScrollPhysics(), + clipBehavior: Clip.antiAliasWithSaveLayer, children: [ - MyContainer.rounded( - height: 46, - width: 46, - paddingAll: 0, - child: Image.asset(Images.avatars[0]), - ), - MySpacing.width(16), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - MyText.labelMedium("Jonathan", fontWeight: 600), - MySpacing.height(8), - MyText.labelSmall("jonathan@gmail.com", fontWeight: 600, muted: true), - ], - ), - ), - MyContainer( - onTap: () { - Get.toNamed('/auth/login'); - }, - color: leftBarTheme.activeItemBackground, - paddingAll: 8, - child: Icon(LucideIcons.log_out, size: 16, color: leftBarTheme.activeItemColor), - ) + Divider(), + labelWidget("Dashboard"), + NavigationItem( + iconData: LucideIcons.layout_dashboard, + title: "Dashboard", + isCondensed: isCondensed, + route: '/dashboard'), + NavigationItem( + iconData: LucideIcons.layout_template, + title: "Attendance", + isCondensed: isCondensed, + route: '/dashboard/attendance'), ], ), ), + ), + Divider(), + if (!isCondensed) userInfoSection(), ], ), ), ); } + Widget userInfoSection() { + 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() @@ -161,13 +186,20 @@ class MenuWidget extends StatefulWidget { 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 []}); + 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 { +class _MenuWidgetState extends State + with UIMixin, SingleTickerProviderStateMixin { bool isHover = false; bool isActive = false; late Animation _iconTurns; @@ -178,8 +210,10 @@ class _MenuWidgetState extends State with UIMixin, SingleTickerProvi @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))); + _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); } @@ -241,12 +275,16 @@ class _MenuWidgetState extends State with UIMixin, SingleTickerProvi }, child: MyContainer.transparent( margin: MySpacing.fromLTRB(16, 0, 16, 8), - color: isActive || isHover ? leftBarTheme.activeItemBackground : Colors.transparent, + 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, + color: (isHover || isActive) + ? leftBarTheme.activeItemColor + : leftBarTheme.onBackground, size: 20, ), ), @@ -308,7 +346,9 @@ class _MenuWidgetState extends State with UIMixin, SingleTickerProvi Icon( widget.iconData, size: 20, - color: isHover || isActive ? leftBarTheme.activeItemColor : leftBarTheme.onBackground, + color: isHover || isActive + ? leftBarTheme.activeItemColor + : leftBarTheme.onBackground, ), MySpacing.width(18), Expanded( @@ -317,7 +357,9 @@ class _MenuWidgetState extends State with UIMixin, SingleTickerProvi maxLines: 1, overflow: TextOverflow.ellipsis, textAlign: TextAlign.start, - color: isHover || isActive ? leftBarTheme.activeItemColor : leftBarTheme.onBackground, + color: isHover || isActive + ? leftBarTheme.activeItemColor + : leftBarTheme.onBackground, ), ), ], @@ -386,7 +428,9 @@ class _MenuItemState extends State with UIMixin { }, child: MyContainer.transparent( margin: MySpacing.fromLTRB(4, 0, 8, 4), - color: isActive || isHover ? leftBarTheme.activeItemBackground : Colors.transparent, + color: isActive || isHover + ? leftBarTheme.activeItemBackground + : Colors.transparent, width: MediaQuery.of(context).size.width, padding: MySpacing.xy(18, 7), child: MyText.bodySmall( @@ -395,7 +439,9 @@ class _MenuItemState extends State with UIMixin { maxLines: 1, textAlign: TextAlign.left, fontSize: 12.5, - color: isActive || isHover ? leftBarTheme.activeItemColor : leftBarTheme.onBackground, + color: isActive || isHover + ? leftBarTheme.activeItemColor + : leftBarTheme.onBackground, fontWeight: isActive || isHover ? 600 : 500, ), ), @@ -410,7 +456,12 @@ class NavigationItem extends StatefulWidget { final bool isCondensed; final String? route; - const NavigationItem({super.key, this.iconData, required this.title, this.isCondensed = false, this.route}); + const NavigationItem( + {super.key, + this.iconData, + required this.title, + this.isCondensed = false, + this.route}); @override _NavigationItemState createState() => _NavigationItemState(); @@ -442,7 +493,9 @@ class _NavigationItemState extends State with UIMixin { }, child: MyContainer.transparent( margin: MySpacing.fromLTRB(16, 0, 16, 8), - color: isActive || isHover ? leftBarTheme.activeItemBackground : Colors.transparent, + color: isActive || isHover + ? leftBarTheme.activeItemBackground + : Colors.transparent, padding: MySpacing.xy(8, 8), child: Row( crossAxisAlignment: CrossAxisAlignment.center, @@ -451,7 +504,9 @@ class _NavigationItemState extends State with UIMixin { Center( child: Icon( widget.iconData, - color: (isHover || isActive) ? leftBarTheme.activeItemColor : leftBarTheme.onBackground, + color: (isHover || isActive) + ? leftBarTheme.activeItemColor + : leftBarTheme.onBackground, size: 20, ), ), @@ -467,7 +522,9 @@ class _NavigationItemState extends State with UIMixin { widget.title, overflow: TextOverflow.clip, maxLines: 1, - color: isActive || isHover ? leftBarTheme.activeItemColor : leftBarTheme.onBackground, + color: isActive || isHover + ? leftBarTheme.activeItemColor + : leftBarTheme.onBackground, ), ) ],