diff --git a/Marco.Pms.Services/Controllers/ServiceProjectController.cs b/Marco.Pms.Services/Controllers/ServiceProjectController.cs index 6150c65..8c56826 100644 --- a/Marco.Pms.Services/Controllers/ServiceProjectController.cs +++ b/Marco.Pms.Services/Controllers/ServiceProjectController.cs @@ -116,6 +116,15 @@ namespace Marco.Pms.Services.Controllers return StatusCode(response.StatusCode, response); } + [HttpGet("branch-type/list")] + public async Task GetBranchTypeList([FromQuery] string? searchString) + { + Employee loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); + var response = await _serviceProject.GetBranchTypeListAsync(searchString, loggedInEmployee, tenantId); + + return StatusCode(response.StatusCode, response); + } + [HttpPost("branch/create")] public async Task CreateProjectBranch([FromBody] ProjectBranchDto model) { diff --git a/Marco.Pms.Services/Service/ServiceInterfaces/IServiceProject.cs b/Marco.Pms.Services/Service/ServiceInterfaces/IServiceProject.cs index 5546c62..56f857e 100644 --- a/Marco.Pms.Services/Service/ServiceInterfaces/IServiceProject.cs +++ b/Marco.Pms.Services/Service/ServiceInterfaces/IServiceProject.cs @@ -18,6 +18,7 @@ namespace Marco.Pms.Services.Service.ServiceInterfaces #region =================================================================== Project Branch Functions =================================================================== Task> GetProjectBranchListByProjectAsync(Guid projectId, bool isActive, string? searchString, int pageNumber, int pageSize, Employee loggedInEmployee, Guid tenantId); Task> GetProjectBranchDetailsAsync(Guid id, Employee loggedInEmployee, Guid tenantId); + Task> GetBranchTypeListAsync(string? searchString, Employee loggedInEmployee, Guid tenantId); Task> CreateProjectBranchAsync(ProjectBranchDto model, Employee loggedInEmployee, Guid tenantId); Task> UpdateProjectBranchAsync(Guid id, ProjectBranchDto model, Employee loggedInEmployee, Guid tenantId); Task> DeleteProjectBranchAsync(Guid id, bool isActive, Employee loggedInEmployee, Guid tenantId); diff --git a/Marco.Pms.Services/Service/ServiceProjectService.cs b/Marco.Pms.Services/Service/ServiceProjectService.cs index 5a44d1a..bd30bdd 100644 --- a/Marco.Pms.Services/Service/ServiceProjectService.cs +++ b/Marco.Pms.Services/Service/ServiceProjectService.cs @@ -614,6 +614,11 @@ namespace Marco.Pms.Services.Service public async Task> GetProjectBranchListByProjectAsync(Guid projectId, bool isActive, string? searchString, int pageNumber, int pageSize, Employee loggedInEmployee, Guid tenantId) { + if (tenantId == Guid.Empty) + { + _logger.LogWarning("GetProjectBranchListByProjectAsync called with invalid tenant context by employee {EmployeeId}", loggedInEmployee.Id); + return ApiResponse.ErrorResponse("Access Denied", "Invalid tenant context.", 403); + } // Log method invocation with parameters for audit and debugging _logger.LogInfo("Fetching project branches for ProjectId: {ProjectId}, IsActive: {IsActive}, Page: {PageNumber}, Size: {PageSize}", projectId, isActive, pageNumber, pageSize); @@ -692,6 +697,11 @@ namespace Marco.Pms.Services.Service /// ApiResponse with the branch details or a standardized error. public async Task> GetProjectBranchDetailsAsync(Guid id, Employee loggedInEmployee, Guid tenantId) { + if (tenantId == Guid.Empty) + { + _logger.LogWarning("GetProjectBranchDetailsAsync called with invalid tenant context by employee {EmployeeId}", loggedInEmployee.Id); + return ApiResponse.ErrorResponse("Access Denied", "Invalid tenant context.", 403); + } _logger.LogInfo("Attempting to fetch details for ProjectBranchId: {ProjectBranchId}, TenantId: {TenantId} by EmployeeId: {EmployeeId}", id, tenantId, loggedInEmployee.Id); @@ -739,6 +749,55 @@ namespace Marco.Pms.Services.Service } } + /// + /// Retrieves a filtered, distinct list of project branch types for a specified tenant. + /// Supports optional search filtering, optimized for read-only access. + /// + /// Optional search string to filter branch types. + /// The employee requesting data, for audit logging. + /// Tenant identifier to scope data in a multi-tenant environment. + /// ApiResponse with list of branch types or error message. + public async Task> GetBranchTypeListAsync(string? searchString, Employee loggedInEmployee, Guid tenantId) + { + if (tenantId == Guid.Empty) + { + _logger.LogWarning("GetBranchTypeListAsync called with invalid tenant context by employee {EmployeeId}", loggedInEmployee.Id); + return ApiResponse.ErrorResponse("Access Denied", "Invalid tenant context.", 403); + } + _logger.LogInfo("Fetching distinct project branch types for TenantId: {TenantId}, RequestedBy: {EmployeeId}", tenantId, loggedInEmployee.Id); + + try + { + // Build initial query with no tracking for optimized read performance + var branchTypeQuery = _context.ProjectBranches + .AsNoTracking() + .Where(pb => pb.TenantId == tenantId) + .Select(pb => pb.BranchType); + + // Apply search filter if provided + if (!string.IsNullOrWhiteSpace(searchString)) + { + _logger.LogDebug("Applying search filter for branch types with searchString: {SearchString}", searchString); + branchTypeQuery = branchTypeQuery.Where(bt => bt.Contains(searchString)); + } + + // Get distinct branch types asynchronously + var branchTypes = await branchTypeQuery + .Distinct() + .OrderBy(bt => bt) + .ToListAsync(); + + _logger.LogInfo("Fetched {Count} distinct branch types for TenantId: {TenantId}", branchTypes.Count, tenantId); + + return ApiResponse.SuccessResponse(branchTypes, $"{branchTypes.Count} project branch types fetched successfully.", 200); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error fetching branch types for TenantId: {TenantId}", tenantId); + return ApiResponse.ErrorResponse("Failed to fetch branch types due to an internal error.", ex.Message, 500); + } + } + /// /// Creates a new project branch associated with a specific service project. /// Applies enterprise-grade validation, logging, and exception handling. @@ -749,6 +808,13 @@ namespace Marco.Pms.Services.Service /// ApiResponse containing created project branch details or error info. public async Task> CreateProjectBranchAsync(ProjectBranchDto model, Employee loggedInEmployee, Guid tenantId) { + + if (tenantId == Guid.Empty) + { + _logger.LogWarning("CreateProjectBranchAsync called with invalid tenant context by employee {EmployeeId}", loggedInEmployee.Id); + return ApiResponse.ErrorResponse("Access Denied", "Invalid tenant context.", 403); + } + _logger.LogInfo("Starting project branch creation. ProjectId: {ProjectId}, TenantId: {TenantId}, CreatedBy: {EmployeeId}", model.ProjectId, tenantId, loggedInEmployee.Id); @@ -807,6 +873,12 @@ namespace Marco.Pms.Services.Service /// ApiResponse indicating success or failure with detailed messages. public async Task> UpdateProjectBranchAsync(Guid id, ProjectBranchDto model, Employee loggedInEmployee, Guid tenantId) { + if (tenantId == Guid.Empty) + { + _logger.LogWarning("UpdateProjectBranchAsync called with invalid tenant context by employee {EmployeeId}", loggedInEmployee.Id); + return ApiResponse.ErrorResponse("Access Denied", "Invalid tenant context.", 403); + } + // Validate ID consistency between route parameter and payload DTO if (!model.Id.HasValue && model.Id != id) { @@ -892,6 +964,11 @@ namespace Marco.Pms.Services.Service /// ApiResponse indicating the result of the operation, with status and descriptive message. public async Task> DeleteProjectBranchAsync(Guid id, bool isActive, Employee loggedInEmployee, Guid tenantId) { + if (tenantId == Guid.Empty) + { + _logger.LogWarning("DeleteProjectBranchAsync called with invalid tenant context by employee {EmployeeId}", loggedInEmployee.Id); + return ApiResponse.ErrorResponse("Access Denied", "Invalid tenant context.", 403); + } _logger.LogInfo("Starting soft delete operation for ProjectBranchId: {ProjectBranchId}, TenantId: {TenantId}, By EmployeeId: {EmployeeId}", id, tenantId, loggedInEmployee.Id); @@ -1704,6 +1781,11 @@ namespace Marco.Pms.Services.Service /// ApiResponse containing the created job ticket view or error details. public async Task> CreateJobTicketAsync(CreateJobTicketDto model, Employee loggedInEmployee, Guid tenantId) { + if (tenantId == Guid.Empty) + { + _logger.LogWarning("CreateJobTicketAsync called with invalid tenant context by employee {EmployeeId}", loggedInEmployee.Id); + return ApiResponse.ErrorResponse("Access Denied", "Invalid tenant context.", 403); + } await using var transaction = await _context.Database.BeginTransactionAsync(); try { @@ -2519,6 +2601,11 @@ namespace Marco.Pms.Services.Service /// ApiResponse containing updated comment details or error information. public async Task> UpdateCommentAsync(Guid id, JobCommentDto model, Employee loggedInEmployee, Guid tenantId) { + if (tenantId == Guid.Empty) + { + _logger.LogWarning("UpdateCommentAsync called with invalid tenant context by employee {EmployeeId}", loggedInEmployee.Id); + return ApiResponse.ErrorResponse("Access Denied", "Invalid tenant context.", 403); + } // Transaction ensures atomic update of comment and attachments. await using var transaction = await _context.Database.BeginTransactionAsync(); try @@ -2733,6 +2820,11 @@ namespace Marco.Pms.Services.Service #region =================================================================== Job Tagging Functions =================================================================== public async Task> GetAttendanceForSelfAsync(Guid jobTicketId, Employee loggedInEmployee, Guid tenantId) { + if (tenantId == Guid.Empty) + { + _logger.LogWarning("GetAttendanceForSelfAsync called with invalid tenant context by employee {EmployeeId}", loggedInEmployee.Id); + return ApiResponse.ErrorResponse("Access Denied", "Invalid tenant context.", 403); + } _logger.LogInfo("GetAttendanceForSelfAsync initiated for EmployeeId: {EmployeeId}, JobTicketId: {JobTicketId}", loggedInEmployee.Id, jobTicketId); try @@ -2807,6 +2899,12 @@ namespace Marco.Pms.Services.Service } public async Task> GetAttendanceLogForAttendanceAsync(Guid jobAttendanceId, Employee loggedInEmployee, Guid tenantId) { + if (tenantId == Guid.Empty) + { + _logger.LogWarning("GetAttendanceLogForAttendanceAsync called with invalid tenant context by employee {EmployeeId}", loggedInEmployee.Id); + return ApiResponse.ErrorResponse("Access Denied", "Invalid tenant context.", 403); + } + _logger.LogInfo("GetAttendanceLogForAttendanceAsync called for JobAttendanceId: {JobAttendanceId}, TenantId: {TenantId}, EmployeeId: {EmployeeId}", jobAttendanceId, tenantId, loggedInEmployee.Id); try @@ -2885,6 +2983,13 @@ namespace Marco.Pms.Services.Service } public async Task> GetAttendanceForJobTeamAsync(Guid jobTicketId, DateTime? startDate, DateTime? endDate, Employee loggedInEmployee, Guid tenantId) { + + if (tenantId == Guid.Empty) + { + _logger.LogWarning("GetAttendanceForJobTeamAsync called with invalid tenant context by employee {EmployeeId}", loggedInEmployee.Id); + return ApiResponse.ErrorResponse("Access Denied", "Invalid tenant context.", 403); + } + _logger.LogInfo("GetAttendanceForJobTeamAsync called for JobTicketId: {JobTicketId}, TenantId: {TenantId}, EmployeeId: {EmployeeId}", jobTicketId, tenantId, loggedInEmployee.Id); try @@ -2942,6 +3047,13 @@ namespace Marco.Pms.Services.Service } public async Task> ManageJobTaggingAsync(JobAttendanceDto model, Employee loggedInEmployee, Guid tenantId) { + + if (tenantId == Guid.Empty) + { + _logger.LogWarning("ManageJobTaggingAsync called with invalid tenant context by employee {EmployeeId}", loggedInEmployee.Id); + return ApiResponse.ErrorResponse("Access Denied", "Invalid tenant context.", 403); + } + _logger.LogInfo("ManageJobTaggingAsync called for EmployeeId: {EmployeeId}, JobTicketId: {JobTicketId}", loggedInEmployee.Id, model.JobTcketId); try