Added the Addition project related information in service project list API

This commit is contained in:
ashutosh.nehete 2025-11-18 13:06:45 +05:30
parent 5522551e67
commit f171b0add6
4 changed files with 107 additions and 22 deletions

View File

@ -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; }

View File

@ -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")]

View File

@ -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);

View File

@ -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>