Compare commits

..

34 Commits

Author SHA1 Message Date
0ac4c23e74 Merge branch 'Ashutosh_Refactor' of https://git.marcoaiot.com/admin/marco.pms.api into Ashutosh_Refactor 2025-07-17 17:23:08 +05:30
b71935dd1f Removed commented code from project Cache 2025-07-17 17:23:02 +05:30
c8978ee9b1 added new function delete all employee entries from cache 2025-07-17 17:23:02 +05:30
0ecf258661 Deleted the unused variable 2025-07-17 17:23:01 +05:30
30d614fa11 Added the logs setp in program.cs 2025-07-17 17:23:01 +05:30
5deb97d73b Added one more condition to check if active is false while removing the employee from buckets 2025-07-17 17:23:01 +05:30
6ac28de56a Removed the reassgining of same object 2025-07-17 17:23:01 +05:30
8735de3d93 Remove the projectHelper and ProjetsHelper and move its bussiness logic to project services 2025-07-17 17:23:01 +05:30
c8ca2d5c49 Optimization of WorkItem Delete API in Project Controller 2025-07-17 17:23:00 +05:30
eabd31f8cf Optimized the Manage infra API in Project Controller 2025-07-17 17:23:00 +05:30
57b7f941e6 Optimized the manage task API in projectController 2025-07-17 17:23:00 +05:30
c90f39082a Optimized the project allocation by employee Id Apis 2025-07-17 17:23:00 +05:30
c03fae4b65 Added Sonar files in git ignore 2025-07-17 17:23:00 +05:30
168922c278 Optimized the Project Allocation API 2025-07-17 17:22:59 +05:30
72dccc0c6a Added Employee ID of creater to bucket in Employee IDs 2025-07-17 17:22:59 +05:30
7914cf20d4 Removed unused code from employee cache class 2025-07-17 17:22:59 +05:30
80149f05f7 Solved the issue of project is not updating properly 2025-07-17 17:22:59 +05:30
560d2f2d4d adde functionality to delete workItems from cache 2025-07-17 17:22:59 +05:30
f4ca7670e3 Refactored: Moved business logic from ProjectController to ProjectService 2025-07-17 17:22:58 +05:30
36eb7aef7f Optimized the Update project API 2025-07-17 17:22:58 +05:30
ca34b01ab0 Optimized the Get project By ID API 2025-07-17 17:22:58 +05:30
b78f58c304 Solved Concurrency Issue 2025-07-17 17:22:58 +05:30
c359212ee5 Optimized both get Project list API and get Project list basic API 2025-07-17 17:22:58 +05:30
7d160a9a52 Refactored the function to add project in cache and added auto Mapper 2025-07-17 17:22:57 +05:30
3bc51f9cd9 Refactor project report APIs to improve performance and readability 2025-07-17 17:22:57 +05:30
3a45bded08 Merge pull request 'Attendance_Weidget_feature' (#105) from Attendance_Weidget_feature into main
Reviewed-on: #105
2025-07-17 11:50:32 +00:00
0ec507c97c Included the jobrole when feaching employee list 2025-07-17 15:31:05 +05:30
d1106bc86b Can able update note of deleted not also 2025-07-17 12:01:47 +05:30
0859284f4a Merge branch 'Attendance_Weidget_feature' of https://git.marcoaiot.com/admin/marco.pms.api into Attendance_Weidget_feature 2025-07-16 18:00:27 +05:30
bbd2054867 only checking if the user have permission of project or not only 2025-07-16 18:00:15 +05:30
e4246df315 Changed the business logic of teams and tasks API in DashboardController to accept project ID and provide data according to project ID or project IDs assigned to logged in user 2025-07-16 18:00:15 +05:30
237b178107 Replace lazy loading with eager loading 2025-07-16 17:52:25 +05:30
2889620c1c only checking if the user have permission of project or not only 2025-07-16 14:49:34 +05:30
08e8e8d75f Changed the business logic of teams and tasks API in DashboardController to accept project ID and provide data according to project ID or project IDs assigned to logged in user 2025-07-16 12:39:16 +05:30
3 changed files with 184 additions and 41 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)
{ {
var tenantId = _userHelper.GetTenantId(); try
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();
var checkedInEmployee = await _context.Attendes.Where(e => e.InTime != null ? e.InTime.Value.Date == date : false).Select(e => e.EmployeeID).ToListAsync();
TeamDashboardVM teamDashboardVM = new TeamDashboardVM
{ {
TotalEmployees = Employees.Count(), var tenantId = _userHelper.GetTenantId();
InToday = checkedInEmployee.Distinct().Count() var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
};
_logger.LogInfo("Today's total checked in employees fetched by employee {EmployeeId}", LoggedInEmployee.Id);
return Ok(ApiResponse<object>.SuccessResponse(teamDashboardVM, "Success", 200));
}
[HttpGet("tasks")] _logger.LogInfo("GetTotalEmployees called by user {UserId} for ProjectId: {ProjectId}", loggedInEmployee.Id, projectId ?? Guid.Empty);
public async Task<IActionResult> GetTotalTasks()
{ // --- Step 1: Get the list of projects the user can access ---
var tenantId = _userHelper.GetTenantId(); // This query is more efficient as it only selects the IDs needed.
var LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); var projects = await _projectsHelper.GetMyProjects(tenantId, loggedInEmployee);
var Tasks = await _context.WorkItems.Where(t => t.TenantId == tenantId).Select(t => new { PlannedWork = t.PlannedWork, CompletedWork = t.CompletedWork }).ToListAsync(); var accessibleActiveProjectIds = projects
TasksDashboardVM tasksDashboardVM = new TasksDashboardVM .Where(p => p.ProjectStatusId == ActiveId)
{ .Select(p => p.Id)
TotalTasks = 0, .ToList();
CompletedTasks = 0 if (!accessibleActiveProjectIds.Any())
}; {
foreach (var task in Tasks) _logger.LogInfo("User {UserId} has no accessible active projects.", loggedInEmployee.Id);
{ return Ok(ApiResponse<TeamDashboardVM>.SuccessResponse(new TeamDashboardVM(), "No accessible active projects found.", 200));
tasksDashboardVM.TotalTasks += task.PlannedWork; }
tasksDashboardVM.CompletedTasks += task.CompletedWork;
// --- Step 2: Build the list of project IDs to query against ---
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));
} }
_logger.LogInfo("Total targeted tasks and total completed tasks fetched by employee {EmployeeId}", LoggedInEmployee.Id);
return Ok(ApiResponse<object>.SuccessResponse(tasksDashboardVM, "Success", 200));
} }
/// <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 loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
_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)
{
// --- Logic for a SINGLE Project ---
// 2a. Security Check: Verify permission for the specific project.
var hasPermission = await _permissionServices.HasProjectPermission(loggedInEmployee, projectId.Value.ToString());
if (!hasPermission)
{
_logger.LogWarning("Access DENIED for user {UserId} on project {ProjectId}.", loggedInEmployee.Id, projectId.Value);
return StatusCode(403, ApiResponse<object>.ErrorResponse("Access Denied.", "You do not have permission to view this project.", 403));
}
// 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

@ -76,14 +76,14 @@ namespace MarcoBMS.Services.Helpers
try try
{ {
List<EmployeeVM> result = new List<EmployeeVM>(); List<EmployeeVM> result = new List<EmployeeVM>();
if (ProjectId != null) if (ProjectId.HasValue)
{ {
result = await (from pa in _context.ProjectAllocations.Where(c => c.ProjectId == ProjectId) result = await _context.ProjectAllocations
join em in _context.Employees.Where(c => c.TenantId == TenentId && c.IsActive == true).Include(fp => fp.JobRole) // Include Feature .Include(pa => pa.Employee)
on pa.EmployeeId equals em.Id .ThenInclude(e => e!.JobRole)
select em.ToEmployeeVMFromEmployee() .Where(c => c.ProjectId == ProjectId.Value && c.IsActive && c.Employee != null)
) .Select(pa => pa.Employee!.ToEmployeeVMFromEmployee())
.ToListAsync(); .ToListAsync();
} }