diff --git a/lib/controller/auth/mpin_controller.dart b/lib/controller/auth/mpin_controller.dart index cde5bc0..f045310 100644 --- a/lib/controller/auth/mpin_controller.dart +++ b/lib/controller/auth/mpin_controller.dart @@ -10,14 +10,17 @@ import 'package:marco/helpers/services/app_logger.dart'; class MPINController extends GetxController { final MyFormValidator basicValidator = MyFormValidator(); final isNewUser = false.obs; + final isChangeMpin = false.obs; final RxBool isLoading = false.obs; final formKey = GlobalKey(); - final digitControllers = List.generate(6, (_) => TextEditingController()); - final focusNodes = List.generate(6, (_) => FocusNode()); + // Updated to 4-digit MPIN + final digitControllers = List.generate(4, (_) => TextEditingController()); + final focusNodes = List.generate(4, (_) => FocusNode()); + + final retypeControllers = List.generate(4, (_) => TextEditingController()); + final retypeFocusNodes = List.generate(4, (_) => FocusNode()); - final retypeControllers = List.generate(6, (_) => TextEditingController()); - final retypeFocusNodes = List.generate(6, (_) => FocusNode()); final RxInt failedAttempts = 0.obs; @override @@ -28,16 +31,27 @@ class MPINController extends GetxController { logSafe("onInit called. isNewUser: ${isNewUser.value}"); } + /// Enable Change MPIN mode + void setChangeMpinMode() { + isChangeMpin.value = true; + isNewUser.value = false; + clearFields(); + clearRetypeFields(); + logSafe("setChangeMpinMode activated"); + } + + /// Handle digit entry and focus movement void onDigitChanged(String value, int index, {bool isRetype = false}) { - logSafe("onDigitChanged -> index: $index, value: $value, isRetype: $isRetype", ); + logSafe("onDigitChanged -> index: $index, value: $value, isRetype: $isRetype"); final nodes = isRetype ? retypeFocusNodes : focusNodes; - if (value.isNotEmpty && index < 5) { + if (value.isNotEmpty && index < 3) { nodes[index + 1].requestFocus(); } else if (value.isEmpty && index > 0) { nodes[index - 1].requestFocus(); } } + /// Submit MPIN for verification or generation Future onSubmitMPIN() async { logSafe("onSubmitMPIN triggered"); @@ -47,19 +61,19 @@ class MPINController extends GetxController { } final enteredMPIN = digitControllers.map((c) => c.text).join(); - logSafe("Entered MPIN: $enteredMPIN", ); + logSafe("Entered MPIN: $enteredMPIN"); - if (enteredMPIN.length < 6) { - _showError("Please enter all 6 digits."); + if (enteredMPIN.length < 4) { + _showError("Please enter all 4 digits."); return; } - if (isNewUser.value) { + if (isNewUser.value || isChangeMpin.value) { final retypeMPIN = retypeControllers.map((c) => c.text).join(); - logSafe("Retyped MPIN: $retypeMPIN", ); + logSafe("Retyped MPIN: $retypeMPIN"); - if (retypeMPIN.length < 6) { - _showError("Please enter all 6 digits in Retype MPIN."); + if (retypeMPIN.length < 4) { + _showError("Please enter all 4 digits in Retype MPIN."); return; } @@ -70,19 +84,20 @@ class MPINController extends GetxController { return; } - logSafe("MPINs matched. Proceeding to generate MPIN."); final bool success = await generateMPIN(mpin: enteredMPIN); if (success) { - logSafe("MPIN generation successful."); + logSafe("MPIN generation/change successful."); showAppSnackbar( title: "Success", - message: "MPIN generated successfully. Please login again.", + message: isChangeMpin.value + ? "MPIN changed successfully." + : "MPIN generated successfully. Please login again.", type: SnackbarType.success, ); await LocalStorage.logout(); } else { - logSafe("MPIN generation failed.", level: LogLevel.warning); + logSafe("MPIN generation/change failed.", level: LogLevel.warning); clearFields(); clearRetypeFields(); } @@ -92,20 +107,25 @@ class MPINController extends GetxController { } } + /// Forgot MPIN Future onForgotMPIN() async { logSafe("onForgotMPIN called"); isNewUser.value = true; + isChangeMpin.value = false; clearFields(); clearRetypeFields(); } + /// Switch to login/enter MPIN screen void switchToEnterMPIN() { logSafe("switchToEnterMPIN called"); isNewUser.value = false; + isChangeMpin.value = false; clearFields(); clearRetypeFields(); } + /// Show error snackbar void _showError(String message) { logSafe("ERROR: $message", level: LogLevel.error); showAppSnackbar( @@ -115,6 +135,7 @@ class MPINController extends GetxController { ); } + /// Navigate to dashboard void _navigateToDashboard({String? message}) { if (message != null) { logSafe("Navigating to Dashboard with message: $message"); @@ -127,6 +148,7 @@ class MPINController extends GetxController { Get.offAll(() => const DashboardScreen()); } + /// Clear the primary MPIN fields void clearFields() { logSafe("clearFields called"); for (final c in digitControllers) { @@ -135,6 +157,7 @@ class MPINController extends GetxController { focusNodes.first.requestFocus(); } + /// Clear the retype MPIN fields void clearRetypeFields() { logSafe("clearRetypeFields called"); for (final c in retypeControllers) { @@ -143,6 +166,7 @@ class MPINController extends GetxController { retypeFocusNodes.first.requestFocus(); } + /// Cleanup @override void onClose() { logSafe("onClose called"); @@ -161,9 +185,8 @@ class MPINController extends GetxController { super.onClose(); } - Future generateMPIN({ - required String mpin, - }) async { + /// Generate MPIN for new user/change MPIN + Future generateMPIN({required String mpin}) async { try { isLoading.value = true; logSafe("generateMPIN started"); @@ -177,7 +200,7 @@ class MPINController extends GetxController { return false; } - logSafe("Calling AuthService.generateMpin for employeeId: $employeeId", ); + logSafe("Calling AuthService.generateMpin for employeeId: $employeeId"); final response = await AuthService.generateMpin( employeeId: employeeId, @@ -187,21 +210,11 @@ class MPINController extends GetxController { isLoading.value = false; if (response == null) { - logSafe("MPIN generated successfully"); - - showAppSnackbar( - title: "Success", - message: "MPIN generated successfully. Please login again.", - type: SnackbarType.success, - ); - - await LocalStorage.logout(); - return true; } else { logSafe("MPIN generation returned error: $response", level: LogLevel.warning); showAppSnackbar( - title: "MPIN Generation Failed", + title: "MPIN Operation Failed", message: "Please check your inputs.", type: SnackbarType.error, ); @@ -213,19 +226,20 @@ class MPINController extends GetxController { } catch (e) { isLoading.value = false; logSafe("Exception in generateMPIN", level: LogLevel.error, error: e); - _showError("Failed to generate MPIN."); + _showError("Failed to process MPIN."); return false; } } + /// Verify MPIN for existing user Future verifyMPIN() async { logSafe("verifyMPIN triggered"); final enteredMPIN = digitControllers.map((c) => c.text).join(); - logSafe("Entered MPIN: $enteredMPIN", ); + logSafe("Entered MPIN: $enteredMPIN"); - if (enteredMPIN.length < 6) { - _showError("Please enter all 6 digits."); + if (enteredMPIN.length < 4) { + _showError("Please enter all 4 digits."); return; } @@ -278,6 +292,7 @@ class MPINController extends GetxController { } } + /// Increment failed attempts and warn void onInvalidMPIN() { failedAttempts.value++; if (failedAttempts.value >= 3) { diff --git a/lib/controller/dashboard/add_employee_controller.dart b/lib/controller/dashboard/add_employee_controller.dart index 6b0e5c0..f016ab3 100644 --- a/lib/controller/dashboard/add_employee_controller.dart +++ b/lib/controller/dashboard/add_employee_controller.dart @@ -110,7 +110,8 @@ class AddEmployeeController extends MyController { logSafe("Failed to fetch roles: null result", level: LogLevel.error); } } catch (e, st) { - logSafe("Error fetching roles", level: LogLevel.error, error: e, stackTrace: st); + logSafe("Error fetching roles", + level: LogLevel.error, error: e, stackTrace: st); } } @@ -120,7 +121,7 @@ class AddEmployeeController extends MyController { update(); } - Future createEmployees() async { + Future?> createEmployees() async { logSafe("Starting employee creation..."); if (selectedGender == null || selectedRoleId == null) { logSafe("Missing gender or role.", level: LogLevel.warning); @@ -129,14 +130,13 @@ class AddEmployeeController extends MyController { message: "Please select both Gender and Role.", type: SnackbarType.warning, ); - return false; + return null; } final firstName = basicValidator.getController("first_name")?.text.trim(); final lastName = basicValidator.getController("last_name")?.text.trim(); - final phoneNumber = basicValidator.getController("phone_number")?.text.trim(); - - logSafe("Creating employee", level: LogLevel.info); + final phoneNumber = + basicValidator.getController("phone_number")?.text.trim(); try { final response = await ApiService.createEmployee( @@ -146,20 +146,24 @@ class AddEmployeeController extends MyController { gender: selectedGender!.name, jobRoleId: selectedRoleId!, ); -logSafe("Response: $response"); - if (response == true) { + + logSafe("Response: $response"); + + if (response != null && response['success'] == true) { logSafe("Employee created successfully."); showAppSnackbar( title: "Success", message: "Employee created successfully!", type: SnackbarType.success, ); - return true; + return response; } else { - logSafe("Failed to create employee (response false)", level: LogLevel.error); + logSafe("Failed to create employee (response false)", + level: LogLevel.error); } } catch (e, st) { - logSafe("Error creating employee", level: LogLevel.error, error: e, stackTrace: st); + logSafe("Error creating employee", + level: LogLevel.error, error: e, stackTrace: st); } showAppSnackbar( @@ -167,7 +171,7 @@ logSafe("Response: $response"); message: "Failed to create employee.", type: SnackbarType.error, ); - return false; + return null; } Future _checkAndRequestContactsPermission() async { @@ -181,7 +185,8 @@ logSafe("Response: $response"); showAppSnackbar( title: "Permission Required", - message: "Please allow Contacts permission from settings to pick a contact.", + message: + "Please allow Contacts permission from settings to pick a contact.", type: SnackbarType.warning, ); return false; @@ -195,7 +200,8 @@ logSafe("Response: $response"); final picked = await FlutterContacts.openExternalPick(); if (picked == null) return; - final contact = await FlutterContacts.getContact(picked.id, withProperties: true); + final contact = + await FlutterContacts.getContact(picked.id, withProperties: true); if (contact == null) { showAppSnackbar( title: "Error", @@ -216,7 +222,8 @@ logSafe("Response: $response"); final indiaPhones = contact.phones.where((p) { final normalized = p.number.replaceAll(RegExp(r'[^0-9+]'), ''); - return normalized.startsWith('+91') || RegExp(r'^\d{10}$').hasMatch(normalized); + return normalized.startsWith('+91') || + RegExp(r'^\d{10}$').hasMatch(normalized); }).toList(); if (indiaPhones.isEmpty) { @@ -256,10 +263,12 @@ logSafe("Response: $response"); ? normalizedPhone.substring(normalizedPhone.length - 10) : normalizedPhone; - basicValidator.getController('phone_number')?.text = phoneWithoutCountryCode; + basicValidator.getController('phone_number')?.text = + phoneWithoutCountryCode; update(); } catch (e, st) { - logSafe("Error fetching contacts", level: LogLevel.error, error: e, stackTrace: st); + logSafe("Error fetching contacts", + level: LogLevel.error, error: e, stackTrace: st); showAppSnackbar( title: "Error", message: "Failed to fetch contacts.", diff --git a/lib/controller/dashboard/employees_screen_controller.dart b/lib/controller/dashboard/employees_screen_controller.dart index 273e080..dd3cd93 100644 --- a/lib/controller/dashboard/employees_screen_controller.dart +++ b/lib/controller/dashboard/employees_screen_controller.dart @@ -17,24 +17,25 @@ class EmployeesScreenController extends GetxController { RxBool isLoading = false.obs; RxMap uploadingStates = {}.obs; - Rxn selectedEmployeeDetails = Rxn(); + Rxn selectedEmployeeDetails = + Rxn(); RxBool isLoadingEmployeeDetails = false.obs; @override void onInit() { super.onInit(); - fetchAllProjects(); - - final projectId = Get.find().selectedProject?.id; - - if (projectId != null) { - selectedProjectId = projectId; - fetchEmployeesByProject(projectId); - } else if (isAllEmployeeSelected.value) { - fetchAllEmployees(); - } else { - clearEmployees(); - } + isLoading.value = true; + fetchAllProjects().then((_) { + final projectId = Get.find().selectedProject?.id; + if (projectId != null) { + selectedProjectId = projectId; + fetchEmployeesByProject(projectId); + } else if (isAllEmployeeSelected.value) { + fetchAllEmployees(); + } else { + clearEmployees(); + } + }); } Future fetchAllProjects() async { @@ -50,7 +51,8 @@ class EmployeesScreenController extends GetxController { ); }, onEmpty: () { - logSafe("No project data found or API call failed.", level: LogLevel.warning); + logSafe("No project data found or API call failed.", + level: LogLevel.warning); }, ); @@ -66,19 +68,19 @@ class EmployeesScreenController extends GetxController { Future fetchAllEmployees() async { isLoading.value = true; + update(['employee_screen_controller']); await _handleApiCall( ApiService.getAllEmployees, onSuccess: (data) { employees.assignAll(data.map((json) => EmployeeModel.fromJson(json))); - logSafe( - "All Employees fetched: ${employees.length} employees loaded.", - level: LogLevel.info, - ); + logSafe("All Employees fetched: ${employees.length} employees loaded.", + level: LogLevel.info); }, onEmpty: () { employees.clear(); - logSafe("No Employee data found or API call failed.", level: LogLevel.warning); + logSafe("No Employee data found or API call failed.", + level: LogLevel.warning); }, ); @@ -88,7 +90,8 @@ class EmployeesScreenController extends GetxController { Future fetchEmployeesByProject(String? projectId) async { if (projectId == null || projectId.isEmpty) { - logSafe("Project ID is required but was null or empty.", level: LogLevel.error); + logSafe("Project ID is required but was null or empty.", + level: LogLevel.error); return; } @@ -106,15 +109,21 @@ class EmployeesScreenController extends GetxController { logSafe( "Employees fetched: ${employees.length} for project $projectId", level: LogLevel.info, - ); }, onEmpty: () { employees.clear(); - logSafe("No employees found for project $projectId.", level: LogLevel.warning, ); + logSafe( + "No employees found for project $projectId.", + level: LogLevel.warning, + ); }, onError: (e) { - logSafe("Error fetching employees for project $projectId", level: LogLevel.error, error: e, ); + logSafe( + "Error fetching employees for project $projectId", + level: LogLevel.error, + error: e, + ); }, ); @@ -131,15 +140,25 @@ class EmployeesScreenController extends GetxController { () => ApiService.getEmployeeDetails(employeeId), onSuccess: (data) { selectedEmployeeDetails.value = EmployeeDetailsModel.fromJson(data); - logSafe("Employee details loaded for $employeeId", level: LogLevel.info, ); + logSafe( + "Employee details loaded for $employeeId", + level: LogLevel.info, + ); }, onEmpty: () { selectedEmployeeDetails.value = null; - logSafe("No employee details found for $employeeId", level: LogLevel.warning, ); + logSafe( + "No employee details found for $employeeId", + level: LogLevel.warning, + ); }, onError: (e) { selectedEmployeeDetails.value = null; - logSafe("Error fetching employee details for $employeeId", level: LogLevel.error, error: e, ); + logSafe( + "Error fetching employee details for $employeeId", + level: LogLevel.error, + error: e, + ); }, ); diff --git a/lib/controller/employee/assign_projects_controller.dart b/lib/controller/employee/assign_projects_controller.dart new file mode 100644 index 0000000..37581ae --- /dev/null +++ b/lib/controller/employee/assign_projects_controller.dart @@ -0,0 +1,145 @@ +import 'package:flutter/widgets.dart'; +import 'package:get/get.dart'; +import 'package:marco/helpers/services/api_service.dart'; +import 'package:marco/helpers/services/app_logger.dart'; +import 'package:marco/model/global_project_model.dart'; +import 'package:marco/model/employees/assigned_projects_model.dart'; +import 'package:marco/controller/project_controller.dart'; + +class AssignProjectController extends GetxController { + final String employeeId; + final String jobRoleId; + + AssignProjectController({ + required this.employeeId, + required this.jobRoleId, + }); + + final ProjectController projectController = Get.put(ProjectController()); + + RxBool isLoading = false.obs; + RxBool isAssigning = false.obs; + + RxList assignedProjectIds = [].obs; + RxList selectedProjects = [].obs; + RxList allProjects = [].obs; + RxList filteredProjects = [].obs; + + @override + void onInit() { + super.onInit(); + WidgetsBinding.instance.addPostFrameCallback((_) { + fetchAllProjectsAndAssignments(); + }); + } + + /// Fetch all projects and assigned projects + Future fetchAllProjectsAndAssignments() async { + isLoading.value = true; + try { + await projectController.fetchProjects(); + allProjects.assignAll(projectController.projects); + filteredProjects.assignAll(allProjects); // initially show all + + final responseList = await ApiService.getAssignedProjects(employeeId); + if (responseList != null) { + final assignedProjects = + responseList.map((e) => AssignedProject.fromJson(e)).toList(); + + assignedProjectIds.assignAll( + assignedProjects.map((p) => p.id).toList(), + ); + selectedProjects.assignAll(assignedProjectIds); + } + + logSafe("All Projects: ${allProjects.map((e) => e.id)}"); + logSafe("Assigned Project IDs: $assignedProjectIds"); + } catch (e, stack) { + logSafe("Error fetching projects or assignments: $e", + level: LogLevel.error); + logSafe("StackTrace: $stack", level: LogLevel.debug); + } finally { + isLoading.value = false; + } + } + + /// Assign selected projects + Future assignProjectsToEmployee() async { + if (selectedProjects.isEmpty) { + logSafe("No projects selected for assignment.", level: LogLevel.warning); + return false; + } + + final List> projectPayload = + selectedProjects.map((id) { + return {"projectId": id, "jobRoleId": jobRoleId, "status": true}; + }).toList(); + + isAssigning.value = true; + try { + final success = await ApiService.assignProjects( + employeeId: employeeId, + projects: projectPayload, + ); + + if (success) { + logSafe("Projects assigned successfully."); + assignedProjectIds.assignAll(selectedProjects); + return true; + } else { + logSafe("Failed to assign projects.", level: LogLevel.error); + return false; + } + } catch (e, stack) { + logSafe("Error assigning projects: $e", level: LogLevel.error); + logSafe("StackTrace: $stack", level: LogLevel.debug); + return false; + } finally { + isAssigning.value = false; + } + } + + /// Toggle project selection + void toggleProjectSelection(String projectId, bool isSelected) { + if (isSelected) { + if (!selectedProjects.contains(projectId)) { + selectedProjects.add(projectId); + } + } else { + selectedProjects.remove(projectId); + } + } + + /// Check if project is selected + bool isProjectSelected(String projectId) { + return selectedProjects.contains(projectId); + } + + /// Select all / deselect all + void toggleSelectAll() { + if (areAllSelected()) { + selectedProjects.clear(); + } else { + selectedProjects.assignAll(allProjects.map((p) => p.id.toString())); + } + } + + /// Are all selected? + bool areAllSelected() { + return selectedProjects.length == allProjects.length && + allProjects.isNotEmpty; + } + + /// Filter projects by search text + void filterProjects(String query) { + if (query.isEmpty) { + filteredProjects.assignAll(allProjects); + } else { + filteredProjects.assignAll( + allProjects + .where((p) => p.name.toLowerCase().contains(query.toLowerCase())) + .toList(), + ); + } + } +} diff --git a/lib/helpers/services/api_endpoints.dart b/lib/helpers/services/api_endpoints.dart index 46548d4..1e386b2 100644 --- a/lib/helpers/services/api_endpoints.dart +++ b/lib/helpers/services/api_endpoints.dart @@ -14,12 +14,15 @@ class ApiEndpoints { static const String getRegularizationLogs = "/attendance/regularize"; static const String uploadAttendanceImage = "/attendance/record-image"; - // Employee Module API Endpoints - static const String getAllEmployeesByProject = "/Project/employees/get"; + // Employee Screen API Endpoints + static const String getAllEmployeesByProject = "/employee/list"; static const String getAllEmployees = "/employee/list"; static const String getRoles = "/roles/jobrole"; - static const String createEmployee = "/employee/manage"; + static const String createEmployee = "/employee/manage-mobile"; static const String getEmployeeInfo = "/employee/profile/get"; + static const String assignEmployee = "/employee/profile/get"; + static const String getAssignedProjects = "/project/assigned-projects"; + static const String assignProjects = "/project/assign-projects"; // Daily Task Module API Endpoints static const String getDailyTask = "/task/list"; diff --git a/lib/helpers/services/api_service.dart b/lib/helpers/services/api_service.dart index cb2a84b..cf3c28d 100644 --- a/lib/helpers/services/api_service.dart +++ b/lib/helpers/services/api_service.dart @@ -663,6 +663,83 @@ class ApiService { return false; } + /// Get list of assigned projects for a specific employee + /// Get list of assigned projects for a specific employee + static Future?> getAssignedProjects(String employeeId) async { + if (employeeId.isEmpty) { + throw ArgumentError("employeeId must not be empty"); + } + + final endpoint = "${ApiEndpoints.getAssignedProjects}/$employeeId"; + + try { + final response = await _getRequest(endpoint); + + if (response == null) { + logSafe("Failed to fetch assigned projects: null response", + level: LogLevel.error); + return null; + } + + final parsed = _parseResponse(response, label: "Assigned Projects"); + if (parsed is List) { + return parsed; + } else { + logSafe("Unexpected response format for assigned projects.", + level: LogLevel.error); + return null; + } + } catch (e, stack) { + logSafe("Exception during getAssignedProjects API: $e", + level: LogLevel.error); + logSafe("StackTrace: $stack", level: LogLevel.debug); + return null; + } + } + + /// Assign projects to a specific employee + static Future assignProjects({ + required String employeeId, + required List> projects, + }) async { + if (employeeId.isEmpty) { + throw ArgumentError("employeeId must not be empty"); + } + if (projects.isEmpty) { + throw ArgumentError("projects list must not be empty"); + } + + final endpoint = "${ApiEndpoints.assignProjects}/$employeeId"; + + logSafe("Assigning projects to employee $employeeId: $projects"); + + try { + final response = await _postRequest(endpoint, projects); + + if (response == null) { + logSafe("Assign projects failed: null response", level: LogLevel.error); + return false; + } + + logSafe("Assign projects response status: ${response.statusCode}"); + logSafe("Assign projects response body: ${response.body}"); + + final json = jsonDecode(response.body); + if (json['success'] == true) { + logSafe("Projects assigned successfully"); + return true; + } else { + logSafe("Failed to assign projects: ${json['message']}", + level: LogLevel.warning); + } + } catch (e, stack) { + logSafe("Exception during assignProjects API: $e", level: LogLevel.error); + logSafe("StackTrace: $stack", level: LogLevel.debug); + } + + return false; + } + static Future updateContactComment( String commentId, String note, String contactId) async { final payload = { @@ -948,8 +1025,7 @@ class ApiService { static Future?> getRoles() async => _getRequest(ApiEndpoints.getRoles).then( (res) => res != null ? _parseResponse(res, label: 'Roles') : null); - - static Future createEmployee({ + static Future?> createEmployee({ required String firstName, required String lastName, required String phoneNumber, @@ -963,15 +1039,20 @@ class ApiService { "gender": gender, "jobRoleId": jobRoleId, }; + final response = await _postRequest( ApiEndpoints.createEmployee, body, customTimeout: extendedTimeout, ); - if (response == null) return false; + if (response == null) return null; + final json = jsonDecode(response.body); - return response.statusCode == 200 && json['success'] == true; + return { + "success": response.statusCode == 200 && json['success'] == true, + "data": json + }; } static Future?> getEmployeeDetails( diff --git a/lib/helpers/widgets/my_custom_skeleton.dart b/lib/helpers/widgets/my_custom_skeleton.dart index e8b0090..0370bff 100644 --- a/lib/helpers/widgets/my_custom_skeleton.dart +++ b/lib/helpers/widgets/my_custom_skeleton.dart @@ -226,6 +226,81 @@ static Widget buildLoadingSkeleton() { }), ); } + static Widget employeeSkeletonCard() { + return MyCard.bordered( + margin: MySpacing.only(bottom: 12), + paddingAll: 12, + borderRadiusAll: 12, + shadow: MyShadow( + elevation: 1.5, + position: MyShadowPosition.bottom, + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Avatar + Container( + height: 35, + width: 35, + decoration: BoxDecoration( + color: Colors.grey.shade300, + shape: BoxShape.circle, + ), + ), + MySpacing.width(12), + + // Name, org, email, phone + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container(height: 12, width: 120, color: Colors.grey.shade300), + MySpacing.height(6), + Container(height: 10, width: 80, color: Colors.grey.shade300), + MySpacing.height(8), + + // Email placeholder + Row( + children: [ + Icon(Icons.email_outlined, size: 14, color: Colors.grey.shade300), + MySpacing.width(4), + Container(height: 10, width: 140, color: Colors.grey.shade300), + ], + ), + MySpacing.height(8), + + // Phone placeholder + Row( + children: [ + Icon(Icons.phone_outlined, size: 14, color: Colors.grey.shade300), + MySpacing.width(4), + Container(height: 10, width: 100, color: Colors.grey.shade300), + MySpacing.width(8), + Container( + height: 16, + width: 16, + decoration: BoxDecoration( + color: Colors.grey.shade300, + shape: BoxShape.circle, + ), + ), + ], + ), + MySpacing.height(8), + + // Tags placeholder + Container(height: 8, width: 80, color: Colors.grey.shade300), + ], + ), + ), + + // Arrow + Icon(Icons.arrow_forward_ios, size: 14, color: Colors.grey.shade300), + ], + ), + ); +} + static Widget contactSkeletonCard() { return MyCard.bordered( margin: MySpacing.only(bottom: 12), diff --git a/lib/model/employees/add_employee_bottom_sheet.dart b/lib/model/employees/add_employee_bottom_sheet.dart index 52d0cf5..f111c4f 100644 --- a/lib/model/employees/add_employee_bottom_sheet.dart +++ b/lib/model/employees/add_employee_bottom_sheet.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; +import 'package:flutter/services.dart'; import 'package:marco/controller/dashboard/add_employee_controller.dart'; import 'package:marco/controller/dashboard/employees_screen_controller.dart'; @@ -274,6 +275,12 @@ class _AddEmployeeBottomSheetState extends State return null; }, keyboardType: TextInputType.phone, + inputFormatters: [ + // Allow only digits + FilteringTextInputFormatter.digitsOnly, + // Limit to 10 digits + LengthLimitingTextInputFormatter(10), + ], decoration: _inputDecoration("e.g., 9876543210") .copyWith( suffixIcon: IconButton( @@ -355,9 +362,12 @@ class _AddEmployeeBottomSheetState extends State onPressed: () async { if (_controller.basicValidator .validateForm()) { - final success = + final result = await _controller.createEmployees(); - if (success) { + + if (result != null && + result['success'] == true) { + final employeeData = result['data']; final employeeController = Get.find(); final projectId = @@ -387,7 +397,7 @@ class _AddEmployeeBottomSheetState extends State _controller.selectedRoleId = null; _controller.update(); - Navigator.pop(context); + Navigator.pop(context, employeeData); } } }, diff --git a/lib/model/employees/assigned_projects_model.dart b/lib/model/employees/assigned_projects_model.dart new file mode 100644 index 0000000..c9add8f --- /dev/null +++ b/lib/model/employees/assigned_projects_model.dart @@ -0,0 +1,93 @@ +class AssignedProjectsResponse { + final bool success; + final String message; + final List data; + final dynamic errors; + final int statusCode; + final DateTime timestamp; + + AssignedProjectsResponse({ + required this.success, + required this.message, + required this.data, + this.errors, + required this.statusCode, + required this.timestamp, + }); + + factory AssignedProjectsResponse.fromJson(Map json) { + return AssignedProjectsResponse( + success: json['success'] ?? false, + message: json['message'] ?? '', + data: (json['data'] as List?) + ?.map((item) => AssignedProject.fromJson(item)) + .toList() ?? + [], + errors: json['errors'], + statusCode: json['statusCode'] ?? 0, + timestamp: DateTime.tryParse(json['timestamp'] ?? '') ?? DateTime.now(), + ); + } + + Map toJson() { + return { + 'success': success, + 'message': message, + 'data': data.map((p) => p.toJson()).toList(), + 'errors': errors, + 'statusCode': statusCode, + 'timestamp': timestamp.toIso8601String(), + }; + } +} + +class AssignedProject { + final String id; + final String name; + final String shortName; + final String projectAddress; + final String contactPerson; + final DateTime? startDate; + final DateTime? endDate; + final String projectStatusId; + + AssignedProject({ + required this.id, + required this.name, + required this.shortName, + required this.projectAddress, + required this.contactPerson, + this.startDate, + this.endDate, + required this.projectStatusId, + }); + + factory AssignedProject.fromJson(Map json) { + return AssignedProject( + id: json['id'] ?? '', + name: json['name'] ?? '', + shortName: json['shortName'] ?? '', + projectAddress: json['projectAddress'] ?? '', + contactPerson: json['contactPerson'] ?? '', + startDate: json['startDate'] != null + ? DateTime.tryParse(json['startDate']) + : null, + endDate: + json['endDate'] != null ? DateTime.tryParse(json['endDate']) : null, + projectStatusId: json['projectStatusId'] ?? '', + ); + } + + Map toJson() { + return { + 'id': id, + 'name': name, + 'shortName': shortName, + 'projectAddress': projectAddress, + 'contactPerson': contactPerson, + 'startDate': startDate?.toIso8601String(), + 'endDate': endDate?.toIso8601String(), + 'projectStatusId': projectStatusId, + }; + } +} diff --git a/lib/model/employees/employee_detail_bottom_sheet.dart b/lib/model/employees/employee_detail_bottom_sheet.dart index 3990ae4..3a0ef87 100644 --- a/lib/model/employees/employee_detail_bottom_sheet.dart +++ b/lib/model/employees/employee_detail_bottom_sheet.dart @@ -5,6 +5,7 @@ import 'package:marco/helpers/widgets/my_text.dart'; import 'package:marco/helpers/widgets/avatar.dart'; import 'package:intl/intl.dart'; import 'package:marco/controller/dashboard/employees_screen_controller.dart'; +import 'package:marco/view/employees/assign_employee_bottom_sheet.dart'; class EmployeeDetailBottomSheet extends StatefulWidget { final String employeeId; @@ -113,39 +114,80 @@ class _EmployeeDetailBottomSheetState extends State { ), ), MySpacing.height(20), - CircleAvatar( - radius: 40, - backgroundColor: Colors.blueGrey[200], - child: Avatar( - firstName: employee.firstName, - lastName: employee.lastName, - size: 60, - ), - ), - MySpacing.height(12), - MyText.titleLarge( - '${employee.firstName} ${employee.lastName}', - fontWeight: 700, - textAlign: TextAlign.center, - ), - if (employee.jobRole.trim().isNotEmpty && - employee.jobRole != 'null') - Padding( - padding: const EdgeInsets.only(top: 6), - child: Chip( - label: Text( - employee.jobRole, - style: const TextStyle( - color: Colors.white, - fontWeight: FontWeight.w600, - ), + + // Row 1: Avatar + Name + Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + CircleAvatar( + radius: 40, + backgroundColor: Colors.blueGrey[200], + child: Avatar( + firstName: employee.firstName, + lastName: employee.lastName, + size: 60, ), - backgroundColor: Colors.blueAccent, + ), + MySpacing.width(16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + MyText.titleLarge( + '${employee.firstName} ${employee.lastName}', + fontWeight: 700, + ), + MySpacing.height(6), + MyText.bodyMedium( + _getDisplayValue(employee.jobRole), + fontWeight: 500, + color: Colors.grey[700], + ), + ], + ), + ), + ], + ), + + MySpacing.height(12), + + // Row 2: Minimal Button + Align( + alignment: Alignment.centerRight, + child: TextButton( + style: TextButton.styleFrom( padding: const EdgeInsets.symmetric( - horizontal: 12, vertical: 6), + horizontal: 10, vertical: 6), + backgroundColor: Colors.blueAccent, + minimumSize: const Size(0, 32), + tapTargetSize: MaterialTapTargetSize.shrinkWrap, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(6), + ), + ), + onPressed: () { + showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.transparent, + builder: (context) => AssignProjectBottomSheet( + employeeId: widget.employeeId, + jobRoleId: employee.jobRoleId, + ), + ); + }, + child: const Text( + 'Assign to Project', + style: TextStyle( + color: Colors.white, + fontSize: 13, + fontWeight: FontWeight.w500, + ), ), ), - MySpacing.height(10), + ), + + MySpacing.height(20), // Contact Info Card _buildInfoCard('Contact Information', [ diff --git a/lib/view/auth/mpin_auth_screen.dart b/lib/view/auth/mpin_auth_screen.dart index c53164d..d1d332c 100644 --- a/lib/view/auth/mpin_auth_screen.dart +++ b/lib/view/auth/mpin_auth_screen.dart @@ -139,6 +139,7 @@ class _MPINAuthScreenState extends State Widget _buildMPINCard(MPINController controller) { return Obx(() { final isNewUser = controller.isNewUser.value; + final isChangeMpin = controller.isChangeMpin.value; return Container( padding: const EdgeInsets.all(24), @@ -156,7 +157,9 @@ class _MPINAuthScreenState extends State child: Column( children: [ MyText( - isNewUser ? 'Generate MPIN' : 'Enter MPIN', + isChangeMpin + ? 'Change MPIN' + : (isNewUser ? 'Generate MPIN' : 'Enter MPIN'), fontSize: 20, fontWeight: 700, color: Colors.black87, @@ -164,17 +167,19 @@ class _MPINAuthScreenState extends State ), const SizedBox(height: 10), MyText( - isNewUser - ? 'Set your 6-digit MPIN for quick login.' - : 'Enter your 6-digit MPIN to continue.', + isChangeMpin + ? 'Set a new 6-digit MPIN for your account.' + : (isNewUser + ? 'Set your 6-digit MPIN for quick login.' + : 'Enter your 6-digit MPIN to continue.'), fontSize: 14, color: Colors.black54, textAlign: TextAlign.center, ), const SizedBox(height: 30), - _buildMPINForm(controller, isNewUser), + _buildMPINForm(controller, isNewUser || isChangeMpin), const SizedBox(height: 32), - _buildSubmitButton(controller, isNewUser), + _buildSubmitButton(controller, isNewUser, isChangeMpin), const SizedBox(height: 20), _buildFooterOptions(controller, isNewUser), ], @@ -183,13 +188,13 @@ class _MPINAuthScreenState extends State }); } - Widget _buildMPINForm(MPINController controller, bool isNewUser) { + Widget _buildMPINForm(MPINController controller, bool showRetype) { return Form( key: controller.formKey, child: Column( children: [ _buildDigitRow(controller, isRetype: false), - if (isNewUser) ...[ + if (showRetype) ...[ const SizedBox(height: 20), MyText( 'Retype MPIN', @@ -206,11 +211,9 @@ class _MPINAuthScreenState extends State } Widget _buildDigitRow(MPINController controller, {required bool isRetype}) { - return Wrap( - alignment: WrapAlignment.center, - spacing: 0, - runSpacing: 12, - children: List.generate(6, (index) { + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: List.generate(4, (index) { return _buildDigitBox(controller, index, isRetype); }), ); @@ -225,29 +228,33 @@ class _MPINAuthScreenState extends State : controller.focusNodes[index]; return Container( - margin: const EdgeInsets.symmetric(horizontal: 6), - width: 30, - height: 55, + margin: const EdgeInsets.symmetric(horizontal: 8), + width: 48, + height: 60, child: TextFormField( controller: textController, focusNode: focusNode, - obscureText: true, + obscureText: false, // Digits are visible maxLength: 1, textAlign: TextAlign.center, style: const TextStyle( fontSize: 24, fontWeight: FontWeight.bold, - letterSpacing: 8, ), keyboardType: TextInputType.number, inputFormatters: [FilteringTextInputFormatter.digitsOnly], onChanged: (value) { controller.onDigitChanged(value, index, isRetype: isRetype); + // Auto-submit only in verification mode if (!isRetype) { final isComplete = controller.digitControllers.every((c) => c.text.isNotEmpty); - if (isComplete && !controller.isLoading.value) { + + if (isComplete && + !controller.isLoading.value && + !controller.isNewUser.value && + !controller.isChangeMpin.value) { controller.onSubmitMPIN(); } } @@ -265,7 +272,8 @@ class _MPINAuthScreenState extends State ); } - Widget _buildSubmitButton(MPINController controller, bool isNewUser) { + Widget _buildSubmitButton( + MPINController controller, bool isNewUser, bool isChangeMpin) { return Obx(() { return MyButton.rounded( onPressed: controller.isLoading.value ? null : controller.onSubmitMPIN, @@ -285,7 +293,9 @@ class _MPINAuthScreenState extends State ), ) : MyText.bodyMedium( - isNewUser ? 'Generate MPIN' : 'Submit MPIN', + isChangeMpin + ? 'Change MPIN' + : (isNewUser ? 'Generate MPIN' : 'Submit MPIN'), color: Colors.white, fontWeight: 700, fontSize: 16, @@ -296,12 +306,13 @@ class _MPINAuthScreenState extends State Widget _buildFooterOptions(MPINController controller, bool isNewUser) { return Obx(() { + final isChangeMpin = controller.isChangeMpin.value; final showBackToLogin = - controller.failedAttempts.value >= 3 && !isNewUser; + controller.failedAttempts.value >= 3 && !isNewUser && !isChangeMpin; return Column( children: [ - if (isNewUser) + if (isNewUser || isChangeMpin) TextButton.icon( onPressed: () => Get.toNamed('/dashboard'), icon: const Icon(Icons.arrow_back, @@ -359,8 +370,8 @@ class _WavePainter extends CustomPainter { final path1 = Path() ..moveTo(0, size.height * 0.2) - ..quadraticBezierTo( - size.width * 0.25, size.height * 0.05, size.width * 0.5, size.height * 0.15) + ..quadraticBezierTo(size.width * 0.25, size.height * 0.05, + size.width * 0.5, size.height * 0.15) ..quadraticBezierTo( size.width * 0.75, size.height * 0.25, size.width, size.height * 0.1) ..lineTo(size.width, 0) diff --git a/lib/view/dashboard/dashboard_screen.dart b/lib/view/dashboard/dashboard_screen.dart index 93d65b3..81e4e72 100644 --- a/lib/view/dashboard/dashboard_screen.dart +++ b/lib/view/dashboard/dashboard_screen.dart @@ -6,14 +6,14 @@ import 'package:marco/controller/project_controller.dart'; import 'package:marco/helpers/services/storage/local_storage.dart'; import 'package:marco/helpers/utils/mixins/ui_mixin.dart'; import 'package:marco/helpers/utils/my_shadow.dart'; -import 'package:marco/helpers/widgets/my_button.dart'; +// import 'package:marco/helpers/widgets/my_button.dart'; import 'package:marco/helpers/widgets/my_card.dart'; import 'package:marco/helpers/widgets/my_container.dart'; import 'package:marco/helpers/widgets/my_spacing.dart'; import 'package:marco/helpers/widgets/my_text.dart'; import 'package:marco/view/dashboard/dashboard_chart.dart'; import 'package:marco/view/layouts/layout.dart'; - + class DashboardScreen extends StatefulWidget { const DashboardScreen({super.key}); @@ -75,71 +75,70 @@ class _DashboardScreenState extends State with UIMixin { crossAxisAlignment: CrossAxisAlignment.start, children: [ AttendanceDashboardChart(), - MySpacing.height(300), ], ), ), ); }, ), - if (!hasMpin) ...[ - MyCard( - borderRadiusAll: 12, - paddingAll: 16, - shadow: MyShadow(elevation: 2), - color: Colors.red.withOpacity(0.05), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Icon(Icons.warning_amber_rounded, - color: Colors.redAccent, size: 28), - MySpacing.width(12), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - MyText.bodyMedium( - "MPIN Not Generated", - color: Colors.redAccent, - fontWeight: 700, - ), - MySpacing.height(4), - MyText.bodySmall( - "To secure your account, please generate your MPIN now.", - color: contentTheme.onBackground.withOpacity(0.8), - ), - MySpacing.height(10), - Align( - alignment: Alignment.center, - child: MyButton.rounded( - onPressed: () { - Get.toNamed("/auth/mpin-auth"); - }, - backgroundColor: contentTheme.brandRed, - padding: const EdgeInsets.symmetric( - horizontal: 20, vertical: 10), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - const Icon(Icons.lock_outline, - size: 18, color: Colors.white), - MySpacing.width(8), - MyText.bodyMedium( - "Generate MPIN", - color: Colors.white, - fontWeight: 600, - ), - ], - ), - ), - ), - ], - ), - ), - ], - ), - ), - ], + // if (!hasMpin) ...[ + // MyCard( + // borderRadiusAll: 12, + // paddingAll: 16, + // shadow: MyShadow(elevation: 2), + // color: Colors.red.withOpacity(0.05), + // child: Row( + // crossAxisAlignment: CrossAxisAlignment.start, + // children: [ + // const Icon(Icons.warning_amber_rounded, + // color: Colors.redAccent, size: 28), + // MySpacing.width(12), + // Expanded( + // child: Column( + // crossAxisAlignment: CrossAxisAlignment.start, + // children: [ + // MyText.bodyMedium( + // "MPIN Not Generated", + // color: Colors.redAccent, + // fontWeight: 700, + // ), + // MySpacing.height(4), + // MyText.bodySmall( + // "To secure your account, please generate your MPIN now.", + // color: contentTheme.onBackground.withOpacity(0.8), + // ), + // MySpacing.height(10), + // Align( + // alignment: Alignment.center, + // child: MyButton.rounded( + // onPressed: () { + // Get.toNamed("/auth/mpin-auth"); + // }, + // backgroundColor: contentTheme.brandRed, + // padding: const EdgeInsets.symmetric( + // horizontal: 20, vertical: 10), + // child: Row( + // mainAxisSize: MainAxisSize.min, + // children: [ + // const Icon(Icons.lock_outline, + // size: 18, color: Colors.white), + // MySpacing.width(8), + // MyText.bodyMedium( + // "Generate MPIN", + // color: Colors.white, + // fontWeight: 600, + // ), + // ], + // ), + // ), + // ), + // ], + // ), + // ), + // ], + // ), + // ), + // ], ], ), ), diff --git a/lib/view/employees/assign_employee_bottom_sheet.dart b/lib/view/employees/assign_employee_bottom_sheet.dart new file mode 100644 index 0000000..a689024 --- /dev/null +++ b/lib/view/employees/assign_employee_bottom_sheet.dart @@ -0,0 +1,280 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:marco/helpers/widgets/my_spacing.dart'; +import 'package:marco/helpers/widgets/my_text.dart'; +import 'package:marco/controller/employee/assign_projects_controller.dart'; +import 'package:marco/model/global_project_model.dart'; +import 'package:marco/helpers/widgets/my_snackbar.dart'; + +class AssignProjectBottomSheet extends StatefulWidget { + final String employeeId; + final String jobRoleId; + + const AssignProjectBottomSheet({ + super.key, + required this.employeeId, + required this.jobRoleId, + }); + + @override + State createState() => + _AssignProjectBottomSheetState(); +} + +class _AssignProjectBottomSheetState extends State { + late final AssignProjectController assignController; + + @override + void initState() { + super.initState(); + assignController = Get.put( + AssignProjectController( + employeeId: widget.employeeId, + jobRoleId: widget.jobRoleId, + ), + tag: '${widget.employeeId}_${widget.jobRoleId}', + ); + } + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + return SafeArea( + top: false, + child: DraggableScrollableSheet( + expand: false, + maxChildSize: 0.9, + minChildSize: 0.4, + initialChildSize: 0.7, + builder: (_, scrollController) { + return Container( + decoration: BoxDecoration( + color: theme.cardColor, + borderRadius: + const BorderRadius.vertical(top: Radius.circular(24)), + boxShadow: const [ + BoxShadow( + color: Colors.black12, + blurRadius: 12, + offset: Offset(0, -2), + ), + ], + ), + padding: MySpacing.all(16), + child: Obx(() { + if (assignController.isLoading.value) { + return const Center(child: CircularProgressIndicator()); + } + + final projects = assignController.allProjects; + if (projects.isEmpty) { + return const Center(child: Text('No projects available.')); + } + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Drag Handle + Center( + child: Container( + width: 40, + height: 5, + decoration: BoxDecoration( + color: Colors.grey.shade300, + borderRadius: BorderRadius.circular(10), + ), + ), + ), + MySpacing.height(12), + + // Header + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + MyText.titleMedium('Assign to Project', fontWeight: 700), + ], + ), + MySpacing.height(4), + + // Sub Info + MyText.bodySmall( + 'Select the projects to assign this employee.', + color: Colors.grey[600], + ), + MySpacing.height(8), + + // Select All Toggle + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'Projects (${projects.length})', + style: const TextStyle( + fontWeight: FontWeight.w600, + fontSize: 16, + ), + ), + TextButton( + onPressed: () { + assignController.toggleSelectAll(); + }, + child: Obx(() { + return Text( + assignController.areAllSelected() + ? 'Deselect All' + : 'Select All', + style: const TextStyle( + color: Colors.blueAccent, + fontWeight: FontWeight.w600, + ), + ); + }), + ), + ], + ), + + // Project List + Expanded( + child: ListView.builder( + controller: scrollController, + itemCount: projects.length, + padding: EdgeInsets.zero, + itemBuilder: (context, index) { + final GlobalProjectModel project = projects[index]; + return Obx(() { + final bool isSelected = + assignController.isProjectSelected( + project.id.toString(), + ); + return Theme( + data: Theme.of(context).copyWith( + checkboxTheme: CheckboxThemeData( + fillColor: + WidgetStateProperty.resolveWith( + (states) { + if (states.contains(WidgetState.selected)) { + return Colors.blueAccent; + } + return Colors.white; + }, + ), + side: const BorderSide( + color: Colors.black, + width: 2, + ), + checkColor: + WidgetStateProperty.all(Colors.white), + ), + ), + child: CheckboxListTile( + dense: true, + value: isSelected, + title: Text( + project.name, + style: const TextStyle( + fontWeight: FontWeight.w500, + fontSize: 14, + ), + ), + onChanged: (checked) { + assignController.toggleProjectSelection( + project.id.toString(), + checked ?? false, + ); + }, + activeColor: Colors.blueAccent, + controlAffinity: ListTileControlAffinity.leading, + contentPadding: EdgeInsets.zero, + visualDensity: const VisualDensity( + horizontal: -4, + vertical: -4, + ), + ), + ); + }); + }, + ), + ), + MySpacing.height(16), + + // Cancel & Save Buttons + Row( + children: [ + Expanded( + child: OutlinedButton.icon( + onPressed: () => Navigator.pop(context), + icon: const Icon(Icons.close, color: Colors.red), + label: MyText.bodyMedium("Cancel", + color: Colors.red, fontWeight: 600), + style: OutlinedButton.styleFrom( + side: const BorderSide(color: Colors.red), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + padding: const EdgeInsets.symmetric( + horizontal: 10, + vertical: 7, + ), + ), + ), + ), + const SizedBox(width: 12), + Expanded( + child: ElevatedButton.icon( + onPressed: () async { + if (assignController.selectedProjects.isEmpty) { + showAppSnackbar( + title: "Error", + message: "Please select at least one project.", + type: SnackbarType.error, + ); + return; + } + await _assignProjects(); + }, + icon: const Icon(Icons.check_circle_outline, + color: Colors.white), + label: MyText.bodyMedium("Assign", + color: Colors.white, fontWeight: 600), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.indigo, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + padding: const EdgeInsets.symmetric( + horizontal: 10, + vertical: 7, + ), + ), + ), + ), + ], + ), + ], + ); + }), + ); + }, + ), + ); + } + + Future _assignProjects() async { + final success = await assignController.assignProjectsToEmployee(); + if (success) { + Get.back(); + showAppSnackbar( + title: "Success", + message: "Employee assigned to selected projects.", + type: SnackbarType.success, + ); + } else { + showAppSnackbar( + title: "Error", + message: "Failed to assign employee.", + type: SnackbarType.error, + ); + } + } +} diff --git a/lib/view/employees/employee_detail_screen.dart b/lib/view/employees/employee_detail_screen.dart new file mode 100644 index 0000000..29558af --- /dev/null +++ b/lib/view/employees/employee_detail_screen.dart @@ -0,0 +1,321 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:intl/intl.dart'; +import 'package:marco/controller/dashboard/employees_screen_controller.dart'; +import 'package:marco/controller/project_controller.dart'; +import 'package:marco/helpers/widgets/avatar.dart'; +import 'package:marco/helpers/widgets/my_spacing.dart'; +import 'package:marco/helpers/widgets/my_text.dart'; +import 'package:marco/view/employees/assign_employee_bottom_sheet.dart'; +import 'package:marco/helpers/utils/launcher_utils.dart'; + +class EmployeeDetailPage extends StatefulWidget { + final String employeeId; + + const EmployeeDetailPage({super.key, required this.employeeId}); + + @override + State createState() => _EmployeeDetailPageState(); +} + +class _EmployeeDetailPageState extends State { + final EmployeesScreenController controller = + Get.put(EmployeesScreenController()); + + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addPostFrameCallback((_) { + controller.fetchEmployeeDetails(widget.employeeId); + }); + } + + @override + void dispose() { + controller.selectedEmployeeDetails.value = null; + super.dispose(); + } + + String _getDisplayValue(dynamic value) { + if (value == null || value.toString().trim().isEmpty || value == 'null') { + return 'NA'; + } + return value.toString(); + } + + String _formatDate(DateTime? date) { + if (date == null || date == DateTime(1)) return 'NA'; + try { + return DateFormat('d/M/yyyy').format(date); + } catch (_) { + return 'NA'; + } + } + + /// Row builder with email/phone tap & copy support + Widget _buildLabelValueRow(String label, String value, + {bool isMultiLine = false}) { + final lowerLabel = label.toLowerCase(); + final isEmail = lowerLabel == 'email'; + final isPhone = lowerLabel == 'phone number' || + lowerLabel == 'emergency phone number'; + + void handleTap() { + if (value == 'NA') return; + if (isEmail) { + LauncherUtils.launchEmail(value); + } else if (isPhone) { + LauncherUtils.launchPhone(value); + } + } + + void handleLongPress() { + if (value == 'NA') return; + LauncherUtils.copyToClipboard(value, typeLabel: label); + } + + final valueWidget = GestureDetector( + onTap: (isEmail || isPhone) ? handleTap : null, + onLongPress: (isEmail || isPhone) ? handleLongPress : null, + child: Text( + value, + style: TextStyle( + fontWeight: FontWeight.normal, + color: (isEmail || isPhone) ? Colors.indigo : Colors.black54, + fontSize: 14, + decoration: + (isEmail || isPhone) ? TextDecoration.underline : TextDecoration.none, + ), + ), + ); + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (isMultiLine) ...[ + Text( + label, + style: const TextStyle( + fontWeight: FontWeight.bold, + color: Colors.black87, + fontSize: 14, + ), + ), + MySpacing.height(4), + valueWidget, + ] else + GestureDetector( + onTap: (isEmail || isPhone) ? handleTap : null, + onLongPress: (isEmail || isPhone) ? handleLongPress : null, + child: RichText( + text: TextSpan( + text: "$label: ", + style: const TextStyle( + fontWeight: FontWeight.bold, + color: Colors.black87, + fontSize: 14, + ), + children: [ + TextSpan( + text: value, + style: TextStyle( + fontWeight: FontWeight.normal, + color: + (isEmail || isPhone) ? Colors.indigo : Colors.black54, + decoration: (isEmail || isPhone) + ? TextDecoration.underline + : TextDecoration.none, + ), + ), + ], + ), + ), + ), + MySpacing.height(10), + Divider(color: Colors.grey[300], height: 1), + MySpacing.height(10), + ], + ); +} + + + /// Info card + Widget _buildInfoCard(employee) { + return Card( + elevation: 3, + shadowColor: Colors.black12, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), + child: Padding( + padding: const EdgeInsets.fromLTRB(12, 16, 12, 16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + MySpacing.height(12), + _buildLabelValueRow('Email', _getDisplayValue(employee.email)), + _buildLabelValueRow( + 'Phone Number', _getDisplayValue(employee.phoneNumber)), + _buildLabelValueRow('Emergency Contact Person', + _getDisplayValue(employee.emergencyContactPerson)), + _buildLabelValueRow('Emergency Phone Number', + _getDisplayValue(employee.emergencyPhoneNumber)), + _buildLabelValueRow('Gender', _getDisplayValue(employee.gender)), + _buildLabelValueRow('Birth Date', _formatDate(employee.birthDate)), + _buildLabelValueRow( + 'Joining Date', _formatDate(employee.joiningDate)), + _buildLabelValueRow( + 'Current Address', + _getDisplayValue(employee.currentAddress), + isMultiLine: true, + ), + _buildLabelValueRow( + 'Permanent Address', + _getDisplayValue(employee.permanentAddress), + isMultiLine: true, + ), + ], + ), + ), + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: const Color(0xFFF1F1F1), + appBar: PreferredSize( + preferredSize: const Size.fromHeight(72), + child: AppBar( + backgroundColor: const Color(0xFFF5F5F5), + elevation: 0.5, + automaticallyImplyLeading: false, + titleSpacing: 0, + title: Padding( + padding: MySpacing.xy(16, 0), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + IconButton( + icon: const Icon(Icons.arrow_back_ios_new, + color: Colors.black, size: 20), + onPressed: () => Get.offNamed('/dashboard/employees'), + ), + MySpacing.width(8), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + MyText.titleLarge( + 'Employee Details', + fontWeight: 700, + color: Colors.black, + ), + MySpacing.height(2), + GetBuilder( + builder: (projectController) { + final projectName = + projectController.selectedProject?.name ?? + 'Select Project'; + return Row( + children: [ + const Icon(Icons.work_outline, + size: 14, color: Colors.grey), + MySpacing.width(4), + Expanded( + child: MyText.bodySmall( + projectName, + fontWeight: 600, + overflow: TextOverflow.ellipsis, + color: Colors.grey[700], + ), + ), + ], + ); + }, + ), + ], + ), + ), + ], + ), + ), + ), + ), + body: Obx(() { + if (controller.isLoadingEmployeeDetails.value) { + return const Center(child: CircularProgressIndicator()); + } + + final employee = controller.selectedEmployeeDetails.value; + if (employee == null) { + return const Center(child: Text('No employee details found.')); + } + + return SafeArea( + child: SingleChildScrollView( + padding: const EdgeInsets.fromLTRB(12, 20, 12, 80), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Row( + children: [ + Avatar( + firstName: employee.firstName, + lastName: employee.lastName, + size: 45, + ), + MySpacing.width(16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + MyText.titleMedium( + '${employee.firstName} ${employee.lastName}', + fontWeight: 700, + ), + MySpacing.height(6), + MyText.bodySmall( + _getDisplayValue(employee.jobRole), + fontWeight: 500, + ), + ], + ), + ), + ], + ), + MySpacing.height(14), + _buildInfoCard(employee), + ], + ), + ), + ); + }), + floatingActionButton: Obx(() { + if (controller.isLoadingEmployeeDetails.value || + controller.selectedEmployeeDetails.value == null) { + return const SizedBox.shrink(); + } + final employee = controller.selectedEmployeeDetails.value!; + return FloatingActionButton.extended( + onPressed: () { + showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.transparent, + builder: (context) => AssignProjectBottomSheet( + employeeId: widget.employeeId, + jobRoleId: employee.jobRoleId, + ), + ); + }, + backgroundColor: Colors.red, + icon: const Icon(Icons.assignment), + label: const Text( + 'Assign to Project', + style: TextStyle(fontSize: 13, fontWeight: FontWeight.w500), + ), + ); + }), + ); + } +} \ No newline at end of file diff --git a/lib/view/employees/employees_screen.dart b/lib/view/employees/employees_screen.dart index 3e3dbef..2c2c1ad 100644 --- a/lib/view/employees/employees_screen.dart +++ b/lib/view/employees/employees_screen.dart @@ -2,19 +2,17 @@ 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/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_card.dart'; import 'package:marco/helpers/widgets/my_spacing.dart'; import 'package:marco/helpers/widgets/my_text.dart'; -import 'package:marco/controller/permission_controller.dart'; import 'package:marco/model/employees/add_employee_bottom_sheet.dart'; import 'package:marco/controller/dashboard/employees_screen_controller.dart'; import 'package:marco/helpers/widgets/avatar.dart'; -import 'package:marco/model/employees/employee_detail_bottom_sheet.dart'; import 'package:marco/controller/project_controller.dart'; import 'package:marco/helpers/widgets/my_custom_skeleton.dart'; +import 'package:marco/view/employees/employee_detail_screen.dart'; +import 'package:marco/model/employee_model.dart'; +import 'package:marco/helpers/utils/launcher_utils.dart'; +import 'package:marco/view/employees/assign_employee_bottom_sheet.dart'; class EmployeesScreen extends StatefulWidget { const EmployeesScreen({super.key}); @@ -24,270 +22,131 @@ class EmployeesScreen extends StatefulWidget { } class _EmployeesScreenState extends State with UIMixin { - final EmployeesScreenController employeeScreenController = + final EmployeesScreenController _employeeController = Get.put(EmployeesScreenController()); - final PermissionController permissionController = - Get.put(PermissionController()); - Future _refreshEmployees() async { - try { - final selectedProjectId = - Get.find().selectedProject?.id; - final isAllSelected = - employeeScreenController.isAllEmployeeSelected.value; - - if (isAllSelected) { - employeeScreenController.selectedProjectId = null; - await employeeScreenController.fetchAllEmployees(); - } else if (selectedProjectId != null) { - employeeScreenController.selectedProjectId = selectedProjectId; - await employeeScreenController - .fetchEmployeesByProject(selectedProjectId); - } else { - // ❗ Clear employees if neither selected - employeeScreenController.clearEmployees(); - } - - employeeScreenController.update(['employee_screen_controller']); - } catch (e, stackTrace) { - debugPrint('Error refreshing employee data: ${e.toString()}'); - debugPrintStack(stackTrace: stackTrace); - } - } + final TextEditingController _searchController = TextEditingController(); + final RxList _filteredEmployees = [].obs; @override void initState() { super.initState(); + WidgetsBinding.instance.addPostFrameCallback((_) { + _initEmployees(); + _searchController.addListener(() { + _filterEmployees(_searchController.text); + }); + }); + } + + Future _initEmployees() async { final selectedProjectId = Get.find().selectedProject?.id; if (selectedProjectId != null) { - employeeScreenController.selectedProjectId = selectedProjectId; - employeeScreenController.fetchEmployeesByProject(selectedProjectId); - } else if (employeeScreenController.isAllEmployeeSelected.value) { - employeeScreenController.selectedProjectId = null; - employeeScreenController.fetchAllEmployees(); + _employeeController.selectedProjectId = selectedProjectId; + await _employeeController.fetchEmployeesByProject(selectedProjectId); + } else if (_employeeController.isAllEmployeeSelected.value) { + _employeeController.selectedProjectId = null; + await _employeeController.fetchAllEmployees(); } else { - employeeScreenController.clearEmployees(); + _employeeController.clearEmployees(); } + _filterEmployees(_searchController.text); + } + + Future _refreshEmployees() async { + try { + final selectedProjectId = + Get.find().selectedProject?.id; + final isAllSelected = _employeeController.isAllEmployeeSelected.value; + + if (isAllSelected) { + _employeeController.selectedProjectId = null; + await _employeeController.fetchAllEmployees(); + } else if (selectedProjectId != null) { + _employeeController.selectedProjectId = selectedProjectId; + await _employeeController.fetchEmployeesByProject(selectedProjectId); + } else { + _employeeController.clearEmployees(); + } + + _filterEmployees(_searchController.text); + _employeeController.update(['employee_screen_controller']); + } catch (e, stackTrace) { + debugPrint('Error refreshing employee data: $e'); + debugPrintStack(stackTrace: stackTrace); + } + } + + void _filterEmployees(String query) { + final employees = _employeeController.employees; + if (query.isEmpty) { + _filteredEmployees.assignAll(employees); + return; + } + final lowerQuery = query.toLowerCase(); + _filteredEmployees.assignAll( + employees.where((e) => + e.name.toLowerCase().contains(lowerQuery) || + e.email.toLowerCase().contains(lowerQuery) || + e.phoneNumber.toLowerCase().contains(lowerQuery) || + e.jobRole.toLowerCase().contains(lowerQuery)), + ); + } + + Future _onAddNewEmployee() async { + final result = await showModalBottomSheet>( + context: context, + isScrollControlled: true, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.vertical(top: Radius.circular(16))), + backgroundColor: Colors.transparent, + builder: (context) => AddEmployeeBottomSheet(), + ); + + if (result == null || result['success'] != true) return; + + final employeeData = result['data']; + final employeeId = employeeData['id'] as String; + final jobRoleId = employeeData['jobRoleId'] as String?; + + await showModalBottomSheet( + context: context, + isScrollControlled: true, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.vertical(top: Radius.circular(24))), + backgroundColor: Colors.transparent, + builder: (context) => AssignProjectBottomSheet( + employeeId: employeeId, + jobRoleId: jobRoleId ?? '', + ), + ); + + await _refreshEmployees(); } @override Widget build(BuildContext context) { return Scaffold( - backgroundColor: const Color(0xFFF5F5F5), - appBar: PreferredSize( - preferredSize: const Size.fromHeight(72), - child: AppBar( - backgroundColor: const Color(0xFFF5F5F5), - elevation: 0.5, - automaticallyImplyLeading: false, - titleSpacing: 0, - title: Padding( - padding: MySpacing.xy(16, 0), - child: Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - IconButton( - icon: const Icon(Icons.arrow_back_ios_new, - color: Colors.black, size: 20), - onPressed: () => Get.offNamed('/dashboard'), - ), - MySpacing.width(8), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - MyText.titleLarge( - 'Employees', - fontWeight: 700, - color: Colors.black, - ), - MySpacing.height(2), - GetBuilder( - builder: (projectController) { - final projectName = - projectController.selectedProject?.name ?? - 'Select Project'; - return Row( - children: [ - const Icon(Icons.work_outline, - size: 14, color: Colors.grey), - MySpacing.width(4), - Expanded( - child: MyText.bodySmall( - projectName, - fontWeight: 600, - overflow: TextOverflow.ellipsis, - color: Colors.grey[700], - ), - ), - ], - ); - }, - ), - ], - ), - ), - ], - ), - ), - ), - ), - floatingActionButton: InkWell( - onTap: () async { - final result = await showModalBottomSheet( - context: context, - isScrollControlled: true, - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.vertical(top: Radius.circular(16)), - ), - backgroundColor: Colors.transparent, - builder: (context) => AddEmployeeBottomSheet(), - ); - - if (result == true) { - await _refreshEmployees(); - } - }, - borderRadius: BorderRadius.circular(28), - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), - decoration: BoxDecoration( - color: Colors.blueAccent, - borderRadius: BorderRadius.circular(28), - boxShadow: const [ - BoxShadow( - color: Colors.black26, - blurRadius: 6, - offset: Offset(0, 3), - ), - ], - ), - child: const Row( - mainAxisSize: MainAxisSize.min, - children: [ - Icon(Icons.add, color: Colors.white), - SizedBox(width: 8), - Text('Add New Employee', style: TextStyle(color: Colors.white)), - ], - ), - ), - ), + backgroundColor: Colors.white, + appBar: _buildAppBar(), + floatingActionButton: _buildFloatingActionButton(), body: SafeArea( child: GetBuilder( - init: employeeScreenController, + init: _employeeController, tag: 'employee_screen_controller', builder: (controller) { + _filterEmployees(_searchController.text); return SingleChildScrollView( - padding: const EdgeInsets.only(bottom: 80), + padding: const EdgeInsets.only(bottom: 40), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Padding( - padding: MySpacing.x(flexSpacing), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - MyBreadcrumb( - children: [ - MyBreadcrumbItem(name: 'Dashboard'), - MyBreadcrumbItem(name: 'Employees', active: true), - ], - ), - ], - ), - ), + MySpacing.height(flexSpacing), + _buildSearchAndActionRow(), MySpacing.height(flexSpacing), Padding( padding: MySpacing.x(flexSpacing), - child: Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - Obx(() { - return Row( - children: [ - Checkbox( - value: employeeScreenController - .isAllEmployeeSelected.value, - activeColor: Colors.blueAccent, - fillColor: - MaterialStateProperty.resolveWith( - (states) { - if (states.contains(MaterialState.selected)) { - return Colors.blueAccent; - } - return Colors.transparent; - }), - checkColor: Colors.white, - side: BorderSide( - color: Colors.black, - width: 2, - ), - onChanged: (value) async { - employeeScreenController - .isAllEmployeeSelected.value = value!; - - if (value) { - employeeScreenController.selectedProjectId = - null; - await employeeScreenController - .fetchAllEmployees(); - } else { - final selectedProjectId = - Get.find() - .selectedProject - ?.id; - - if (selectedProjectId != null) { - employeeScreenController - .selectedProjectId = - selectedProjectId; - await employeeScreenController - .fetchEmployeesByProject( - selectedProjectId); - } else { - // ✅ THIS is your critical path - employeeScreenController.clearEmployees(); - } - } - - employeeScreenController - .update(['employee_screen_controller']); - }, - ), - MyText.bodyMedium( - "All Employees", - fontWeight: 600, - ), - ], - ); - }), - const SizedBox(width: 16), - MyText.bodyMedium("Refresh", fontWeight: 600), - Tooltip( - message: 'Refresh Data', - child: InkWell( - borderRadius: BorderRadius.circular(24), - onTap: _refreshEmployees, - child: MouseRegion( - cursor: SystemMouseCursors.click, - child: const Padding( - padding: EdgeInsets.all(8), - child: Icon( - Icons.refresh, - color: Colors.green, - size: 28, - ), - ), - ), - ), - ), - ], - ), - ), - Padding( - padding: MySpacing.x(flexSpacing), - child: dailyProgressReportTab(), + child: _buildEmployeeList(), ), ], ), @@ -298,118 +157,367 @@ class _EmployeesScreenState extends State with UIMixin { ); } - Widget dailyProgressReportTab() { + PreferredSizeWidget _buildAppBar() { + return PreferredSize( + preferredSize: const Size.fromHeight(72), + child: AppBar( + backgroundColor: const Color(0xFFF5F5F5), + elevation: 0.5, + automaticallyImplyLeading: false, + titleSpacing: 0, + title: Padding( + padding: MySpacing.xy(16, 0), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + IconButton( + icon: const Icon(Icons.arrow_back_ios_new, + color: Colors.black, size: 20), + onPressed: () => Get.offNamed('/dashboard'), + ), + MySpacing.width(8), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + MyText.titleLarge( + 'Employees', + fontWeight: 700, + color: Colors.black, + ), + MySpacing.height(2), + GetBuilder( + builder: (projectController) { + final projectName = + projectController.selectedProject?.name ?? + 'Select Project'; + return Row( + children: [ + const Icon(Icons.work_outline, + size: 14, color: Colors.grey), + MySpacing.width(4), + Expanded( + child: MyText.bodySmall( + projectName, + fontWeight: 600, + overflow: TextOverflow.ellipsis, + color: Colors.grey[700], + ), + ), + ], + ); + }, + ), + ], + ), + ), + ], + ), + ), + ), + ); + } + + Widget _buildFloatingActionButton() { + return InkWell( + onTap: _onAddNewEmployee, + borderRadius: BorderRadius.circular(28), + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + decoration: BoxDecoration( + color: Colors.red, + borderRadius: BorderRadius.circular(28), + boxShadow: const [ + BoxShadow( + color: Colors.black26, + blurRadius: 6, + offset: Offset(0, 3), + ), + ], + ), + child: const Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(Icons.add, color: Colors.white), + SizedBox(width: 8), + Text('Add New Employee', style: TextStyle(color: Colors.white)), + ], + ), + ), + ); + } + + Widget _buildSearchAndActionRow() { + return Padding( + padding: MySpacing.x(flexSpacing), + child: Row( + children: [ + Expanded(child: _buildSearchField()), + const SizedBox(width: 8), + _buildRefreshButton(), + const SizedBox(width: 4), + _buildPopupMenu(), + ], + ), + ); + } + + Widget _buildSearchField() { + return SizedBox( + height: 36, + child: TextField( + controller: _searchController, + style: const TextStyle(fontSize: 13, height: 1.2), + decoration: InputDecoration( + isDense: true, + contentPadding: + const EdgeInsets.symmetric(horizontal: 8, vertical: 8), + prefixIcon: const Icon(Icons.search, size: 18, color: Colors.grey), + prefixIconConstraints: + const BoxConstraints(minWidth: 32, minHeight: 32), + hintText: 'Search contacts...', + hintStyle: const TextStyle(fontSize: 13, color: Colors.grey), + filled: true, + fillColor: Colors.white, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: BorderSide(color: Colors.grey.shade300, width: 1), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: BorderSide(color: Colors.grey.shade300, width: 1), + ), + suffixIcon: _searchController.text.isNotEmpty + ? GestureDetector( + onTap: () { + _searchController.clear(); + _filterEmployees(''); + setState(() {}); + }, + child: const Icon(Icons.close, size: 18, color: Colors.grey), + ) + : null, + ), + onChanged: (_) => setState(() {}), + ), + ); + } + + Widget _buildRefreshButton() { + return Tooltip( + message: 'Refresh Data', + child: InkWell( + borderRadius: BorderRadius.circular(24), + onTap: _refreshEmployees, + child: const Padding( + padding: EdgeInsets.all(10), + child: Icon(Icons.refresh, color: Colors.green, size: 28), + ), + ), + ); + } + + Widget _buildPopupMenu() { + return PopupMenuButton( + icon: Stack( + clipBehavior: Clip.none, + children: [ + const Icon(Icons.tune, color: Colors.black), + Obx(() { + return _employeeController.isAllEmployeeSelected.value + ? Positioned( + right: -1, + top: -1, + child: Container( + width: 10, + height: 10, + decoration: const BoxDecoration( + color: Colors.red, shape: BoxShape.circle), + ), + ) + : const SizedBox.shrink(); + }), + ], + ), + onSelected: (value) async { + if (value == 'all_employees') { + _employeeController.isAllEmployeeSelected.value = + !_employeeController.isAllEmployeeSelected.value; + + if (_employeeController.isAllEmployeeSelected.value) { + _employeeController.selectedProjectId = null; + await _employeeController.fetchAllEmployees(); + } else { + final selectedProjectId = + Get.find().selectedProject?.id; + if (selectedProjectId != null) { + _employeeController.selectedProjectId = selectedProjectId; + await _employeeController + .fetchEmployeesByProject(selectedProjectId); + } else { + _employeeController.clearEmployees(); + } + } + _filterEmployees(_searchController.text); + _employeeController.update(['employee_screen_controller']); + } + }, + itemBuilder: (context) => [ + PopupMenuItem( + value: 'all_employees', + child: Obx( + () => Row( + children: [ + Checkbox( + value: _employeeController.isAllEmployeeSelected.value, + onChanged: (bool? value) => + Navigator.pop(context, 'all_employees'), + checkColor: Colors.white, + activeColor: Colors.red, + side: const BorderSide(color: Colors.black, width: 1.5), + fillColor: MaterialStateProperty.resolveWith((states) { + if (states.contains(MaterialState.selected)) { + return Colors.red; + } + return Colors.white; + }), + ), + const Text('All Employees'), + ], + ), + ), + ), + ], + ); + } + + Widget _buildEmployeeList() { return Obx(() { - final isLoading = employeeScreenController.isLoading.value; - final employees = employeeScreenController.employees; + final isLoading = _employeeController.isLoading.value; + final employees = _filteredEmployees; + + // Show skeleton loader while data is being fetched if (isLoading) { - return SkeletonLoaders.employeeListSkeletonLoader(); + return ListView.separated( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: 8, // number of skeleton items + separatorBuilder: (_, __) => MySpacing.height(12), + itemBuilder: (_, __) => SkeletonLoaders.employeeSkeletonCard(), + ); } + + // Show empty state when no employees are found if (employees.isEmpty) { return Padding( - padding: const EdgeInsets.only(top: 50), + padding: const EdgeInsets.only(top: 60), child: Center( child: MyText.bodySmall( - "No Assigned Employees Found", + "No Employees Found", fontWeight: 600, + color: Colors.grey[700], ), ), ); } - return SingleChildScrollView( - padding: const EdgeInsets.only(bottom: 80), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: employees.map((employee) { - return InkWell( - onTap: () { - showModalBottomSheet( - context: context, - isScrollControlled: true, - backgroundColor: Colors.transparent, - builder: (context) => - EmployeeDetailBottomSheet(employeeId: employee.id), - ); - }, - child: MyCard.bordered( - borderRadiusAll: 12, - paddingAll: 10, - margin: MySpacing.bottom(12), - shadow: MyShadow(elevation: 3), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Avatar( - firstName: employee.firstName, - lastName: employee.lastName, - size: 41, + + // Show the actual employee list + return ListView.separated( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + padding: MySpacing.only(bottom: 80), + itemCount: employees.length, + separatorBuilder: (_, __) => MySpacing.height(12), + itemBuilder: (context, index) { + final employee = employees[index]; + final nameParts = employee.name.trim().split(' '); + final firstName = nameParts.first; + final lastName = nameParts.length > 1 ? nameParts.last : ''; + + return InkWell( + onTap: () => + Get.to(() => EmployeeDetailPage(employeeId: employee.id)), + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 0), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Avatar(firstName: firstName, lastName: lastName, size: 35), + MySpacing.width(12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + MyText.titleSmall( + employee.name, + fontWeight: 600, + overflow: TextOverflow.ellipsis, + ), + if (employee.jobRole.isNotEmpty) + MyText.bodySmall( + employee.jobRole, + color: Colors.grey[700], + overflow: TextOverflow.ellipsis, ), - const SizedBox(width: 16), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, + MySpacing.height(8), + if (employee.email.isNotEmpty && employee.email != '-') + GestureDetector( + onTap: () => + LauncherUtils.launchEmail(employee.email), + onLongPress: () => LauncherUtils.copyToClipboard( + employee.email, + typeLabel: 'Email'), + child: Row( children: [ - Wrap( - crossAxisAlignment: WrapCrossAlignment.center, - spacing: 6, - children: [ - MyText.titleMedium( - employee.name, - fontWeight: 600, - overflow: TextOverflow.visible, - maxLines: null, - ), - MyText.titleSmall( - '(${employee.jobRole})', - fontWeight: 400, - overflow: TextOverflow.visible, - maxLines: null, - ), - ], - ), - const SizedBox(height: 8), - if (employee.email.isNotEmpty && - employee.email != '-') ...[ - Row( - children: [ - const Icon(Icons.email, - size: 16, color: Colors.red), - const SizedBox(width: 4), - Flexible( - child: MyText.titleSmall( - employee.email, - fontWeight: 400, - overflow: TextOverflow.ellipsis, - ), - ), - ], + const Icon(Icons.email_outlined, + size: 16, color: Colors.indigo), + MySpacing.width(4), + ConstrainedBox( + constraints: + const BoxConstraints(maxWidth: 180), + child: MyText.labelSmall( + employee.email, + overflow: TextOverflow.ellipsis, + color: Colors.indigo, + decoration: TextDecoration.underline, ), - const SizedBox(height: 8), - ], - Row( - children: [ - const Icon(Icons.phone, - size: 16, color: Colors.blueAccent), - const SizedBox(width: 4), - MyText.titleSmall( - employee.phoneNumber, - fontWeight: 400, - ), - ], ), ], ), ), - ], - ) - ], + if (employee.email.isNotEmpty && employee.email != '-') + MySpacing.height(6), + if (employee.phoneNumber.isNotEmpty) + GestureDetector( + onTap: () => + LauncherUtils.launchPhone(employee.phoneNumber), + onLongPress: () => LauncherUtils.copyToClipboard( + employee.phoneNumber, + typeLabel: 'Phone'), + child: Row( + children: [ + const Icon(Icons.phone_outlined, + size: 16, color: Colors.indigo), + MySpacing.width(4), + MyText.labelSmall( + employee.phoneNumber, + color: Colors.indigo, + decoration: TextDecoration.underline, + ), + ], + ), + ), + ], + ), ), - )); - }).toList(), - ), + const Icon(Icons.arrow_forward_ios, + color: Colors.grey, size: 16), + ], + ), + ), + ); + }, ); }); } diff --git a/lib/view/layouts/layout.dart b/lib/view/layouts/layout.dart index 541da48..44db173 100644 --- a/lib/view/layouts/layout.dart +++ b/lib/view/layouts/layout.dart @@ -26,6 +26,23 @@ class _LayoutState extends State { final bool isBetaEnvironment = ApiEndpoints.baseUrl.contains("stage"); final projectController = Get.find(); + bool hasMpin = true; + + @override + void initState() { + super.initState(); + _checkMpinStatus(); + } + + Future _checkMpinStatus() async { + final bool mpinStatus = await LocalStorage.getIsMpin(); + if (mounted) { + setState(() { + hasMpin = mpinStatus; + }); + } + } + @override Widget build(BuildContext context) { return MyResponsive(builder: (context, _, screenMT) { @@ -43,7 +60,7 @@ class _LayoutState extends State { Widget _buildScaffold(BuildContext context, {bool isMobile = false}) { return Scaffold( key: controller.scaffoldKey, - endDrawer: UserProfileBar(), + endDrawer: const UserProfileBar(), floatingActionButton: widget.floatingActionButton, body: SafeArea( child: GestureDetector( @@ -68,28 +85,7 @@ class _LayoutState extends State { ), ], ), - Obx(() { - if (!projectController.isProjectSelectionExpanded.value) { - return const SizedBox.shrink(); - } - return Positioned( - top: 95, - left: 16, - right: 16, - child: Material( - elevation: 4, - borderRadius: BorderRadius.circular(12), - child: Container( - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(12), - ), - padding: const EdgeInsets.all(10), - child: _buildProjectList(context, isMobile), - ), - ), - ); - }), + _buildProjectDropdown(context, isMobile), ], ), ), @@ -97,6 +93,7 @@ class _LayoutState extends State { ); } + /// Header Section Widget _buildHeader(BuildContext context, bool isMobile) { return Padding( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6), @@ -147,46 +144,65 @@ class _LayoutState extends State { const SizedBox(width: 12), Expanded( child: hasProjects - ? GestureDetector( - onTap: () => projectController - .isProjectSelectionExpanded - .toggle(), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( + ? (projectController.projects.length > 1 + ? GestureDetector( + onTap: () => projectController + .isProjectSelectionExpanded + .toggle(), + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, children: [ - Expanded( - child: Row( - children: [ - Expanded( - child: MyText.bodyLarge( - selectedProject?.name ?? - "Select Project", - fontWeight: 700, - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), + Row( + children: [ + Expanded( + child: Row( + children: [ + Expanded( + child: MyText.bodyLarge( + selectedProject?.name ?? + "Select Project", + fontWeight: 700, + maxLines: 1, + overflow: + TextOverflow.ellipsis, + ), + ), + Icon( + isExpanded + ? Icons + .arrow_drop_up_outlined + : Icons + .arrow_drop_down_outlined, + color: Colors.black, + ), + ], ), - Icon( - isExpanded - ? Icons.arrow_drop_up_outlined - : Icons - .arrow_drop_down_outlined, - color: Colors.black, - ), - ], - ), + ), + ], + ), + MyText.bodyMedium( + "Hi, ${employeeInfo?.firstName ?? ''}", + color: Colors.black54, ), ], ), - MyText.bodyMedium( - "Hi, ${employeeInfo?.firstName ?? ''}", - color: Colors.black54, - ), - ], - ), - ) + ) + : Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + MyText.bodyLarge( + selectedProject?.name ?? "No Project", + fontWeight: 700, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + MyText.bodyMedium( + "Hi, ${employeeInfo?.firstName ?? ''}", + color: Colors.black54, + ), + ], + )) : Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -217,11 +233,32 @@ class _LayoutState extends State { fontWeight: 700, ), ), - IconButton( - icon: const Icon(Icons.menu), - onPressed: () => - controller.scaffoldKey.currentState?.openEndDrawer(), - ), + Stack( + clipBehavior: Clip.none, + alignment: Alignment.center, + children: [ + IconButton( + icon: const Icon(Icons.menu), + onPressed: () => controller.scaffoldKey.currentState + ?.openEndDrawer(), + ), + if (!hasMpin) + Positioned( + right: 10, + top: 10, + child: Container( + width: 14, + height: 14, + decoration: BoxDecoration( + color: Colors.redAccent, + shape: BoxShape.circle, + border: + Border.all(color: Colors.white, width: 2), + ), + ), + ), + ], + ) ], ), ), @@ -243,6 +280,7 @@ class _LayoutState extends State { ); } + /// Loading Skeleton for Header Widget _buildLoadingSkeleton() { return Card( elevation: 4, @@ -293,6 +331,32 @@ class _LayoutState extends State { ); } + /// Project List Popup + Widget _buildProjectDropdown(BuildContext context, bool isMobile) { + return Obx(() { + if (!projectController.isProjectSelectionExpanded.value) { + return const SizedBox.shrink(); + } + return Positioned( + top: 95, + left: 16, + right: 16, + child: Material( + elevation: 4, + borderRadius: BorderRadius.circular(12), + child: Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + ), + padding: const EdgeInsets.all(10), + child: _buildProjectList(context, isMobile), + ), + ), + ); + }); + } + Widget _buildProjectList(BuildContext context, bool isMobile) { return Column( crossAxisAlignment: CrossAxisAlignment.start, diff --git a/lib/view/layouts/user_profile_right_bar.dart b/lib/view/layouts/user_profile_right_bar.dart index 8b37bbf..d85674e 100644 --- a/lib/view/layouts/user_profile_right_bar.dart +++ b/lib/view/layouts/user_profile_right_bar.dart @@ -9,6 +9,8 @@ import 'package:marco/helpers/widgets/my_text.dart'; import 'package:flutter_lucide/flutter_lucide.dart'; import 'package:marco/model/employee_info.dart'; import 'package:marco/helpers/widgets/avatar.dart'; +import 'package:get/get.dart'; +import 'package:marco/controller/auth/mpin_controller.dart'; class UserProfileBar extends StatefulWidget { final bool isCondensed; @@ -24,11 +26,13 @@ class _UserProfileBarState extends State final ThemeCustomizer customizer = ThemeCustomizer.instance; bool isCondensed = false; EmployeeInfo? employeeInfo; + bool hasMpin = true; @override void initState() { super.initState(); _loadEmployeeInfo(); + _checkMpinStatus(); } void _loadEmployeeInfo() { @@ -37,6 +41,13 @@ class _UserProfileBarState extends State }); } + Future _checkMpinStatus() async { + final bool mpinStatus = await LocalStorage.getIsMpin(); + setState(() { + hasMpin = mpinStatus; + }); + } + @override Widget build(BuildContext context) { isCondensed = widget.isCondensed; @@ -44,19 +55,23 @@ class _UserProfileBarState extends State return MyCard( borderRadiusAll: 16, paddingAll: 0, - shadow: MyShadow(position: MyShadowPosition.centerRight, elevation: 4), + shadow: MyShadow( + position: MyShadowPosition.centerRight, + elevation: 6, + blurRadius: 12, + ), child: AnimatedContainer( decoration: BoxDecoration( gradient: LinearGradient( colors: [ - leftBarTheme.background.withOpacity(0.95), - leftBarTheme.background.withOpacity(0.85), + leftBarTheme.background.withOpacity(0.97), + leftBarTheme.background.withOpacity(0.88), ], begin: Alignment.topCenter, end: Alignment.bottomCenter, ), ), - width: isCondensed ? 90 : 250, + width: isCondensed ? 90 : 260, duration: const Duration(milliseconds: 300), curve: Curves.easeInOut, child: SafeArea( @@ -65,9 +80,10 @@ class _UserProfileBarState extends State left: false, right: false, child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, children: [ userProfileSection(), - MySpacing.height(8), + MySpacing.height(16), supportAndSettingsMenu(), const Spacer(), logoutButton(), @@ -78,17 +94,18 @@ class _UserProfileBarState extends State ); } + /// User Profile Section - Avatar + Name Widget userProfileSection() { if (employeeInfo == null) { return const Padding( - padding: EdgeInsets.all(24.0), + padding: EdgeInsets.symmetric(vertical: 32), child: Center(child: CircularProgressIndicator()), ); } return Container( width: double.infinity, - padding: MySpacing.xy(58, 68), + padding: MySpacing.fromLTRB(20, 50, 30, 50), decoration: BoxDecoration( color: leftBarTheme.activeItemBackground, borderRadius: const BorderRadius.only( @@ -96,55 +113,102 @@ class _UserProfileBarState extends State topRight: Radius.circular(16), ), ), - child: Column( + child: Row( children: [ Avatar( - firstName: employeeInfo?.firstName ?? 'First', - lastName: employeeInfo?.lastName ?? 'Name', - size: 60, + firstName: employeeInfo?.firstName ?? 'F', + lastName: employeeInfo?.lastName ?? 'N', + size: 50, ), - MySpacing.height(12), - MyText.labelLarge( - "${employeeInfo?.firstName ?? 'First'} ${employeeInfo?.lastName ?? 'Last'}", - fontWeight: 700, - color: leftBarTheme.activeItemColor, - textAlign: TextAlign.center, + MySpacing.width(12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + MyText.bodyMedium( + "${employeeInfo?.firstName ?? 'First'} ${employeeInfo?.lastName ?? 'Last'}", + fontWeight: 700, + color: leftBarTheme.activeItemColor, + ), + ], + ), ), ], ), ); } + /// Menu Section with Settings, Support & MPIN Widget supportAndSettingsMenu() { return Padding( padding: MySpacing.xy(16, 16), child: Column( children: [ - menuItem(icon: LucideIcons.settings, label: "Settings"), - MySpacing.height(12), - menuItem(icon: LucideIcons.badge_help, label: "Support"), + menuItem( + icon: LucideIcons.settings, + label: "Settings", + ), + MySpacing.height(14), + menuItem( + icon: LucideIcons.badge_help, + label: "Support", + ), + MySpacing.height(14), + menuItem( + icon: LucideIcons.lock, + label: hasMpin ? "Change MPIN" : "Set MPIN", + iconColor: hasMpin ? leftBarTheme.onBackground : Colors.redAccent, + labelColor: hasMpin ? leftBarTheme.onBackground : Colors.redAccent, + onTap: () { + final controller = Get.put(MPINController()); + if (hasMpin) { + controller.setChangeMpinMode(); + } + Navigator.pushNamed(context, "/auth/mpin-auth"); + }, + filled: true, + ), ], ), ); } - Widget menuItem({required IconData icon, required String label}) { + Widget menuItem({ + required IconData icon, + required String label, + Color? iconColor, + Color? labelColor, + VoidCallback? onTap, + bool filled = false, + }) { return InkWell( - onTap: () {}, - borderRadius: BorderRadius.circular(10), - hoverColor: leftBarTheme.activeItemBackground.withOpacity(0.2), - splashColor: leftBarTheme.activeItemBackground.withOpacity(0.3), - child: Padding( - padding: MySpacing.xy(12, 10), + onTap: onTap, + borderRadius: BorderRadius.circular(12), + hoverColor: leftBarTheme.activeItemBackground.withOpacity(0.25), + splashColor: leftBarTheme.activeItemBackground.withOpacity(0.35), + child: Container( + padding: MySpacing.xy(14, 12), + decoration: BoxDecoration( + color: filled + ? leftBarTheme.activeItemBackground.withOpacity(0.15) + : Colors.transparent, + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: filled + ? leftBarTheme.activeItemBackground.withOpacity(0.3) + : Colors.transparent, + width: 1, + ), + ), child: Row( children: [ - Icon(icon, size: 20, color: leftBarTheme.onBackground), - MySpacing.width(12), + Icon(icon, size: 22, color: iconColor ?? leftBarTheme.onBackground), + MySpacing.width(14), Expanded( child: MyText.bodyMedium( label, - color: leftBarTheme.onBackground, - fontWeight: 500, + color: labelColor ?? leftBarTheme.onBackground, + fontWeight: 600, ), ), ], @@ -153,142 +217,19 @@ class _UserProfileBarState extends State ); } + /// Logout Button Widget logoutButton() { return InkWell( onTap: () async { - bool? confirm = await showDialog( - context: context, - builder: (context) { - return Dialog( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(20), - ), - child: Padding( - padding: - const EdgeInsets.symmetric(horizontal: 24, vertical: 28), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Icon( - LucideIcons.log_out, - size: 48, - color: Colors.redAccent, - ), - const SizedBox(height: 16), - Text( - "Logout Confirmation", - style: TextStyle( - fontSize: 20, - fontWeight: FontWeight.w700, - color: Theme.of(context).colorScheme.onBackground, - ), - ), - const SizedBox(height: 12), - Text( - "Are you sure you want to logout?\nYou will need to login again to continue.", - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 14, - color: Theme.of(context) - .colorScheme - .onSurface - .withOpacity(0.7), - ), - ), - const SizedBox(height: 24), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Expanded( - child: TextButton( - onPressed: () => Navigator.pop(context, false), - style: TextButton.styleFrom( - foregroundColor: Colors.grey.shade700, - ), - child: const Text("Cancel"), - ), - ), - const SizedBox(width: 12), - Expanded( - child: ElevatedButton( - onPressed: () async { - await LocalStorage.logout(); - }, - style: ElevatedButton.styleFrom( - backgroundColor: Colors.redAccent, - foregroundColor: Colors.white, - padding: const EdgeInsets.symmetric(vertical: 12), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - ), - ), - child: const Text("Logout"), - ), - ), - ], - ) - ], - ), - ), - ); - }, - ); - - if (confirm == true) { - // Show animated loader dialog - showDialog( - context: context, - barrierDismissible: false, - builder: (context) => Dialog( - backgroundColor: Colors.transparent, - child: Column( - mainAxisSize: MainAxisSize.min, - children: const [ - CircularProgressIndicator(), - SizedBox(height: 12), - Text( - "Logging you out...", - style: TextStyle(color: Colors.white), - ) - ], - ), - ), - ); - - await LocalStorage.logout(); - - if (mounted) { - Navigator.pop(context); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Row( - children: [ - const Icon(LucideIcons.check, color: Colors.green), - const SizedBox(width: 12), - const Text("You’ve been logged out successfully."), - ], - ), - backgroundColor: Colors.grey.shade900, - behavior: SnackBarBehavior.floating, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - ), - duration: const Duration(seconds: 3), - ), - ); - } - } + await _showLogoutConfirmation(); }, borderRadius: const BorderRadius.only( bottomLeft: Radius.circular(16), bottomRight: Radius.circular(16), ), - hoverColor: leftBarTheme.activeItemBackground.withOpacity(0.2), - splashColor: leftBarTheme.activeItemBackground.withOpacity(0.3), - child: AnimatedContainer( - duration: const Duration(milliseconds: 150), - curve: Curves.easeInOut, - width: double.infinity, + hoverColor: leftBarTheme.activeItemBackground.withOpacity(0.25), + splashColor: leftBarTheme.activeItemBackground.withOpacity(0.35), + child: Container( padding: MySpacing.all(16), decoration: BoxDecoration( color: leftBarTheme.activeItemBackground, @@ -316,4 +257,78 @@ class _UserProfileBarState extends State ), ); } + + Future _showLogoutConfirmation() async { + bool? confirm = await showDialog( + context: context, + builder: (context) => _buildLogoutDialog(context), + ); + + if (confirm == true) { + await LocalStorage.logout(); + } + } + + Widget _buildLogoutDialog(BuildContext context) { + return Dialog( + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 28), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(LucideIcons.log_out, size: 48, color: Colors.redAccent), + const SizedBox(height: 16), + Text( + "Logout Confirmation", + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.w700, + color: Theme.of(context).colorScheme.onBackground, + ), + ), + const SizedBox(height: 12), + Text( + "Are you sure you want to logout?\nYou will need to login again to continue.", + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 14, + color: Theme.of(context).colorScheme.onSurface.withOpacity(0.7), + ), + ), + const SizedBox(height: 24), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: TextButton( + onPressed: () => Navigator.pop(context, false), + style: TextButton.styleFrom( + foregroundColor: Colors.grey.shade700, + ), + child: const Text("Cancel"), + ), + ), + const SizedBox(width: 12), + Expanded( + child: ElevatedButton( + onPressed: () => Navigator.pop(context, true), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.redAccent, + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric(vertical: 12), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + child: const Text("Logout"), + ), + ), + ], + ), + ], + ), + ), + ); + } }