Added an API to get the attendance overview project-wise
This commit is contained in:
parent
5387e009cb
commit
9c5df63134
@ -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; }
|
||||
}
|
||||
}
|
||||
@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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)
|
||||
{
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user