configured the current user info for displaying on sidebar and topbar #5
@ -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 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<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);
|
||||
|
@ -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');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
|
40
lib/helpers/widgets/avatar.dart
Normal file
40
lib/helpers/widgets/avatar.dart
Normal 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
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
77
lib/model/employee_info.dart
Normal file
77
lib/model/employee_info.dart
Normal 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,
|
||||
};
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
@ -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,
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
|
@ -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,
|
||||
),
|
||||
)
|
||||
],
|
||||
|
Loading…
x
Reference in New Issue
Block a user