diff --git a/lib/controller/dashboard/add_employee_controller.dart b/lib/controller/dashboard/add_employee_controller.dart new file mode 100644 index 0000000..56d0d67 --- /dev/null +++ b/lib/controller/dashboard/add_employee_controller.dart @@ -0,0 +1,119 @@ +import 'package:file_picker/file_picker.dart'; +import 'package:flutter/material.dart'; +import 'package:marco/controller/my_controller.dart'; +import 'package:marco/helpers/widgets/my_form_validator.dart'; +import 'package:marco/helpers/services/api_service.dart'; +import 'package:get/get.dart'; +import 'package:logger/logger.dart'; + +enum Gender { + male, + female, + other; + + const Gender(); +} + +final Logger logger = Logger(); + +class AddEmployeeController extends MyController { + List files = []; + MyFormValidator basicValidator = MyFormValidator(); + Gender? selectedGender; + List> roles = []; + String? selectedRoleId; + + @override + void onInit() { + super.onInit(); + logger.i("Initializing AddEmployeeController..."); + fetchRoles(); + basicValidator.addField( + 'first_name', + label: "First Name", + required: true, + controller: TextEditingController(), + ); + basicValidator.addField( + 'phone_number', + label: "Phone Number", + required: true, + controller: TextEditingController(), + ); + basicValidator.addField( + 'last_name', + label: "Last Name", + required: true, + controller: TextEditingController(), + ); + logger.i("Fields initialized for first_name, phone_number, last_name."); + } + + bool showOnline = true; + + final List categories = []; + + void onGenderSelected(Gender? gender) { + selectedGender = gender; + logger.i("Gender selected: ${gender?.name}"); + update(); + } + + Future fetchRoles() async { + logger.i("Fetching roles..."); + final result = await ApiService.getRoles(); + if (result != null) { + roles = List>.from(result); + logger.i("Roles fetched successfully."); + update(); + } else { + logger.e("Failed to fetch roles."); + } + } + + void onRoleSelected(String? roleId) { + selectedRoleId = roleId; + logger.i("Role selected: $roleId"); + update(); + } + + Future createEmployees() async { + logger.i("Starting employee creation..."); + if (selectedGender == null || selectedRoleId == null) { + logger.w("Missing gender or role."); + Get.snackbar( + "Missing Fields", + "Please select both Gender and Role.", + snackPosition: SnackPosition.BOTTOM, + ); + return; + } + + final firstName = basicValidator.getController("first_name")?.text.trim(); + final lastName = basicValidator.getController("last_name")?.text.trim(); + final phoneNumber = + basicValidator.getController("phone_number")?.text.trim(); + + logger.i( + "Creating employee with Name: $firstName $lastName, Phone: $phoneNumber, Gender: ${selectedGender!.name}"); + + final response = await ApiService.createEmployee( + firstName: firstName!, + lastName: lastName!, + phoneNumber: phoneNumber!, + gender: selectedGender!.name, + jobRoleId: selectedRoleId!, + ); + + if (response == true) { + logger.i("Employee created successfully."); + Get.back(); // Or navigate as needed + Get.snackbar("Success", "Employee created successfully!", + snackPosition: SnackPosition.BOTTOM); + } else { + logger.e("Failed to create employee."); + Get.snackbar("Error", "Failed to create employee.", + snackPosition: SnackPosition.BOTTOM); + } + } +} diff --git a/lib/controller/dashboard/employees_screen_controller.dart b/lib/controller/dashboard/employees_screen_controller.dart new file mode 100644 index 0000000..023e908 --- /dev/null +++ b/lib/controller/dashboard/employees_screen_controller.dart @@ -0,0 +1,102 @@ +import 'package:get/get.dart'; +import 'package:logger/logger.dart'; +import 'package:marco/helpers/services/api_service.dart'; +import 'package:marco/model/attendance_model.dart'; +import 'package:marco/model/project_model.dart'; +import 'package:marco/model/employee_model.dart'; + +final Logger log = Logger(); + +class EmployeesScreenController extends GetxController { + List attendances = []; + List projects = []; + String? selectedProjectId; + List employees = []; + + RxBool isLoading = false.obs; + RxMap uploadingStates = {}.obs; + + @override + void onInit() { + super.onInit(); + fetchAllProjects(); + } + + Future fetchAllProjects() async { + isLoading.value = true; + await _handleApiCall( + ApiService.getProjects, + onSuccess: (data) { + projects = data.map((json) => ProjectModel.fromJson(json)).toList(); + log.i("Projects fetched: ${projects.length} projects loaded."); + }, + onEmpty: () => log.w("No project data found or API call failed."), + ); + isLoading.value = false; + update(); + } + + Future fetchAllEmployees() async { + isLoading.value = true; + await _handleApiCall( + ApiService.getAllEmployees, + onSuccess: (data) { + employees = data.map((json) => EmployeeModel.fromJson(json)).toList(); + log.i("All Employees fetched: ${employees.length} employees loaded."); + }, + onEmpty: () => log.w("No Employee data found or API call failed."), + ); + isLoading.value = false; + update(); + } + + Future fetchEmployeesByProject(String? projectId) async { + if (projectId == null || projectId.isEmpty) { + log.e("Project ID is required but was null or empty."); + return; + } + + isLoading.value = true; + await _handleApiCall( + () => ApiService.getAllEmployeesByProject(projectId), + onSuccess: (data) { + employees = data.map((json) => EmployeeModel.fromJson(json)).toList(); + for (var emp in employees) { + uploadingStates[emp.id] = false.obs; + } + log.i("Employees fetched: ${employees.length} for project $projectId"); + update(); + }, + onEmpty: () { + log.w("No employees found for project $projectId."); + employees = []; + update(); + }, + onError: (e) => + log.e("Error fetching employees for project $projectId: $e"), + ); + isLoading.value = false; + } + + Future _handleApiCall( + Future?> Function() apiCall, { + required Function(List) onSuccess, + required Function() onEmpty, + Function(dynamic error)? onError, + }) async { + try { + final response = await apiCall(); + if (response != null && response.isNotEmpty) { + onSuccess(response); + } else { + onEmpty(); + } + } catch (e) { + if (onError != null) { + onError(e); + } else { + log.e("API call error: $e"); + } + } + } +} diff --git a/lib/helpers/services/api_endpoints.dart b/lib/helpers/services/api_endpoints.dart index 212bfac..3901ada 100644 --- a/lib/helpers/services/api_endpoints.dart +++ b/lib/helpers/services/api_endpoints.dart @@ -10,5 +10,8 @@ class ApiEndpoints { static const String uploadAttendanceImage = "/attendance/record-image"; // Employee Screen API Endpoints - + static const String getAllEmployeesByProject = "/Project/employees/get"; + static const String getAllEmployees = "/employee/list"; + static const String getRoles = "/roles/jobrole"; + static const String createEmployee = "/employee/manage-mobile"; } diff --git a/lib/helpers/services/api_service.dart b/lib/helpers/services/api_service.dart index 68b4cda..abb7bc5 100644 --- a/lib/helpers/services/api_service.dart +++ b/lib/helpers/services/api_service.dart @@ -53,7 +53,7 @@ class ApiService { Uri uri = Uri.parse("${ApiEndpoints.baseUrl}$endpoint") .replace(queryParameters: queryParams); - _log('GET request: $uri'); + _log("GET $uri"); try { http.Response response = @@ -69,7 +69,7 @@ class ApiService { queryParams: queryParams, hasRetried: true); } } - _log("Refresh failed."); + _log("Token refresh failed."); } return response; } catch (e) { @@ -78,35 +78,44 @@ class ApiService { } } - static Future _postRequest(String endpoint, dynamic body) async { + static Future _postRequest( + String endpoint, dynamic body) async { String? token = await _getToken(); if (token == null) return null; final uri = Uri.parse("${ApiEndpoints.baseUrl}$endpoint"); - _log("POST request to $uri with body: $body"); + + _log("POST $uri"); + _log("Headers: ${_headers(token)}"); + _log("Body: $body"); try { final response = await http .post(uri, headers: _headers(token), body: jsonEncode(body)) .timeout(timeout); + + _log("Response Status: ${response.statusCode}"); return response; } catch (e) { _log("HTTP POST Exception: $e"); return null; } } - - // ===== API Calls ===== + // ===== Attendence Screen API Calls ===== static Future?> getProjects() async { final response = await _getRequest(ApiEndpoints.getProjects); - return response != null ? _parseResponse(response, label: 'Projects') : null; + return response != null + ? _parseResponse(response, label: 'Projects') + : null; } static Future?> getEmployeesByProject(String projectId) async { final response = await _getRequest(ApiEndpoints.getEmployeesByProject, queryParams: {"projectId": projectId}); - return response != null ? _parseResponse(response, label: 'Employees') : null; + return response != null + ? _parseResponse(response, label: 'Employees') + : null; } static Future?> getAttendanceLogs(String projectId, @@ -115,24 +124,30 @@ class ApiService { "projectId": projectId, if (dateFrom != null) "dateFrom": DateFormat('yyyy-MM-dd').format(dateFrom), - if (dateTo != null) - "dateTo": DateFormat('yyyy-MM-dd').format(dateTo), + if (dateTo != null) "dateTo": DateFormat('yyyy-MM-dd').format(dateTo), }; final response = await _getRequest(ApiEndpoints.getAttendanceLogs, queryParams: query); - return response != null ? _parseResponse(response, label: 'Attendance Logs') : null; + return response != null + ? _parseResponse(response, label: 'Attendance Logs') + : null; } static Future?> getAttendanceLogView(String id) async { - final response = await _getRequest("${ApiEndpoints.getAttendanceLogView}/$id"); - return response != null ? _parseResponse(response, label: 'Log Details') : null; + final response = + await _getRequest("${ApiEndpoints.getAttendanceLogView}/$id"); + return response != null + ? _parseResponse(response, label: 'Log Details') + : null; } static Future?> getRegularizationLogs(String projectId) async { final response = await _getRequest(ApiEndpoints.getRegularizationLogs, queryParams: {"projectId": projectId}); - return response != null ? _parseResponse(response, label: 'Regularization Logs') : null; + return response != null + ? _parseResponse(response, label: 'Regularization Logs') + : null; } // ===== Upload Attendance Image ===== @@ -204,4 +219,74 @@ class ApiService { final imageNumber = count.toString().padLeft(3, '0'); return "${employeeId}_${dateStr}_$imageNumber.jpg"; } + +// ===== Employee Screen API Calls ===== + static Future?> getAllEmployeesByProject( + String projectId) async { + if (projectId.isEmpty) { + throw ArgumentError('projectId must not be empty'); + } + + final String endpoint = + "${ApiEndpoints.getAllEmployeesByProject}/$projectId"; + final response = await _getRequest(endpoint); + + return response != null + ? _parseResponse(response, label: 'Employees by Project') + : null; + } + + static Future?> getAllEmployees() async { + final response = await _getRequest(ApiEndpoints.getAllEmployees); + return response != null + ? _parseResponse(response, label: 'All Employees') + : null; + } + + static Future?> getRoles() async { + final response = await _getRequest(ApiEndpoints.getRoles); + return response != null + ? _parseResponse(response, label: 'All Employees') + : null; + } + + static Future createEmployee({ + required String firstName, + required String lastName, + required String phoneNumber, + required String gender, + required String jobRoleId, + }) async { + final body = { + "firstName": firstName, + "lastName": lastName, + "phoneNumber": phoneNumber, + "gender": gender, + "jobRoleId": jobRoleId, + }; + + // Make the API request + final response = await _postRequest(ApiEndpoints.createEmployee, body); + + if (response == null) { + _log("Error: No response from server."); + return false; + } + + final json = jsonDecode(response.body); + + if (response.statusCode == 200) { + if (json['success'] == true) { + return true; + } else { + _log( + "Failed to create employee: ${json['message'] ?? 'Unknown error'}"); + return false; + } + } else { + _log( + "Failed to create employee. Status code: ${response.statusCode}, Response: ${json['message'] ?? 'No message'}"); + return false; + } + } } diff --git a/lib/helpers/widgets/my_refresh_wrapper.dart b/lib/helpers/widgets/my_refresh_wrapper.dart index ce75a39..bfa27c3 100644 --- a/lib/helpers/widgets/my_refresh_wrapper.dart +++ b/lib/helpers/widgets/my_refresh_wrapper.dart @@ -1,26 +1,23 @@ import 'package:flutter/material.dart'; -class MyRefreshWrapper extends StatelessWidget { +class MyRefreshableContent extends StatelessWidget { final Future Function() onRefresh; final Widget child; - final EdgeInsetsGeometry? padding; - const MyRefreshWrapper({ + const MyRefreshableContent({ Key? key, required this.onRefresh, required this.child, - this.padding, }) : super(key: key); @override Widget build(BuildContext context) { return RefreshIndicator( onRefresh: onRefresh, - backgroundColor: Colors.red, // Set background color to red - color: Colors.white, // Set spinner color to white + backgroundColor: Colors.red, + color: Colors.white, child: SingleChildScrollView( - padding: padding, - physics: const AlwaysScrollableScrollPhysics(), + physics: const AlwaysScrollableScrollPhysics(), child: child, ), ); diff --git a/lib/model/employee_model.dart b/lib/model/employee_model.dart index 6dd4c8b..4a9dc19 100644 --- a/lib/model/employee_model.dart +++ b/lib/model/employee_model.dart @@ -7,6 +7,9 @@ class EmployeeModel { final String checkOut; final int activity; int action; + final String jobRole; + final String email; + final String phoneNumber; EmployeeModel({ required this.id, @@ -17,6 +20,9 @@ class EmployeeModel { required this.checkOut, required this.activity, required this.action, + required this.jobRole, + required this.email, + required this.phoneNumber, }); factory EmployeeModel.fromJson(Map json) { @@ -29,6 +35,9 @@ class EmployeeModel { checkOut: json['checkOut']?.toString() ?? '-', action: json['action'] ?? 0, activity: json['activity'] ?? 0, + jobRole: json['jobRole']?.toString() ?? '-', + email: json['email']?.toString() ?? '-', + phoneNumber: json['phoneNumber']?.toString() ?? '-', ); } @@ -43,6 +52,9 @@ class EmployeeModel { 'checkOut': checkOut, 'action': action, 'activity': activity, + 'jobRole': jobRole.isEmpty ? '-' : jobRole, + 'email': email.isEmpty ? '-' : email, + 'phoneNumber': phoneNumber.isEmpty ? '-' : phoneNumber, }; } } diff --git a/lib/routes.dart b/lib/routes.dart index f28f214..77a1abe 100644 --- a/lib/routes.dart +++ b/lib/routes.dart @@ -12,6 +12,9 @@ import 'package:marco/view/error_pages/error_500_screen.dart'; // import 'package:marco/view/dashboard/attendance_screen.dart'; import 'package:marco/view/dashboard/attendanceScreen.dart'; import 'package:marco/view/dashboard/dashboard_screen.dart'; +import 'package:marco/view/dashboard/add_employee_screen.dart'; +import 'package:marco/view/dashboard/employee_screen.dart'; + class AuthMiddleware extends GetMiddleware { @override RouteSettings? redirect(String? route) { @@ -21,20 +24,58 @@ class AuthMiddleware extends GetMiddleware { getPageRoute() { var routes = [ - GetPage(name: '/', page: () => const AttendanceScreen(), middlewares: [AuthMiddleware()]), + GetPage( + name: '/', + page: () => const AttendanceScreen(), + middlewares: [AuthMiddleware()]), // Dashboard - GetPage(name: '/dashboard/attendance', page: () => AttendanceScreen(), middlewares: [AuthMiddleware()]), - GetPage(name: '/dashboard', page: () => DashboardScreen(), middlewares: [AuthMiddleware()]), + GetPage( + name: '/dashboard/attendance', + page: () => AttendanceScreen(), + middlewares: [AuthMiddleware()]), + GetPage( + name: '/dashboard', + page: () => DashboardScreen(), + middlewares: [AuthMiddleware()]), + GetPage( + name: '/dashboard/employees', + page: () => EmployeeScreen(), + middlewares: [AuthMiddleware()]), + // Employees Creation + GetPage( + name: '/employees/addEmployee', + page: () => AddEmployeeScreen(), + middlewares: [AuthMiddleware()]), // Authentication GetPage(name: '/auth/login', page: () => LoginScreen()), - GetPage(name: '/auth/register_account', page: () => const RegisterAccountScreen()), - GetPage(name: '/auth/forgot_password', page: () => const ForgotPasswordScreen()), - GetPage(name: '/auth/reset_password', page: () => const ResetPasswordScreen()), + GetPage( + name: '/auth/register_account', + page: () => const RegisterAccountScreen()), + GetPage( + name: '/auth/forgot_password', + page: () => const ForgotPasswordScreen()), + GetPage( + name: '/auth/reset_password', page: () => const ResetPasswordScreen()), // Error - GetPage(name: '/error/coming_soon', page: () => ComingSoonScreen(), middlewares: [AuthMiddleware()]), - GetPage(name: '/error/500', page: () => Error500Screen(), middlewares: [AuthMiddleware()]), - GetPage(name: '/error/404', page: () => Error404Screen(), middlewares: [AuthMiddleware()]), + GetPage( + name: '/error/coming_soon', + page: () => ComingSoonScreen(), + middlewares: [AuthMiddleware()]), + GetPage( + name: '/error/500', + page: () => Error500Screen(), + middlewares: [AuthMiddleware()]), + GetPage( + name: '/error/404', + page: () => Error404Screen(), + middlewares: [AuthMiddleware()]), ]; - return routes.map((e) => GetPage(name: e.name, page: e.page, middlewares: e.middlewares, transition: Transition.noTransition)).toList(); + return routes + .map((e) => GetPage( + name: e.name, + page: e.page, + middlewares: e.middlewares, + transition: Transition.noTransition)) + .toList(); } diff --git a/lib/view/dashboard/add_employee_screen.dart b/lib/view/dashboard/add_employee_screen.dart new file mode 100644 index 0000000..e0829f4 --- /dev/null +++ b/lib/view/dashboard/add_employee_screen.dart @@ -0,0 +1,270 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_lucide/flutter_lucide.dart'; +import 'package:get/get.dart'; +import 'package:marco/controller/dashboard/add_employee_controller.dart'; +import 'package:marco/helpers/theme/app_theme.dart'; +import 'package:marco/helpers/utils/mixins/ui_mixin.dart'; +import 'package:marco/helpers/utils/my_shadow.dart'; +import 'package:marco/helpers/widgets/my_breadcrumb.dart'; +import 'package:marco/helpers/widgets/my_breadcrumb_item.dart'; +import 'package:marco/helpers/widgets/my_button.dart'; +import 'package:marco/helpers/widgets/my_card.dart'; +import 'package:marco/helpers/widgets/my_flex.dart'; +import 'package:marco/helpers/widgets/my_flex_item.dart'; +import 'package:marco/helpers/widgets/my_spacing.dart'; +import 'package:marco/helpers/widgets/my_text.dart'; +import 'package:marco/helpers/widgets/my_text_style.dart'; +import 'package:marco/view/layouts/layout.dart'; + +class AddEmployeeScreen extends StatefulWidget { + const AddEmployeeScreen({super.key}); + + @override + State createState() => _AddEmployeeScreenState(); +} + +class _AddEmployeeScreenState extends State with UIMixin { + AddEmployeeController controller = Get.put(AddEmployeeController()); + + @override + Widget build(BuildContext context) { + return Layout( + child: GetBuilder( + init: controller, + tag: 'add_employee_controller', + builder: (controller) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: MySpacing.x(flexSpacing), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + MyText.titleMedium( + "Add Employee", + fontSize: 18, + fontWeight: 600, + ), + MyBreadcrumb( + children: [ + MyBreadcrumbItem(name: 'Employee'), + MyBreadcrumbItem(name: 'Add Employee'), + ], + ), + ], + ), + ), + MySpacing.height(flexSpacing), + Padding( + padding: MySpacing.x(flexSpacing / 2), + child: MyFlex( + children: [ + MyFlexItem(sizes: "lg-8 md-12", child: detail()), + ], + ), + ), + ], + ); + }, + ), + ); + } + + Widget detail() { + return Form( + key: controller + .basicValidator.formKey, // Ensure the key is correctly assigned + child: MyCard.bordered( + borderRadiusAll: 4, + border: Border.all(color: Colors.grey.withOpacity(0.2)), + shadow: MyShadow(elevation: 1, position: MyShadowPosition.bottom), + paddingAll: 24, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon(LucideIcons.server, size: 16), + MySpacing.width(12), + MyText.titleMedium("General", fontWeight: 600), + ], + ), + MySpacing.height(24), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + MyText.labelMedium("First Name"), + MySpacing.height(8), + TextFormField( + validator: + controller.basicValidator.getValidation('first_name'), + controller: + controller.basicValidator.getController('first_name'), + keyboardType: TextInputType.name, + decoration: InputDecoration( + hintText: "eg: Jhon", + hintStyle: MyTextStyle.bodySmall(xMuted: true), + border: outlineInputBorder, + enabledBorder: outlineInputBorder, + focusedBorder: focusedInputBorder, + contentPadding: MySpacing.all(16), + isCollapsed: true, + floatingLabelBehavior: FloatingLabelBehavior.never, + ), + ), + MySpacing.height(24), + MyText.labelMedium("Last Name"), + MySpacing.height(8), + TextFormField( + validator: + controller.basicValidator.getValidation('last_name'), + controller: + controller.basicValidator.getController('last_name'), + keyboardType: TextInputType.name, + decoration: InputDecoration( + hintText: "eg: Doe", + hintStyle: MyTextStyle.bodySmall(xMuted: true), + border: outlineInputBorder, + enabledBorder: outlineInputBorder, + focusedBorder: focusedInputBorder, + contentPadding: MySpacing.all(16), + isCollapsed: true, + floatingLabelBehavior: FloatingLabelBehavior.never, + ), + ), + MySpacing.height(24), + MyText.labelMedium("Phone Number"), + MySpacing.height(8), + TextFormField( + validator: + controller.basicValidator.getValidation('phone_number'), + controller: + controller.basicValidator.getController('phone_number'), + keyboardType: TextInputType.phone, + decoration: InputDecoration( + hintText: "eg: +91 9876543210", + hintStyle: MyTextStyle.bodySmall(xMuted: true), + border: outlineInputBorder, + enabledBorder: outlineInputBorder, + focusedBorder: focusedInputBorder, + contentPadding: MySpacing.all(16), + isCollapsed: true, + floatingLabelBehavior: FloatingLabelBehavior.never, + ), + ), + MySpacing.height(24), + MyFlex(contentPadding: false, children: [ + MyFlexItem( + sizes: 'lg-6 md-12', + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + MyText.labelMedium("Select Gender"), + MySpacing.height(8), + DropdownButtonFormField( + value: controller.selectedGender, + dropdownColor: contentTheme.background, + menuMaxHeight: 200, + isDense: true, + items: Gender.values.map((gender) { + return DropdownMenuItem( + value: gender, + child: MyText.labelMedium( + gender.name[0].toUpperCase() + + gender.name.substring(1), + ), + ); + }).toList(), + icon: const Icon(Icons.expand_more, size: 20), + decoration: InputDecoration( + hintText: "Select Gender", + hintStyle: MyTextStyle.bodySmall(xMuted: true), + border: outlineInputBorder, + enabledBorder: outlineInputBorder, + focusedBorder: focusedInputBorder, + contentPadding: MySpacing.all(14), + isCollapsed: true, + floatingLabelBehavior: FloatingLabelBehavior.never, + ), + onChanged: controller.onGenderSelected, + ), + ], + ), + ), + ]), + MySpacing.height(24), + MyFlex(contentPadding: false, children: [ + MyFlexItem( + sizes: 'lg-6 md-12', + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + MyText.labelMedium("Select Role"), + MySpacing.height(8), + DropdownButtonFormField( + value: controller.selectedRoleId, + dropdownColor: contentTheme.background, + decoration: InputDecoration( + hintText: "Select Role", + hintStyle: MyTextStyle.bodySmall(xMuted: true), + border: outlineInputBorder, + enabledBorder: outlineInputBorder, + focusedBorder: focusedInputBorder, + contentPadding: MySpacing.all(14), + isCollapsed: true, + floatingLabelBehavior: FloatingLabelBehavior.never, + ), + icon: const Icon(Icons.expand_more, size: 20), + isDense: true, + items: controller.roles.map((role) { + return DropdownMenuItem( + value: role['id'], + child: Text(role['name']), + ); + }).toList(), + onChanged: controller.onRoleSelected, + ), + ], + ), + ), + ]), + MySpacing.height(24), + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + MyButton.text( + onPressed: () { + Get.toNamed('/dashboard/employees'); + }, + padding: MySpacing.xy(20, 16), + splashColor: + contentTheme.secondary.withValues(alpha: 0.1), + child: MyText.bodySmall('Cancel'), + ), + MySpacing.width(12), + MyButton( + onPressed: () async { + if (controller.basicValidator.validateForm()) { + await controller.createEmployees(); + } + }, + elevation: 0, + padding: MySpacing.xy(20, 16), + backgroundColor: contentTheme.primary, + borderRadiusAll: AppStyle.buttonRadius.medium, + child: MyText.bodySmall( + 'Save', + color: contentTheme.onPrimary, + ), + ), + ], + ), + ], + ), + ], + ), + ), + ); + } +} diff --git a/lib/view/dashboard/attendanceScreen.dart b/lib/view/dashboard/attendanceScreen.dart index 982b6b8..6a40650 100644 --- a/lib/view/dashboard/attendanceScreen.dart +++ b/lib/view/dashboard/attendanceScreen.dart @@ -40,7 +40,7 @@ class _AttendanceScreenState extends State with UIMixin { @override Widget build(BuildContext context) { return Layout( - child: MyRefreshWrapper( + child: MyRefreshableContent( onRefresh: () async { if (attendanceController.selectedProjectId != null) { await attendanceController.fetchEmployeesByProject( diff --git a/lib/view/dashboard/employee_screen.dart b/lib/view/dashboard/employee_screen.dart new file mode 100644 index 0000000..ae083ae --- /dev/null +++ b/lib/view/dashboard/employee_screen.dart @@ -0,0 +1,260 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:marco/helpers/theme/app_theme.dart'; +import 'package:marco/helpers/utils/mixins/ui_mixin.dart'; +import 'package:marco/helpers/widgets/my_breadcrumb.dart'; +import 'package:marco/helpers/widgets/my_breadcrumb_item.dart'; +import 'package:marco/helpers/widgets/my_flex.dart'; +import 'package:marco/helpers/widgets/my_flex_item.dart'; +import 'package:marco/helpers/widgets/my_spacing.dart'; +import 'package:marco/helpers/widgets/my_text.dart'; +import 'package:marco/view/layouts/layout.dart'; +import 'package:marco/controller/permission_controller.dart'; +import 'package:marco/controller/dashboard/employees_screen_controller.dart'; +import 'package:marco/helpers/widgets/my_loading_component.dart'; +import 'package:marco/helpers/widgets/my_refresh_wrapper.dart'; +import 'package:marco/model/my_paginated_table.dart'; + +class EmployeeScreen extends StatefulWidget { + const EmployeeScreen({super.key}); + + @override + State createState() => _EmployeeScreenState(); +} + +class _EmployeeScreenState extends State with UIMixin { + final EmployeesScreenController employeesScreenController = + Get.put(EmployeesScreenController()); + final PermissionController permissionController = + Get.put(PermissionController()); + + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addPostFrameCallback((_) async { + employeesScreenController.selectedProjectId = null; + await employeesScreenController.fetchAllEmployees(); + employeesScreenController.update(); + }); + } + + @override + Widget build(BuildContext context) { + return Layout( + child: Stack( + children: [ + GetBuilder( + init: employeesScreenController, + builder: (controller) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: MySpacing.x(flexSpacing), + child: MyText.titleMedium("Employee", + fontSize: 18, fontWeight: 600), + ), + MySpacing.height(flexSpacing), + Padding( + padding: MySpacing.x(flexSpacing), + child: MyBreadcrumb( + children: [ + MyBreadcrumbItem(name: 'Dashboard'), + MyBreadcrumbItem(name: 'Employee', active: true), + ], + ), + ), + MySpacing.height(flexSpacing), + Padding( + padding: MySpacing.x(flexSpacing), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Container( + decoration: BoxDecoration( + border: Border.all( + color: Colors.black, + width: 1.5, + ), + borderRadius: BorderRadius.circular(4), + ), + child: PopupMenuButton( + onSelected: (String value) async { + if (value.isEmpty) { + employeesScreenController.selectedProjectId = + null; + await employeesScreenController + .fetchAllEmployees(); + } else { + employeesScreenController.selectedProjectId = + value; + await employeesScreenController + .fetchEmployeesByProject(value); + } + employeesScreenController.update(); + }, + itemBuilder: (BuildContext context) { + List> items = [ + PopupMenuItem( + value: '', + child: MyText.bodySmall('All Employees', + fontWeight: 600), + ), + ]; + + items.addAll( + employeesScreenController.projects + .map>((project) { + return PopupMenuItem( + value: project.id, + child: MyText.bodySmall(project.name), + ); + }).toList(), + ); + + return items; + }, + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 12.0, vertical: 8.0), + child: MyText.bodySmall( + employeesScreenController.selectedProjectId == + null + ? 'All Employees' + : employeesScreenController.projects + .firstWhere((project) => + project.id == + employeesScreenController + .selectedProjectId) + .name, + fontWeight: 600, + ), + ), + ), + ), + const SizedBox(width: 10), + ElevatedButton( + onPressed: () { + Get.toNamed('/employees/addEmployee'); + }, + child: Text('Add New Employee'), + ), + ], + ), + ), + MySpacing.height(flexSpacing), + Padding( + padding: MySpacing.x(flexSpacing / 2), + child: MyFlex( + children: [ + MyFlexItem(sizes: 'lg-6 ', child: employeeListTab()), + ], + ), + ), + ], + ); + }, + ), + Obx(() { + return employeesScreenController.isLoading.value + ? Container( + color: Colors.black.withOpacity(0.05), + child: const Center( + child: LoadingComponent( + isLoading: true, + loadingText: 'Loading Employees...', + child: SizedBox.shrink(), + ), + ), + ) + : const SizedBox.shrink(); + }), + ], + ), + ); + } + + Widget employeeListTab() { + if (employeesScreenController.employees.isEmpty) { + return Center( + child: MyText.bodySmall("No Employees Assigned to This Project", + fontWeight: 600), + ); + } + + final columns = [ + DataColumn(label: MyText.labelLarge('Name', color: contentTheme.primary)), + DataColumn( + label: MyText.labelLarge('Contact', color: contentTheme.primary)), + DataColumn( + label: MyText.labelLarge('Actions', color: contentTheme.primary)), + ]; + + final rows = + employeesScreenController.employees.asMap().entries.map((entry) { + var employee = entry.value; + return DataRow( + cells: [ + DataCell( + Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + MyText.bodyMedium(employee.name, fontWeight: 600), + const SizedBox(height: 2), + MyText.bodySmall(employee.jobRole, color: Colors.grey), + ], + ), + ), + DataCell( + Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + MyText.bodyMedium(employee.email, fontWeight: 600), + const SizedBox(height: 2), + MyText.bodySmall(employee.phoneNumber, color: Colors.grey), + ], + ), + ), + DataCell( + Row( + children: [ + IconButton( + icon: const Icon(Icons.visibility), + tooltip: 'View', + onPressed: () { + // View employee action + }, + ), + IconButton( + icon: const Icon(Icons.edit), + tooltip: 'Edit', + onPressed: () { + // Edit employee action + }, + ), + ], + ), + ), + ], + ); + }).toList(); + + return MyRefreshableContent( + onRefresh: () async { + if (employeesScreenController.selectedProjectId == null) { + await employeesScreenController.fetchAllEmployees(); + } else { + await employeesScreenController.fetchEmployeesByProject( + employeesScreenController.selectedProjectId!, + ); + } + }, + child: MyPaginatedTable( + columns: columns, + rows: rows, + ), + ); + } +} diff --git a/lib/view/layouts/left_bar.dart b/lib/view/layouts/left_bar.dart index b98ed62..c2a2daa 100644 --- a/lib/view/layouts/left_bar.dart +++ b/lib/view/layouts/left_bar.dart @@ -119,6 +119,11 @@ class _LeftBarState extends State title: "Attendance", isCondensed: isCondensed, route: '/dashboard/attendance'), + NavigationItem( + iconData: LucideIcons.users, + title: "Employees", + isCondensed: isCondensed, + route: '/dashboard/employees'), ], ), ),