Compare commits
	
		
			34 Commits
		
	
	
		
			5e84ee9345
			...
			0ac4c23e74
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 0ac4c23e74 | |||
| b71935dd1f | |||
| c8978ee9b1 | |||
| 0ecf258661 | |||
| 30d614fa11 | |||
| 5deb97d73b | |||
| 6ac28de56a | |||
| 8735de3d93 | |||
| c8ca2d5c49 | |||
| eabd31f8cf | |||
| 57b7f941e6 | |||
| c90f39082a | |||
| c03fae4b65 | |||
| 168922c278 | |||
| 72dccc0c6a | |||
| 7914cf20d4 | |||
| 80149f05f7 | |||
| 560d2f2d4d | |||
| f4ca7670e3 | |||
| 36eb7aef7f | |||
| ca34b01ab0 | |||
| b78f58c304 | |||
| c359212ee5 | |||
| 7d160a9a52 | |||
| 3bc51f9cd9 | |||
| 3a45bded08 | |||
| 0ec507c97c | |||
| d1106bc86b | |||
| 0859284f4a | |||
| bbd2054867 | |||
| e4246df315 | |||
| 237b178107 | |||
| 2889620c1c | |||
| 08e8e8d75f | 
@ -21,12 +21,15 @@ namespace Marco.Pms.Services.Controllers
 | 
			
		||||
    {
 | 
			
		||||
        private readonly ApplicationDbContext _context;
 | 
			
		||||
        private readonly UserHelper _userHelper;
 | 
			
		||||
        private readonly ProjectsHelper _projectsHelper;
 | 
			
		||||
        private readonly ILoggingService _logger;
 | 
			
		||||
        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;
 | 
			
		||||
            _userHelper = userHelper;
 | 
			
		||||
            _projectsHelper = projectsHelper;
 | 
			
		||||
            _logger = logger;
 | 
			
		||||
            _permissionServices = permissionServices;
 | 
			
		||||
        }
 | 
			
		||||
@ -162,46 +165,186 @@ namespace Marco.Pms.Services.Controllers
 | 
			
		||||
            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")]
 | 
			
		||||
        public async Task<IActionResult> GetTotalEmployees()
 | 
			
		||||
        public async Task<IActionResult> GetTotalEmployees([FromQuery] Guid? projectId)
 | 
			
		||||
        {
 | 
			
		||||
            var tenantId = _userHelper.GetTenantId();
 | 
			
		||||
            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
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                TotalEmployees = Employees.Count(),
 | 
			
		||||
                InToday = checkedInEmployee.Distinct().Count()
 | 
			
		||||
            };
 | 
			
		||||
            _logger.LogInfo("Today's total checked in employees fetched by employee {EmployeeId}", LoggedInEmployee.Id);
 | 
			
		||||
            return Ok(ApiResponse<object>.SuccessResponse(teamDashboardVM, "Success", 200));
 | 
			
		||||
        }
 | 
			
		||||
                var tenantId = _userHelper.GetTenantId();
 | 
			
		||||
                var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
 | 
			
		||||
 | 
			
		||||
        [HttpGet("tasks")]
 | 
			
		||||
        public async Task<IActionResult> GetTotalTasks()
 | 
			
		||||
        {
 | 
			
		||||
            var tenantId = _userHelper.GetTenantId();
 | 
			
		||||
            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
 | 
			
		||||
            {
 | 
			
		||||
                TotalTasks = 0,
 | 
			
		||||
                CompletedTasks = 0
 | 
			
		||||
            };
 | 
			
		||||
            foreach (var task in Tasks)
 | 
			
		||||
            {
 | 
			
		||||
                tasksDashboardVM.TotalTasks += task.PlannedWork;
 | 
			
		||||
                tasksDashboardVM.CompletedTasks += task.CompletedWork;
 | 
			
		||||
                _logger.LogInfo("GetTotalEmployees called by user {UserId} for ProjectId: {ProjectId}", loggedInEmployee.Id, projectId ?? Guid.Empty);
 | 
			
		||||
 | 
			
		||||
                // --- Step 1: Get the list of projects the user can access ---
 | 
			
		||||
                // This query is more efficient as it only selects the IDs needed.
 | 
			
		||||
                var projects = await _projectsHelper.GetMyProjects(tenantId, loggedInEmployee);
 | 
			
		||||
                var accessibleActiveProjectIds = projects
 | 
			
		||||
                    .Where(p => p.ProjectStatusId == ActiveId)
 | 
			
		||||
                    .Select(p => p.Id)
 | 
			
		||||
                    .ToList();
 | 
			
		||||
                if (!accessibleActiveProjectIds.Any())
 | 
			
		||||
                {
 | 
			
		||||
                    _logger.LogInfo("User {UserId} has no accessible active projects.", loggedInEmployee.Id);
 | 
			
		||||
                    return Ok(ApiResponse<TeamDashboardVM>.SuccessResponse(new TeamDashboardVM(), "No accessible active projects found.", 200));
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                // --- 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")]
 | 
			
		||||
        public async Task<IActionResult> GetPendingAttendance()
 | 
			
		||||
        {
 | 
			
		||||
 | 
			
		||||
@ -1086,7 +1086,7 @@ namespace Marco.Pms.Services.Helpers
 | 
			
		||||
            var LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
 | 
			
		||||
            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)
 | 
			
		||||
                {
 | 
			
		||||
                    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);
 | 
			
		||||
 | 
			
		||||
@ -76,14 +76,14 @@ namespace MarcoBMS.Services.Helpers
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                List<EmployeeVM> result = new List<EmployeeVM>();
 | 
			
		||||
                if (ProjectId != null)
 | 
			
		||||
                if (ProjectId.HasValue)
 | 
			
		||||
                {
 | 
			
		||||
 | 
			
		||||
                    result = await (from pa in _context.ProjectAllocations.Where(c => c.ProjectId == ProjectId)
 | 
			
		||||
                                    join em in _context.Employees.Where(c => c.TenantId == TenentId && c.IsActive == true).Include(fp => fp.JobRole) // Include Feature
 | 
			
		||||
                                    on pa.EmployeeId equals em.Id
 | 
			
		||||
                                    select em.ToEmployeeVMFromEmployee()
 | 
			
		||||
                                       )
 | 
			
		||||
                    result = await _context.ProjectAllocations
 | 
			
		||||
                        .Include(pa => pa.Employee)
 | 
			
		||||
                            .ThenInclude(e => e!.JobRole)
 | 
			
		||||
                        .Where(c => c.ProjectId == ProjectId.Value && c.IsActive && c.Employee != null)
 | 
			
		||||
                        .Select(pa => pa.Employee!.ToEmployeeVMFromEmployee())
 | 
			
		||||
                        .ToListAsync();
 | 
			
		||||
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user