Added the Addition project related information in service project list API
This commit is contained in:
parent
5522551e67
commit
f171b0add6
@ -15,6 +15,10 @@ namespace Marco.Pms.Model.ViewModels.ServiceProject
|
||||
public StatusMaster? Status { get; set; }
|
||||
public BasicOrganizationVm? Client { get; set; }
|
||||
public List<ServiceMasterVM>? Services { get; set; }
|
||||
public int TeamMemberCount { get; set; }
|
||||
public int ActiveJobsCount { get; set; }
|
||||
public int JobsPassedDueDateCount { get; set; }
|
||||
public int JobMembersCount { get; set; }
|
||||
public string? ContactName { get; set; }
|
||||
public string? ContactPhone { get; set; }
|
||||
public string? ContactEmail { get; set; }
|
||||
|
||||
@ -36,10 +36,10 @@ namespace Marco.Pms.Services.Controllers
|
||||
#region =================================================================== Service Project Functions ===================================================================
|
||||
|
||||
[HttpGet("list")]
|
||||
public async Task<IActionResult> GetServiceProjectList([FromQuery] int pageNumber = 1, [FromQuery] int pageSize = 20)
|
||||
public async Task<IActionResult> GetServiceProjectList([FromQuery] string? searchString, [FromQuery] int pageNumber = 1, [FromQuery] int pageSize = 20)
|
||||
{
|
||||
Employee loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
|
||||
var response = await _serviceProject.GetServiceProjectListAsync(pageNumber, pageSize, loggedInEmployee, tenantId);
|
||||
var response = await _serviceProject.GetServiceProjectListAsync(searchString, pageNumber, pageSize, loggedInEmployee, tenantId);
|
||||
return StatusCode(response.StatusCode, response);
|
||||
|
||||
}
|
||||
@ -120,6 +120,10 @@ namespace Marco.Pms.Services.Controllers
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region =================================================================== Service Project Talking Points Functions ===================================================================
|
||||
|
||||
#endregion
|
||||
|
||||
#region =================================================================== Job Tickets Functions ===================================================================
|
||||
|
||||
[HttpGet("job/list")]
|
||||
|
||||
@ -8,7 +8,7 @@ namespace Marco.Pms.Services.Service.ServiceInterfaces
|
||||
public interface IServiceProject
|
||||
{
|
||||
#region =================================================================== Service Project Functions ===================================================================
|
||||
Task<ApiResponse<object>> GetServiceProjectListAsync(int pageNumber, int pageSize, Employee loggedInEmployee, Guid tenantId);
|
||||
Task<ApiResponse<object>> GetServiceProjectListAsync(string? searchString, int pageNumber, int pageSize, Employee loggedInEmployee, Guid tenantId);
|
||||
Task<ApiResponse<object>> GetServiceProjectDetailsAsync(Guid id, Employee loggedInEmployee, Guid tenantId);
|
||||
Task<ApiResponse<object>> CreateServiceProjectAsync(ServiceProjectDto serviceProject, Employee loggedInEmployee, Guid TenantId);
|
||||
Task<ApiResponse<object>> UpdateServiceProjectAsync(Guid id, ServiceProjectDto serviceProject, Employee loggedInEmployee, Guid tenantId);
|
||||
@ -20,6 +20,10 @@ namespace Marco.Pms.Services.Service.ServiceInterfaces
|
||||
Task<ApiResponse<object>> ManageServiceProjectAllocationAsync(List<ServiceProjectAllocationDto> model, Employee loggedInEmployee, Guid tenantId);
|
||||
#endregion
|
||||
|
||||
#region =================================================================== Service Project Talking Points Functions ===================================================================
|
||||
|
||||
#endregion
|
||||
|
||||
#region =================================================================== Job Tickets Functions ===================================================================
|
||||
Task<ApiResponse<object>> GetJobTicketsListAsync(Guid? projectId, int pageNumber, int pageSize, bool isActive, Employee loggedInEmployee, Guid tenantId);
|
||||
Task<ApiResponse<object>> GetJobTicketDetailsAsync(Guid id, Employee loggedInEmployee, Guid tenantId);
|
||||
|
||||
@ -33,6 +33,11 @@ namespace Marco.Pms.Services.Service
|
||||
|
||||
private readonly Guid NewStatus = Guid.Parse("32d76a02-8f44-4aa0-9b66-c3716c45a918");
|
||||
private readonly Guid AssignedStatus = Guid.Parse("cfa1886d-055f-4ded-84c6-42a2a8a14a66");
|
||||
private readonly Guid InProgressStatus = Guid.Parse("5a6873a5-fed7-4745-a52f-8f61bf3bd72d");
|
||||
private readonly Guid ReviewStatus = Guid.Parse("aab71020-2fb8-44d9-9430-c9a7e9bf33b0");
|
||||
private readonly Guid DoneStatus = Guid.Parse("ed10ab57-dbaa-4ca5-8ecd-56745dcbdbd7");
|
||||
private readonly Guid ClosedStatus = Guid.Parse("3ddeefb5-ae3c-4e10-a922-35e0a452bb69");
|
||||
private readonly Guid OnHoldStatus = Guid.Parse("75a0c8b8-9c6a-41af-80bf-b35bab722eb2");
|
||||
|
||||
public ServiceProjectService(IDbContextFactory<ApplicationDbContext> dbContextFactory,
|
||||
IServiceScopeFactory serviceScopeFactory,
|
||||
@ -51,19 +56,21 @@ namespace Marco.Pms.Services.Service
|
||||
|
||||
#region =================================================================== Service Project Functions ===================================================================
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves a paginated list of active service projects including their clients, status, creators, and related services for a given tenant.
|
||||
/// Retrieves a paginated list of active service projects for a tenant, including related services, job counts, and team member information.
|
||||
/// </summary>
|
||||
/// <param name="pageNumber">Page number (1-based) for pagination.</param>
|
||||
/// <param name="pageSize">Number of records per page.</param>
|
||||
/// <param name="loggedInEmployee">Employee making the request (for logging).</param>
|
||||
/// <param name="tenantId">Tenant identifier for multi-tenant isolation.</param>
|
||||
/// <returns>ApiResponse containing paged service projects with related data or error information.</returns>
|
||||
public async Task<ApiResponse<object>> GetServiceProjectListAsync(int pageNumber, int pageSize, Employee loggedInEmployee, Guid tenantId)
|
||||
/// <param name="searchString">Optional search string to filter projects by name.</param>
|
||||
/// <param name="pageNumber">The page number starting from 1.</param>
|
||||
/// <param name="pageSize">The number of items per page.</param>
|
||||
/// <param name="loggedInEmployee">Currently authenticated employee making the request.</param>
|
||||
/// <param name="tenantId">Tenant unique identifier for multi-tenant data isolation.</param>
|
||||
/// <returns>Returns an ApiResponse containing paginated projects data or error details.</returns>
|
||||
public async Task<ApiResponse<object>> GetServiceProjectListAsync(string? searchString, int pageNumber, int pageSize, Employee loggedInEmployee, Guid tenantId)
|
||||
{
|
||||
if (tenantId == Guid.Empty)
|
||||
{
|
||||
_logger.LogWarning("GetServiceProjectListAsync called with invalid tenant context by employee {EmployeeId}", loggedInEmployee.Id);
|
||||
_logger.LogWarning("Invalid tenant context in GetServiceProjectListAsync invoked by EmployeeId {EmployeeId}", loggedInEmployee.Id);
|
||||
return ApiResponse<object>.ErrorResponse("Access Denied", "Invalid tenant context.", 403);
|
||||
}
|
||||
|
||||
@ -75,17 +82,26 @@ namespace Marco.Pms.Services.Service
|
||||
|
||||
try
|
||||
{
|
||||
// Base query for active projects scoped by tenant including necessary related entities
|
||||
// Base query for active service projects with tenant isolation and necessary eager loading.
|
||||
var serviceProjectQuery = _context.ServiceProjects
|
||||
.Include(sp => sp.Client)
|
||||
.Include(sp => sp.Status)
|
||||
.Include(sp => sp.CreatedBy).ThenInclude(e => e!.JobRole)
|
||||
.Where(sp => sp.TenantId == tenantId && sp.IsActive);
|
||||
|
||||
// Apply search filter if provided (case-insensitive)
|
||||
if (!string.IsNullOrWhiteSpace(searchString))
|
||||
{
|
||||
var normalizedSearch = searchString.Trim().ToLowerInvariant();
|
||||
serviceProjectQuery = serviceProjectQuery
|
||||
.Where(sp => sp.Name.ToLower().Contains(normalizedSearch));
|
||||
}
|
||||
|
||||
// Calculate total count and pages for pagination metadata
|
||||
var totalEntities = await serviceProjectQuery.CountAsync();
|
||||
var totalPages = (int)Math.Ceiling((double)totalEntities / pageSize);
|
||||
|
||||
// Fetch paged projects ordered by creation date descending
|
||||
// Fetch projects for the requested page with ordering by newest
|
||||
var serviceProjects = await serviceProjectQuery
|
||||
.OrderByDescending(sp => sp.CreatedAt)
|
||||
.Skip((pageNumber - 1) * pageSize)
|
||||
@ -94,15 +110,64 @@ namespace Marco.Pms.Services.Service
|
||||
|
||||
var serviceProjectIds = serviceProjects.Select(sp => sp.Id).ToList();
|
||||
|
||||
// Load related service mappings with services for current page projects (avoid N+1)
|
||||
// Load related services in a single query to prevent N+1 issue
|
||||
var serviceProjectServiceMappings = await _context.ServiceProjectServiceMapping
|
||||
.Include(sps => sps.Service)
|
||||
.Where(sps => serviceProjectIds.Contains(sps.ProjectId) &&
|
||||
sps.Service != null &&
|
||||
sps.TenantId == tenantId)
|
||||
.Where(sps => serviceProjectIds.Contains(sps.ProjectId) && sps.Service != null && sps.TenantId == tenantId)
|
||||
.ToListAsync();
|
||||
|
||||
// Map each project with its related services into the view models
|
||||
// Execute related aggregate counts in parallel with separate contexts and async queries
|
||||
var jobTask = Task.Run(async () =>
|
||||
{
|
||||
await using var context = await _dbContextFactory.CreateDbContextAsync();
|
||||
return await context.JobTickets
|
||||
.Where(jt => serviceProjectIds.Contains(jt.ProjectId) && jt.TenantId == tenantId && jt.IsActive)
|
||||
.GroupBy(jt => jt.ProjectId)
|
||||
.Select(g => new
|
||||
{
|
||||
ProjectId = g.Key,
|
||||
JobsPassedDueDateCount = g.Count(jt => jt.DueDate.Date < DateTime.UtcNow.Date),
|
||||
ActiveJobsCount = g.Count(jt => jt.StatusId == AssignedStatus || jt.StatusId == InProgressStatus || jt.StatusId == ReviewStatus)
|
||||
})
|
||||
.ToListAsync();
|
||||
});
|
||||
|
||||
var teamMemberTask = Task.Run(async () =>
|
||||
{
|
||||
await using var context = await _dbContextFactory.CreateDbContextAsync();
|
||||
return await context.ServiceProjectAllocations
|
||||
.Where(spa => serviceProjectIds.Contains(spa.ProjectId) && spa.TenantId == tenantId && spa.IsActive)
|
||||
.GroupBy(spa => spa.ProjectId)
|
||||
.Select(g => new
|
||||
{
|
||||
ProjectId = g.Key,
|
||||
TeamMemberCount = g.Select(spa => spa.EmployeeId).Distinct().Count()
|
||||
})
|
||||
.ToListAsync();
|
||||
});
|
||||
|
||||
var jobMembersTask = Task.Run(async () =>
|
||||
{
|
||||
await using var context = await _dbContextFactory.CreateDbContextAsync();
|
||||
return await context.JobEmployeeMappings
|
||||
.Include(jem => jem.JobTicket)
|
||||
.Where(jem => jem.JobTicket != null && serviceProjectIds.Contains(jem.JobTicket.ProjectId) && jem.TenantId == tenantId)
|
||||
.GroupBy(jem => jem.JobTicket!.ProjectId)
|
||||
.Select(g => new
|
||||
{
|
||||
ProjectId = g.Key,
|
||||
JobMembersCount = g.Select(jem => jem.AssigneeId).Distinct().Count()
|
||||
})
|
||||
.ToListAsync();
|
||||
});
|
||||
|
||||
await Task.WhenAll(jobTask, jobMembersTask, teamMemberTask);
|
||||
|
||||
var jobTickets = jobTask.Result;
|
||||
var jobMembers = jobMembersTask.Result;
|
||||
var teamMembers = teamMemberTask.Result;
|
||||
|
||||
// Map the service projects into view models including related data
|
||||
var serviceProjectVMs = serviceProjects.Select(sp =>
|
||||
{
|
||||
var relatedServices = serviceProjectServiceMappings
|
||||
@ -112,6 +177,10 @@ namespace Marco.Pms.Services.Service
|
||||
|
||||
var projectVm = _mapper.Map<ServiceProjectVM>(sp);
|
||||
projectVm.Services = _mapper.Map<List<ServiceMasterVM>>(relatedServices);
|
||||
projectVm.TeamMemberCount = teamMembers.FirstOrDefault(tm => tm.ProjectId == sp.Id)?.TeamMemberCount ?? 0;
|
||||
projectVm.ActiveJobsCount = jobTickets.FirstOrDefault(jt => jt.ProjectId == sp.Id)?.ActiveJobsCount ?? 0;
|
||||
projectVm.JobsPassedDueDateCount = jobTickets.FirstOrDefault(jt => jt.ProjectId == sp.Id)?.JobsPassedDueDateCount ?? 0;
|
||||
projectVm.JobMembersCount = jobMembers.FirstOrDefault(jm => jm.ProjectId == sp.Id)?.JobMembersCount ?? 0;
|
||||
return projectVm;
|
||||
}).ToList();
|
||||
|
||||
@ -120,22 +189,26 @@ namespace Marco.Pms.Services.Service
|
||||
CurrentPage = pageNumber,
|
||||
TotalPages = totalPages,
|
||||
TotalEntities = totalEntities,
|
||||
Data = serviceProjectVMs,
|
||||
Data = serviceProjectVMs
|
||||
};
|
||||
|
||||
_logger.LogInfo("Retrieved {Count} service projects for tenant {TenantId} by employee {EmployeeId}. Page {PageNumber}/{TotalPages}",
|
||||
_logger.LogInfo("Returned {Count} service projects for tenant {TenantId} requested by EmployeeId {EmployeeId} (Page {PageNumber}/{TotalPages})",
|
||||
serviceProjectVMs.Count, tenantId, loggedInEmployee.Id, pageNumber, totalPages);
|
||||
|
||||
return ApiResponse<object>.SuccessResponse(response, "Projects retrieved successfully.", 200);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "An unexpected error occurred in GetServiceProjectListAsync for tenant {TenantId} by employee {EmployeeId}",
|
||||
_logger.LogError(ex, "Error in GetServiceProjectListAsync for tenant {TenantId} invoked by EmployeeId {EmployeeId}",
|
||||
tenantId, loggedInEmployee.Id);
|
||||
return ApiResponse<object>.ErrorResponse("An internal server error occurred. Please try again later.", null, 500);
|
||||
return ApiResponse<object>.ErrorResponse(
|
||||
"Internal Server Error",
|
||||
"An unexpected error occurred while retrieving projects. Please try again later.",
|
||||
500);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves detailed information for a specific service project, including related client, status, services, and audit information.
|
||||
/// </summary>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user