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 StatusMaster? Status { get; set; }
|
||||||
public BasicOrganizationVm? Client { get; set; }
|
public BasicOrganizationVm? Client { get; set; }
|
||||||
public List<ServiceMasterVM>? Services { 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? ContactName { get; set; }
|
||||||
public string? ContactPhone { get; set; }
|
public string? ContactPhone { get; set; }
|
||||||
public string? ContactEmail { get; set; }
|
public string? ContactEmail { get; set; }
|
||||||
|
|||||||
@ -36,10 +36,10 @@ namespace Marco.Pms.Services.Controllers
|
|||||||
#region =================================================================== Service Project Functions ===================================================================
|
#region =================================================================== Service Project Functions ===================================================================
|
||||||
|
|
||||||
[HttpGet("list")]
|
[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();
|
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);
|
return StatusCode(response.StatusCode, response);
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -120,6 +120,10 @@ namespace Marco.Pms.Services.Controllers
|
|||||||
}
|
}
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
|
#region =================================================================== Service Project Talking Points Functions ===================================================================
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
#region =================================================================== Job Tickets Functions ===================================================================
|
#region =================================================================== Job Tickets Functions ===================================================================
|
||||||
|
|
||||||
[HttpGet("job/list")]
|
[HttpGet("job/list")]
|
||||||
|
|||||||
@ -8,7 +8,7 @@ namespace Marco.Pms.Services.Service.ServiceInterfaces
|
|||||||
public interface IServiceProject
|
public interface IServiceProject
|
||||||
{
|
{
|
||||||
#region =================================================================== Service Project Functions ===================================================================
|
#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>> GetServiceProjectDetailsAsync(Guid id, Employee loggedInEmployee, Guid tenantId);
|
||||||
Task<ApiResponse<object>> CreateServiceProjectAsync(ServiceProjectDto serviceProject, 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);
|
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);
|
Task<ApiResponse<object>> ManageServiceProjectAllocationAsync(List<ServiceProjectAllocationDto> model, Employee loggedInEmployee, Guid tenantId);
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
|
#region =================================================================== Service Project Talking Points Functions ===================================================================
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
#region =================================================================== Job Tickets Functions ===================================================================
|
#region =================================================================== Job Tickets Functions ===================================================================
|
||||||
Task<ApiResponse<object>> GetJobTicketsListAsync(Guid? projectId, int pageNumber, int pageSize, bool isActive, Employee loggedInEmployee, Guid tenantId);
|
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);
|
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 NewStatus = Guid.Parse("32d76a02-8f44-4aa0-9b66-c3716c45a918");
|
||||||
private readonly Guid AssignedStatus = Guid.Parse("cfa1886d-055f-4ded-84c6-42a2a8a14a66");
|
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,
|
public ServiceProjectService(IDbContextFactory<ApplicationDbContext> dbContextFactory,
|
||||||
IServiceScopeFactory serviceScopeFactory,
|
IServiceScopeFactory serviceScopeFactory,
|
||||||
@ -51,19 +56,21 @@ namespace Marco.Pms.Services.Service
|
|||||||
|
|
||||||
#region =================================================================== Service Project Functions ===================================================================
|
#region =================================================================== Service Project Functions ===================================================================
|
||||||
|
|
||||||
|
|
||||||
/// <summary>
|
/// <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>
|
/// </summary>
|
||||||
/// <param name="pageNumber">Page number (1-based) for pagination.</param>
|
/// <param name="searchString">Optional search string to filter projects by name.</param>
|
||||||
/// <param name="pageSize">Number of records per page.</param>
|
/// <param name="pageNumber">The page number starting from 1.</param>
|
||||||
/// <param name="loggedInEmployee">Employee making the request (for logging).</param>
|
/// <param name="pageSize">The number of items per page.</param>
|
||||||
/// <param name="tenantId">Tenant identifier for multi-tenant isolation.</param>
|
/// <param name="loggedInEmployee">Currently authenticated employee making the request.</param>
|
||||||
/// <returns>ApiResponse containing paged service projects with related data or error information.</returns>
|
/// <param name="tenantId">Tenant unique identifier for multi-tenant data isolation.</param>
|
||||||
public async Task<ApiResponse<object>> GetServiceProjectListAsync(int pageNumber, int pageSize, Employee loggedInEmployee, Guid tenantId)
|
/// <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)
|
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);
|
return ApiResponse<object>.ErrorResponse("Access Denied", "Invalid tenant context.", 403);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -75,17 +82,26 @@ namespace Marco.Pms.Services.Service
|
|||||||
|
|
||||||
try
|
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
|
var serviceProjectQuery = _context.ServiceProjects
|
||||||
.Include(sp => sp.Client)
|
.Include(sp => sp.Client)
|
||||||
.Include(sp => sp.Status)
|
.Include(sp => sp.Status)
|
||||||
.Include(sp => sp.CreatedBy).ThenInclude(e => e!.JobRole)
|
.Include(sp => sp.CreatedBy).ThenInclude(e => e!.JobRole)
|
||||||
.Where(sp => sp.TenantId == tenantId && sp.IsActive);
|
.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 totalEntities = await serviceProjectQuery.CountAsync();
|
||||||
var totalPages = (int)Math.Ceiling((double)totalEntities / pageSize);
|
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
|
var serviceProjects = await serviceProjectQuery
|
||||||
.OrderByDescending(sp => sp.CreatedAt)
|
.OrderByDescending(sp => sp.CreatedAt)
|
||||||
.Skip((pageNumber - 1) * pageSize)
|
.Skip((pageNumber - 1) * pageSize)
|
||||||
@ -94,15 +110,64 @@ namespace Marco.Pms.Services.Service
|
|||||||
|
|
||||||
var serviceProjectIds = serviceProjects.Select(sp => sp.Id).ToList();
|
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
|
var serviceProjectServiceMappings = await _context.ServiceProjectServiceMapping
|
||||||
.Include(sps => sps.Service)
|
.Include(sps => sps.Service)
|
||||||
.Where(sps => serviceProjectIds.Contains(sps.ProjectId) &&
|
.Where(sps => serviceProjectIds.Contains(sps.ProjectId) && sps.Service != null && sps.TenantId == tenantId)
|
||||||
sps.Service != null &&
|
|
||||||
sps.TenantId == tenantId)
|
|
||||||
.ToListAsync();
|
.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 serviceProjectVMs = serviceProjects.Select(sp =>
|
||||||
{
|
{
|
||||||
var relatedServices = serviceProjectServiceMappings
|
var relatedServices = serviceProjectServiceMappings
|
||||||
@ -112,6 +177,10 @@ namespace Marco.Pms.Services.Service
|
|||||||
|
|
||||||
var projectVm = _mapper.Map<ServiceProjectVM>(sp);
|
var projectVm = _mapper.Map<ServiceProjectVM>(sp);
|
||||||
projectVm.Services = _mapper.Map<List<ServiceMasterVM>>(relatedServices);
|
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;
|
return projectVm;
|
||||||
}).ToList();
|
}).ToList();
|
||||||
|
|
||||||
@ -120,22 +189,26 @@ namespace Marco.Pms.Services.Service
|
|||||||
CurrentPage = pageNumber,
|
CurrentPage = pageNumber,
|
||||||
TotalPages = totalPages,
|
TotalPages = totalPages,
|
||||||
TotalEntities = totalEntities,
|
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);
|
serviceProjectVMs.Count, tenantId, loggedInEmployee.Id, pageNumber, totalPages);
|
||||||
|
|
||||||
return ApiResponse<object>.SuccessResponse(response, "Projects retrieved successfully.", 200);
|
return ApiResponse<object>.SuccessResponse(response, "Projects retrieved successfully.", 200);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
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);
|
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>
|
/// <summary>
|
||||||
/// Retrieves detailed information for a specific service project, including related client, status, services, and audit information.
|
/// Retrieves detailed information for a specific service project, including related client, status, services, and audit information.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user