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));
|
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)
|
.Include(e => e.Currency)
|
||||||
.Where(e => e.TenantId == tenantId); // Always filter by TenantId first.
|
.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)
|
if (cacheList == null)
|
||||||
{
|
{
|
||||||
//await _cache.AddExpensesListToCache(expenses: await expensesQuery.ToListAsync(), tenantId);
|
//await _cache.AddExpensesListToCache(expenses: await expensesQuery.ToListAsync(), tenantId);
|
||||||
|
|||||||
@ -304,7 +304,9 @@ namespace Marco.Pms.Services.Service
|
|||||||
responseVms = responseVms
|
responseVms = responseVms
|
||||||
.OrderBy(p => p.Name)
|
.OrderBy(p => p.Name)
|
||||||
.Skip((pageNumber - 1) * pageSize)
|
.Skip((pageNumber - 1) * pageSize)
|
||||||
.Take(pageSize).ToList();
|
.Take(pageSize)
|
||||||
|
.OrderBy(p => p.ShortName)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
// --- Step 4: Return the combined result ---
|
// --- 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)
|
private async Task<ProjectDetailsVM> GetProjectViewModel(Guid? id, Project project)
|
||||||
{
|
{
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user