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 17328a6..640d023 100644 --- a/lib/helpers/services/api_endpoints.dart +++ b/lib/helpers/services/api_endpoints.dart @@ -15,11 +15,14 @@ class ApiEndpoints { static const String uploadAttendanceImage = "/attendance/record-image"; // Employee Screen API Endpoints - static const String getAllEmployeesByProject = "/Project/employees/get"; + 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 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 Screen 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 230fde7..e0a3b64 100644 --- a/lib/helpers/services/api_service.dart +++ b/lib/helpers/services/api_service.dart @@ -477,6 +477,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 = { 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/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/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..3bcae42 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'; class EmployeesScreen extends StatefulWidget { const EmployeesScreen({super.key}); @@ -28,6 +26,34 @@ class _EmployeesScreenState extends State with UIMixin { Get.put(EmployeesScreenController()); final PermissionController permissionController = Get.put(PermissionController()); + + final TextEditingController _searchController = TextEditingController(); + final RxList _filteredEmployees = [].obs; + + @override + void initState() { + super.initState(); + + WidgetsBinding.instance.addPostFrameCallback((_) { + 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(); + } else { + employeeScreenController.clearEmployees(); + } + + _searchController.addListener(() { + _filterEmployees(_searchController.text); + }); + }); + } + Future _refreshEmployees() async { try { final selectedProjectId = @@ -43,10 +69,10 @@ class _EmployeesScreenState extends State with UIMixin { await employeeScreenController .fetchEmployeesByProject(selectedProjectId); } else { - // ❗ Clear employees if neither selected employeeScreenController.clearEmployees(); } + _filterEmployees(_searchController.text); employeeScreenController.update(['employee_screen_controller']); } catch (e, stackTrace) { debugPrint('Error refreshing employee data: ${e.toString()}'); @@ -54,26 +80,27 @@ class _EmployeesScreenState extends State with UIMixin { } } - @override - void initState() { - super.initState(); - 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(); + void _filterEmployees(String query) { + final employees = employeeScreenController.employees; + if (query.isEmpty) { + _filteredEmployees.assignAll(employees); } else { - employeeScreenController.clearEmployees(); + final lowerQuery = query.toLowerCase(); + _filteredEmployees.assignAll( + employees.where((e) { + return e.name.toLowerCase().contains(lowerQuery) || + e.email.toLowerCase().contains(lowerQuery) || + e.phoneNumber.toLowerCase().contains(lowerQuery) || + e.jobRole.toLowerCase().contains(lowerQuery); + }), + ); } } @override Widget build(BuildContext context) { return Scaffold( - backgroundColor: const Color(0xFFF5F5F5), + backgroundColor: Colors.white, appBar: PreferredSize( preferredSize: const Size.fromHeight(72), child: AppBar( @@ -95,7 +122,7 @@ class _EmployeesScreenState extends State with UIMixin { Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, + mainAxisSize: MainAxisSize.min, children: [ MyText.titleLarge( 'Employees', @@ -153,7 +180,7 @@ class _EmployeesScreenState extends State with UIMixin { child: Container( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), decoration: BoxDecoration( - color: Colors.blueAccent, + color: Colors.red, borderRadius: BorderRadius.circular(28), boxShadow: const [ BoxShadow( @@ -178,113 +205,184 @@ class _EmployeesScreenState extends State with UIMixin { init: employeeScreenController, 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), + + // Search Bar + Actions Row 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, + // Search Field + Expanded( + child: 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, ), - 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']); - }, + 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, ), - MyText.bodyMedium( - "All Employees", - fontWeight: 600, - ), - ], - ); - }), - const SizedBox(width: 16), - MyText.bodyMedium("Refresh", fontWeight: 600), + onChanged: (value) => setState(() {}), + ), + ), + ), + + const SizedBox(width: 8), + + // Refresh Button 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, - ), - ), + child: const Padding( + padding: EdgeInsets.all(10), + child: Icon(Icons.refresh, + color: Colors.green, size: 28), ), ), ), + const SizedBox(width: 4), + + // Three-dot Menu + PopupMenuButton( + icon: Stack( + clipBehavior: Clip.none, + children: [ + const Icon(Icons.tune, color: Colors.black), + Obx(() { + return employeeScreenController + .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') { + employeeScreenController + .isAllEmployeeSelected.value = + !employeeScreenController + .isAllEmployeeSelected.value; + + if (employeeScreenController + .isAllEmployeeSelected.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 { + employeeScreenController.clearEmployees(); + } + } + _filterEmployees(_searchController.text); + employeeScreenController + .update(['employee_screen_controller']); + } + }, + itemBuilder: (context) => [ + PopupMenuItem( + value: 'all_employees', + child: Obx( + () => Row( + children: [ + Checkbox( + value: employeeScreenController + .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'), + ], + ), + ), + ), + ], + ) ], ), ), + MySpacing.height(flexSpacing), + + // Employee List Padding( padding: MySpacing.x(flexSpacing), child: dailyProgressReportTab(), @@ -301,115 +399,128 @@ class _EmployeesScreenState extends State with UIMixin { Widget dailyProgressReportTab() { return Obx(() { final isLoading = employeeScreenController.isLoading.value; - final employees = employeeScreenController.employees; + final employees = _filteredEmployees; + if (isLoading) { - return SkeletonLoaders.employeeListSkeletonLoader(); + return ListView.separated( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: 10, + separatorBuilder: (_, __) => MySpacing.height(12), + itemBuilder: (_, __) => SkeletonLoaders.employeeSkeletonCard(), + ); } + if (employees.isEmpty) { return Padding( padding: const EdgeInsets.only(top: 50), child: Center( child: MyText.bodySmall( - "No Assigned Employees Found", + "No Employees Found", fontWeight: 600, ), ), ); } - 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, - ), - const SizedBox(width: 16), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, + + 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(horizontal: 0, 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), + ], + 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, - ), - ], + ), + ], + ), + ), + 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, ), ], ), ), ], - ) + ], + ), + ), + Column( + children: const [ + Icon(Icons.arrow_forward_ios, + color: Colors.grey, size: 16), ], ), - )); - }).toList(), - ), + ], + ), + ), + ); + }, ); }); } diff --git a/lib/view/layouts/layout.dart b/lib/view/layouts/layout.dart index 541da48..4000606 100644 --- a/lib/view/layouts/layout.dart +++ b/lib/view/layouts/layout.dart @@ -147,46 +147,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: [