configured the current user info for displaying on sidebar and topbar #5

Merged
vaibhav.surve merged 1 commits from Vaibhav_Task-#136 into main 2025-04-30 10:33:52 +00:00
8 changed files with 430 additions and 268 deletions

View File

@ -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 = <UserPermission>[].obs;
var employeeInfo = Rxn<EmployeeInfo>(); // 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<void> _loadStoredPermissions() async {
// Load permissions and employee info from SharedPreferences
Future<void> _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<dynamic> parsedList = jsonDecode(storedJson);
final List<dynamic> parsedList = jsonDecode(storedPermissionsJson);
permissions.assignAll(
parsedList
.map((e) => UserPermission.fromJson(e as Map<String, dynamic>))
@ -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<void> _storePermissions() async {
// Save permissions and employee info to SharedPreferences
Future<void> _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<void> loadPermissions(String token) async {
// Public method to load permissions and employee info (usually called from outside)
Future<void> 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<void> _loadPermissionsFromAPI() async {
// Internal helper to load token and fetch permissions and employee info from API
Future<void> _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<String?> _getAuthToken() async {
final prefs = await SharedPreferences.getInstance();
return prefs.getString(
'jwt_token'); // Or 'auth_token' if thats the key you're using
return prefs.getString('jwt_token'); // Or 'auth_token' if thats 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<void> _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);

View File

@ -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<List<UserPermission>> 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<dynamic> featurePermissions = decoded['data']['featurePermissions'];
// Debug the decoded data
print('Decoded Data: $decoded');
// Extract featurePermissions
final List<dynamic> 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<UserPermission>(
(permissionId) => UserPermission.fromJson({'id': permissionId}))
.map<UserPermission>((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<EmployeeInfo> 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');
}
}
}

View File

@ -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<bool> setUserPermissions(
List<UserPermission> permissions) async {
@ -48,6 +50,26 @@ class LocalStorage {
return preferences.remove(_userPermissionsKey);
}
// Store EmployeeInfo
static Future<bool> 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<String, dynamic> json = jsonDecode(storedJson);
return EmployeeInfo.fromJson(json);
}
return null; // Return null if no employee info is found
}
static Future<bool> removeEmployeeInfo() async {
return preferences.remove(_employeeInfoKey);
}
// Other methods for handling JWT, refresh token, etc.
static Future<void> init() async {
_preferencesInstance = await SharedPreferences.getInstance();
await initData();

View File

@ -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
),
),
);
}
}

View File

@ -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<String, dynamic> 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<String, dynamic> 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,
};
}
}

View File

@ -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<List<Widget>> _dashboardStats() {
List<Widget> _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<List<Widget>> 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);
}

View File

@ -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,
)
],
),
),
)
],
),
);

View File

@ -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<LeftBar> with SingleTickerProviderStateMixin, UIMixin {
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();
employeeInfo = LocalStorage.getEmployeeInfo();
}
@override
@ -68,75 +75,93 @@ class _LeftBarState extends State<LeftBar> 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<MenuItem> 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<MenuWidget> with UIMixin, SingleTickerProviderStateMixin {
class _MenuWidgetState extends State<MenuWidget>
with UIMixin, SingleTickerProviderStateMixin {
bool isHover = false;
bool isActive = false;
late Animation<double> _iconTurns;
@ -178,8 +210,10 @@ class _MenuWidgetState extends State<MenuWidget> with UIMixin, SingleTickerProvi
@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)));
_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);
}
@ -241,12 +275,16 @@ class _MenuWidgetState extends State<MenuWidget> 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<MenuWidget> 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<MenuWidget> 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<MenuItem> 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<MenuItem> 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<NavigationItem> 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<NavigationItem> 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<NavigationItem> with UIMixin {
widget.title,
overflow: TextOverflow.clip,
maxLines: 1,
color: isActive || isHover ? leftBarTheme.activeItemColor : leftBarTheme.onBackground,
color: isActive || isHover
? leftBarTheme.activeItemColor
: leftBarTheme.onBackground,
),
)
],