Merge pull request 'Attendance_Weidget_feature' (#105) from Attendance_Weidget_feature into main

Reviewed-on: #105
This commit is contained in:
Vikas Nale 2025-07-17 11:50:32 +00:00
commit 3a45bded08
3 changed files with 179 additions and 35 deletions

View File

@ -21,12 +21,15 @@ namespace Marco.Pms.Services.Controllers
{ {
private readonly ApplicationDbContext _context; private readonly ApplicationDbContext _context;
private readonly UserHelper _userHelper; private readonly UserHelper _userHelper;
private readonly ProjectsHelper _projectsHelper;
private readonly ILoggingService _logger; private readonly ILoggingService _logger;
private readonly PermissionServices _permissionServices; private readonly PermissionServices _permissionServices;
public DashboardController(ApplicationDbContext context, UserHelper userHelper, ILoggingService logger, PermissionServices permissionServices) public static readonly Guid ActiveId = Guid.Parse("b74da4c2-d07e-46f2-9919-e75e49b12731");
public DashboardController(ApplicationDbContext context, UserHelper userHelper, ProjectsHelper projectsHelper, ILoggingService logger, PermissionServices permissionServices)
{ {
_context = context; _context = context;
_userHelper = userHelper; _userHelper = userHelper;
_projectsHelper = projectsHelper;
_logger = logger; _logger = logger;
_permissionServices = permissionServices; _permissionServices = permissionServices;
} }
@ -162,46 +165,186 @@ namespace Marco.Pms.Services.Controllers
return Ok(ApiResponse<object>.SuccessResponse(projectDashboardVM, "Success", 200)); return Ok(ApiResponse<object>.SuccessResponse(projectDashboardVM, "Success", 200));
} }
/// <summary>
/// Retrieves a dashboard summary of total employees and today's attendance.
/// If a projectId is provided, it returns totals for that project; otherwise, for all accessible active projects.
/// </summary>
/// <param name="projectId">Optional. The ID of a specific project to get totals for.</param>
[HttpGet("teams")] [HttpGet("teams")]
public async Task<IActionResult> GetTotalEmployees() public async Task<IActionResult> GetTotalEmployees([FromQuery] Guid? projectId)
{
try
{ {
var tenantId = _userHelper.GetTenantId(); var tenantId = _userHelper.GetTenantId();
var LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
var date = DateTime.UtcNow.Date;
var Employees = await _context.Employees.Where(e => e.TenantId == tenantId && e.IsActive == true).Select(e => e.Id).ToListAsync(); _logger.LogInfo("GetTotalEmployees called by user {UserId} for ProjectId: {ProjectId}", loggedInEmployee.Id, projectId ?? Guid.Empty);
var checkedInEmployee = await _context.Attendes.Where(e => e.InTime != null ? e.InTime.Value.Date == date : false).Select(e => e.EmployeeID).ToListAsync(); // --- Step 1: Get the list of projects the user can access ---
// This query is more efficient as it only selects the IDs needed.
TeamDashboardVM teamDashboardVM = new TeamDashboardVM var projects = await _projectsHelper.GetMyProjects(tenantId, loggedInEmployee);
var accessibleActiveProjectIds = projects
.Where(p => p.ProjectStatusId == ActiveId)
.Select(p => p.Id)
.ToList();
if (!accessibleActiveProjectIds.Any())
{ {
TotalEmployees = Employees.Count(), _logger.LogInfo("User {UserId} has no accessible active projects.", loggedInEmployee.Id);
InToday = checkedInEmployee.Distinct().Count() return Ok(ApiResponse<TeamDashboardVM>.SuccessResponse(new TeamDashboardVM(), "No accessible active projects found.", 200));
};
_logger.LogInfo("Today's total checked in employees fetched by employee {EmployeeId}", LoggedInEmployee.Id);
return Ok(ApiResponse<object>.SuccessResponse(teamDashboardVM, "Success", 200));
} }
[HttpGet("tasks")] // --- Step 2: Build the list of project IDs to query against ---
public async Task<IActionResult> GetTotalTasks() List<Guid> finalProjectIds;
if (projectId.HasValue)
{
// Security Check: Ensure the requested project is in the user's accessible list.
var hasPermission = await _permissionServices.HasProjectPermission(loggedInEmployee, projectId.Value.ToString());
if (!hasPermission)
{
_logger.LogWarning("Access DENIED for user {UserId} on project {ProjectId} (not active or not accessible).", loggedInEmployee.Id, projectId.Value);
return StatusCode(403, ApiResponse<object>.ErrorResponse("Access Denied.", "You do not have permission to view this project, or it is not active.", 403));
}
finalProjectIds = new List<Guid> { projectId.Value };
}
else
{
finalProjectIds = accessibleActiveProjectIds;
}
// --- Step 3: Run efficient aggregation queries SEQUENTIALLY ---
// Since we only have one DbContext instance, we await each query one by one.
// Query 1: Count total distinct employees allocated to the final project list
int totalEmployees = await _context.ProjectAllocations
.Where(pa => pa.TenantId == tenantId &&
finalProjectIds.Contains(pa.ProjectId) &&
pa.IsActive)
.Select(pa => pa.EmployeeId)
.Distinct()
.CountAsync();
// Query 2: Count total distinct employees who checked in today
// Use an efficient date range check
var today = DateTime.UtcNow.Date;
var tomorrow = today.AddDays(1);
int inTodays = await _context.Attendes
.Where(a => a.InTime >= today && a.InTime < tomorrow &&
finalProjectIds.Contains(a.ProjectID))
.Select(a => a.EmployeeID)
.Distinct()
.CountAsync();
// --- Step 4: Assemble the response ---
var teamDashboardVM = new TeamDashboardVM
{
TotalEmployees = totalEmployees,
InToday = inTodays
};
_logger.LogInfo("Successfully fetched team dashboard for user {UserId}. Total: {TotalEmployees}, InToday: {InToday}",
loggedInEmployee.Id, teamDashboardVM.TotalEmployees, teamDashboardVM.InToday);
return Ok(ApiResponse<TeamDashboardVM>.SuccessResponse(teamDashboardVM, "Dashboard data retrieved successfully.", 200));
}
catch (Exception ex)
{
_logger.LogError("An unexpected error occurred in GetTotalEmployees for projectId {ProjectId} \n {Error}", projectId ?? Guid.Empty, ex.Message);
return StatusCode(500, ApiResponse<object>.ErrorResponse("An internal server error occurred.", null, 500));
}
}
/// <summary>
/// Retrieves a dashboard summary of total planned and completed tasks.
/// If a projectId is provided, it returns totals for that project; otherwise, for all accessible projects.
/// </summary>
/// <param name="projectId">Optional. The ID of a specific project to get totals for.</param>
/// <returns>An ApiResponse containing the task dashboard summary.</returns>
[HttpGet("tasks")] // Example route
public async Task<IActionResult> GetTotalTasks1([FromQuery] Guid? projectId) // Changed to FromQuery as it's optional
{
try
{ {
var tenantId = _userHelper.GetTenantId(); var tenantId = _userHelper.GetTenantId();
var LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
var Tasks = await _context.WorkItems.Where(t => t.TenantId == tenantId).Select(t => new { PlannedWork = t.PlannedWork, CompletedWork = t.CompletedWork }).ToListAsync();
TasksDashboardVM tasksDashboardVM = new TasksDashboardVM _logger.LogInfo("GetTotalTasks called by user {UserId} for ProjectId: {ProjectId}", loggedInEmployee.Id, projectId ?? Guid.Empty);
// --- Step 1: Build the base IQueryable for WorkItems ---
// This query is NOT executed yet. We will add more filters to it.
var baseWorkItemQuery = _context.WorkItems.Where(t => t.TenantId == tenantId);
// --- Step 2: Apply Filters based on the request (Project or All Accessible) ---
if (projectId.HasValue)
{ {
TotalTasks = 0, // --- Logic for a SINGLE Project ---
CompletedTasks = 0
}; // 2a. Security Check: Verify permission for the specific project.
foreach (var task in Tasks) var hasPermission = await _permissionServices.HasProjectPermission(loggedInEmployee, projectId.Value.ToString());
if (!hasPermission)
{ {
tasksDashboardVM.TotalTasks += task.PlannedWork; _logger.LogWarning("Access DENIED for user {UserId} on project {ProjectId}.", loggedInEmployee.Id, projectId.Value);
tasksDashboardVM.CompletedTasks += task.CompletedWork; return StatusCode(403, ApiResponse<object>.ErrorResponse("Access Denied.", "You do not have permission to view this project.", 403));
}
_logger.LogInfo("Total targeted tasks and total completed tasks fetched by employee {EmployeeId}", LoggedInEmployee.Id);
return Ok(ApiResponse<object>.SuccessResponse(tasksDashboardVM, "Success", 200));
} }
// 2b. Add project-specific filter to the base query.
// This is more efficient than fetching workAreaIds separately.
baseWorkItemQuery = baseWorkItemQuery
.Where(wi => wi.WorkArea != null &&
wi.WorkArea.Floor != null &&
wi.WorkArea.Floor.Building != null &&
wi.WorkArea.Floor.Building.ProjectId == projectId.Value);
}
else
{
// --- Logic for ALL Accessible Projects ---
// 2c. Get a list of all projects the user is allowed to see.
var accessibleProject = await _projectsHelper.GetMyProjects(tenantId, loggedInEmployee);
var accessibleProjectIds = accessibleProject.Select(p => p.Id).ToList();
if (!accessibleProjectIds.Any())
{
_logger.LogInfo("User {UserId} has no accessible projects.", loggedInEmployee.Id);
// Return a zeroed-out dashboard if the user has no projects.
return Ok(ApiResponse<TasksDashboardVM>.SuccessResponse(new TasksDashboardVM(), "No accessible projects found.", 200));
}
// 2d. Add a filter to include all work items from all accessible projects.
baseWorkItemQuery = baseWorkItemQuery
.Where(wi => wi.WorkArea != null &&
wi.WorkArea.Floor != null &&
wi.WorkArea.Floor.Building != null &&
accessibleProjectIds.Contains(wi.WorkArea.Floor.Building.ProjectId));
}
// --- Step 3: Execute the Aggregation Query ON THE DATABASE SERVER ---
// This is the most powerful optimization. The database does all the summing.
// EF Core translates this into a single, efficient SQL query like:
// SELECT SUM(PlannedWork), SUM(CompletedWork) FROM WorkItems WHERE ...
var tasksDashboardVM = await baseWorkItemQuery
.GroupBy(x => 1) // Group by a constant to aggregate all rows into one result.
.Select(g => new TasksDashboardVM
{
TotalTasks = g.Sum(wi => wi.PlannedWork),
CompletedTasks = g.Sum(wi => wi.CompletedWork)
})
.FirstOrDefaultAsync(); // Use FirstOrDefaultAsync as GroupBy might return no rows.
// If the query returned no work items, the result will be null. Default to a zeroed object.
tasksDashboardVM ??= new TasksDashboardVM();
_logger.LogInfo("Successfully fetched task dashboard for user {UserId}. Total: {TotalTasks}, Completed: {CompletedTasks}",
loggedInEmployee.Id, tasksDashboardVM.TotalTasks, tasksDashboardVM.CompletedTasks);
return Ok(ApiResponse<TasksDashboardVM>.SuccessResponse(tasksDashboardVM, "Dashboard data retrieved successfully.", 200));
}
catch (Exception ex)
{
_logger.LogError("An unexpected error occurred in GetTotalTasks for projectId {ProjectId} \n {Error}", projectId ?? Guid.Empty, ex.Message);
return StatusCode(500, ApiResponse<object>.ErrorResponse("An internal server error occurred.", null, 500));
}
}
[HttpGet("pending-attendance")] [HttpGet("pending-attendance")]
public async Task<IActionResult> GetPendingAttendance() public async Task<IActionResult> GetPendingAttendance()
{ {

View File

@ -1086,7 +1086,7 @@ namespace Marco.Pms.Services.Helpers
var LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); var LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
if (noteDto != null && id == noteDto.Id) if (noteDto != null && id == noteDto.Id)
{ {
Contact? contact = await _context.Contacts.FirstOrDefaultAsync(c => c.Id == noteDto.ContactId && c.IsActive && c.TenantId == tenantId); Contact? contact = await _context.Contacts.FirstOrDefaultAsync(c => c.Id == noteDto.ContactId && c.TenantId == tenantId);
if (contact != null) if (contact != null)
{ {
ContactNote? contactNote = await _context.ContactNotes.Include(cn => cn.Createdby).Include(cn => cn.Contact).FirstOrDefaultAsync(n => n.Id == noteDto.Id && n.ContactId == contact.Id && n.IsActive); ContactNote? contactNote = await _context.ContactNotes.Include(cn => cn.Createdby).Include(cn => cn.Contact).FirstOrDefaultAsync(n => n.Id == noteDto.Id && n.ContactId == contact.Id && n.IsActive);

View File

@ -81,6 +81,7 @@ namespace MarcoBMS.Services.Helpers
result = await _context.ProjectAllocations result = await _context.ProjectAllocations
.Include(pa => pa.Employee) .Include(pa => pa.Employee)
.ThenInclude(e => e!.JobRole)
.Where(c => c.ProjectId == ProjectId.Value && c.IsActive && c.Employee != null) .Where(c => c.ProjectId == ProjectId.Value && c.IsActive && c.Employee != null)
.Select(pa => pa.Employee!.ToEmployeeVMFromEmployee()) .Select(pa => pa.Employee!.ToEmployeeVMFromEmployee())
.ToListAsync(); .ToListAsync();