Added an API to get the attendance overview project-wise

This commit is contained in:
ashutosh.nehete 2025-12-10 10:42:05 +05:30
parent 5387e009cb
commit 9c5df63134
4 changed files with 136 additions and 2 deletions

View File

@ -0,0 +1,10 @@
namespace Marco.Pms.Model.ViewModels.DashBoard
{
public class ProjectAttendanceOverviewVM
{
public Guid ProjectId { get; set; }
public string? ProjectName { get; set; }
public int TeamCount { get; set; }
public int AttendanceCount { get; set; }
}
}

View File

@ -1707,5 +1707,122 @@ namespace Marco.Pms.Services.Controllers
return Ok(ApiResponse<object>.SuccessResponse(response, "job progression fetched successfully", 200));
}
[HttpGet("project/attendance-overview")]
public async Task<IActionResult> GetProjectAttendanceOverViewAsync([FromQuery] DateTime? date, CancellationToken cancellationToken)
{
// 1. Validation and Setup
if (tenantId == Guid.Empty)
{
_logger.LogWarning("GetProjectAttendanceOverView: Invalid request - TenantId is empty.");
return BadRequest(ApiResponse<object>.ErrorResponse("Invalid TenantId", "Provided Invalid TenantId", 400));
}
// Default to UTC Today if null, ensuring only Date component is used
var targetDate = date?.Date ?? DateTime.UtcNow.Date;
_logger.LogInfo("GetProjectAttendanceOverView: Starting fetch for Tenant {TenantId} on Date {Date}", tenantId, targetDate);
try
{
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
if (loggedInEmployee == null)
{
_logger.LogWarning("GetProjectAttendanceOverView: Employee not found for current user.");
return Unauthorized(ApiResponse<object>.ErrorResponse("Unauthorized", "Employee profile not found.", 401));
}
// 2. Permission Check
using var scope = _serviceScopeFactory.CreateScope();
var _permission = scope.ServiceProvider.GetRequiredService<PermissionServices>();
var hasPermission = await _permission.HasPermission(PermissionsMaster.ManageProject, loggedInEmployee.Id);
// 3. Determine Scope of Projects (Filtering Project IDs)
// We select only the IDs first to keep the memory footprint low before aggregation
var projectQuery = _context.ProjectAllocations
.AsNoTracking()
.Where(pa => pa.TenantId == tenantId && pa.IsActive);
if (!hasPermission)
{
// If no admin permission, restrict to projects the employee is allocated to
projectQuery = projectQuery.Where(pa => pa.EmployeeId == loggedInEmployee.Id);
}
var visibleProjectIds = await projectQuery
.Select(pa => pa.ProjectId)
.Distinct()
.ToListAsync(cancellationToken);
if (!visibleProjectIds.Any())
{
return Ok(ApiResponse<List<ProjectAttendanceOverviewVM>>.SuccessResponse(new List<ProjectAttendanceOverviewVM>(), "No projects found.", 200));
}
// 4. Parallel Data Fetching (Optimization)
// We fetch Project Details/Allocations AND Attendance counts separately to avoid complex Cartesian products in SQL
// Query A: Get Project Details and Total Allocation Counts
var projectsTask = _context.ProjectAllocations
.AsNoTracking()
.Where(pa => pa.TenantId == tenantId &&
pa.IsActive &&
visibleProjectIds.Contains(pa.ProjectId) &&
pa.Project != null)
.GroupBy(pa => new { pa.ProjectId, pa.Project!.Name })
.Select(g => new
{
ProjectId = g.Key.ProjectId,
Name = g.Key.Name,
TeamCount = g.Count()
})
.ToListAsync(cancellationToken);
// Query B: Get Attendance Counts for the specific date
await using var context = await _dbContextFactory.CreateDbContextAsync();
var attendanceTask = context.Attendes
.AsNoTracking()
.Where(a => a.TenantId == tenantId &&
visibleProjectIds.Contains(a.ProjectID) &&
a.AttendanceDate.Date == targetDate)
.GroupBy(a => a.ProjectID)
.Select(g => new
{
ProjectId = g.Key,
Count = g.Count()
})
.ToDictionaryAsync(k => k.ProjectId, v => v.Count, cancellationToken);
await Task.WhenAll(projectsTask, attendanceTask);
var projects = await projectsTask;
var attendanceMap = await attendanceTask;
// 5. In-Memory Projection
// Merging the two datasets efficiently
var response = projects.Select(p => new ProjectAttendanceOverviewVM
{
ProjectId = p.ProjectId,
ProjectName = p.Name,
TeamCount = p.TeamCount,
// O(1) Lookup from the dictionary
AttendanceCount = attendanceMap.ContainsKey(p.ProjectId) ? attendanceMap[p.ProjectId] : 0
})
.OrderBy(p => p.ProjectName)
.ToList();
_logger.LogInfo("GetProjectAttendanceOverView: Successfully fetched {Count} projects for Tenant {TenantId}", response.Count, tenantId);
return Ok(ApiResponse<List<ProjectAttendanceOverviewVM>>.SuccessResponse(response, "Attendance overview fetched successfully", 200));
}
catch (Exception ex)
{
_logger.LogError(ex, "GetProjectAttendanceOverView: An unexpected error occurred for Tenant {TenantId}", tenantId);
// Do not expose raw Exception details to client in production
return StatusCode(500, ApiResponse<object>.ErrorResponse("Internal Server Error", "An error occurred while processing your request.", 500));
}
}
}
}

View File

@ -153,6 +153,11 @@ namespace Marco.Pms.Services.Service
.Include(e => e.Currency)
.Where(e => e.TenantId == tenantId); // Always filter by TenantId first.
//using var scope = _serviceScopeFactory.CreateScope();
//var _projectServices = scope.ServiceProvider.GetRequiredService<IProjectServices>();
//var allprojectIds = await _projectServices.GetBothProjectIdsAsync(loggedInEmployee.Id, tenantId);
if (cacheList == null)
{
//await _cache.AddExpensesListToCache(expenses: await expensesQuery.ToListAsync(), tenantId);

View File

@ -304,7 +304,9 @@ namespace Marco.Pms.Services.Service
responseVms = responseVms
.OrderBy(p => p.Name)
.Skip((pageNumber - 1) * pageSize)
.Take(pageSize).ToList();
.Take(pageSize)
.OrderBy(p => p.ShortName)
.ToList();
// --- Step 4: Return the combined result ---
@ -3267,7 +3269,7 @@ namespace Marco.Pms.Services.Service
}
}
return finalViewModels;
return finalViewModels.OrderBy(p => p.Name).ToList();
}
private async Task<ProjectDetailsVM> GetProjectViewModel(Guid? id, Project project)
{