931 lines
49 KiB
C#
931 lines
49 KiB
C#
using Marco.Pms.DataAccess.Data;
|
|
using Marco.Pms.Model.Activities;
|
|
using Marco.Pms.Model.Dtos.Attendance;
|
|
using Marco.Pms.Model.Employees;
|
|
using Marco.Pms.Model.Entitlements;
|
|
using Marco.Pms.Model.Expenses;
|
|
using Marco.Pms.Model.Projects;
|
|
using Marco.Pms.Model.Utilities;
|
|
using Marco.Pms.Model.ViewModels.DashBoard;
|
|
using Marco.Pms.Services.Service;
|
|
using Marco.Pms.Services.Service.ServiceInterfaces;
|
|
using MarcoBMS.Services.Helpers;
|
|
using MarcoBMS.Services.Service;
|
|
using Microsoft.AspNetCore.Authorization;
|
|
using Microsoft.AspNetCore.Mvc;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using System.Globalization;
|
|
|
|
namespace Marco.Pms.Services.Controllers
|
|
{
|
|
[Authorize]
|
|
[Route("api/[controller]")]
|
|
[ApiController]
|
|
public class DashboardController : ControllerBase
|
|
{
|
|
private readonly ApplicationDbContext _context;
|
|
private readonly UserHelper _userHelper;
|
|
private readonly IProjectServices _projectServices;
|
|
private readonly ILoggingService _logger;
|
|
private readonly PermissionServices _permissionServices;
|
|
private readonly IServiceScopeFactory _serviceScopeFactory;
|
|
public static readonly Guid ActiveId = Guid.Parse("b74da4c2-d07e-46f2-9919-e75e49b12731");
|
|
private static readonly Guid Draft = Guid.Parse("297e0d8f-f668-41b5-bfea-e03b354251c8");
|
|
private static readonly Guid Review = Guid.Parse("6537018f-f4e9-4cb3-a210-6c3b2da999d7");
|
|
private static readonly Guid Approve = Guid.Parse("4068007f-c92f-4f37-a907-bc15fe57d4d8");
|
|
private static readonly Guid ProcessPending = Guid.Parse("f18c5cfd-7815-4341-8da2-2c2d65778e27");
|
|
private static readonly Guid Processed = Guid.Parse("61578360-3a49-4c34-8604-7b35a3787b95");
|
|
private static readonly Guid RejectedByReviewer = Guid.Parse("965eda62-7907-4963-b4a1-657fb0b2724b");
|
|
private static readonly Guid RejectedByApprover = Guid.Parse("d1ee5eec-24b6-4364-8673-a8f859c60729");
|
|
private readonly Guid tenantId;
|
|
|
|
public DashboardController(ApplicationDbContext context,
|
|
UserHelper userHelper,
|
|
IProjectServices projectServices,
|
|
IServiceScopeFactory serviceScopeFactory,
|
|
ILoggingService logger,
|
|
PermissionServices permissionServices)
|
|
{
|
|
_context = context;
|
|
_userHelper = userHelper;
|
|
_projectServices = projectServices;
|
|
_logger = logger;
|
|
_serviceScopeFactory = serviceScopeFactory;
|
|
_permissionServices = permissionServices;
|
|
tenantId = userHelper.GetTenantId();
|
|
}
|
|
[HttpGet("progression")]
|
|
public async Task<IActionResult> GetGraph([FromQuery] double days, [FromQuery] string FromDate, [FromQuery] Guid? projectId)
|
|
{
|
|
var LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
|
|
|
|
DateTime fromDate = new DateTime();
|
|
DateTime toDate = new DateTime();
|
|
List<ProjectProgressionVM>? projectProgressionVMs = new List<ProjectProgressionVM>();
|
|
if (FromDate != null && DateTime.TryParse(FromDate, out fromDate) == false)
|
|
{
|
|
return BadRequest(ApiResponse<object>.ErrorResponse("Invalid starting date.", "Invalid starting date.", 400));
|
|
|
|
}
|
|
var firstTask = await _context.TaskAllocations.Select(t => new { t.TenantId, t.AssignmentDate }).FirstOrDefaultAsync(t => t.TenantId == tenantId);
|
|
if (FromDate == null) fromDate = DateTime.UtcNow.Date;
|
|
if (firstTask == null) firstTask = new { TenantId = tenantId, AssignmentDate = DateTime.UtcNow };
|
|
|
|
|
|
if (days >= 0)
|
|
{
|
|
double negativeDays = 0 - days;
|
|
toDate = fromDate.AddDays(negativeDays);
|
|
|
|
if (firstTask != null && (firstTask.AssignmentDate.Date >= toDate.Date))
|
|
{
|
|
toDate = firstTask.AssignmentDate;
|
|
}
|
|
if (projectId == null)
|
|
{
|
|
List<TaskAllocation> tasks = await _context.TaskAllocations.Where(t => t.AssignmentDate.Date <= fromDate.Date && t.AssignmentDate.Date >= toDate.Date && t.TenantId == tenantId).ToListAsync();
|
|
|
|
double flagDays = 0;
|
|
while (negativeDays < flagDays)
|
|
{
|
|
ProjectProgressionVM ProjectProgressionVM = new ProjectProgressionVM();
|
|
ProjectProgressionVM.ProjectId = projectId != null ? projectId.Value : Guid.Empty;
|
|
ProjectProgressionVM.ProjectName = "";
|
|
var date = fromDate.AddDays(flagDays);
|
|
if (date >= (firstTask != null ? firstTask.AssignmentDate.Date : null))
|
|
{
|
|
var todayTasks = tasks.Where(t => t.AssignmentDate.Date == date.Date).ToList();
|
|
double plannedTaks = 0;
|
|
double completedTasks = 0;
|
|
ProjectProgressionVM.Date = date;
|
|
|
|
foreach (var task in todayTasks)
|
|
{
|
|
plannedTaks += task.PlannedTask;
|
|
completedTasks += task.CompletedTask;
|
|
}
|
|
ProjectProgressionVM.PlannedTask = plannedTaks;
|
|
ProjectProgressionVM.CompletedTask = completedTasks;
|
|
|
|
projectProgressionVMs.Add(ProjectProgressionVM);
|
|
}
|
|
flagDays -= 1;
|
|
}
|
|
_logger.LogInfo("Project Progression report for all projects fetched successfully by employee {EmployeeId}", LoggedInEmployee.Id);
|
|
}
|
|
else
|
|
{
|
|
var project = await _context.Projects.FirstOrDefaultAsync(p => p.Id == projectId);
|
|
List<Building> buildings = await _context.Buildings.Where(b => b.ProjectId == projectId && b.TenantId == tenantId).ToListAsync();
|
|
List<Guid> idList = buildings.Select(b => b.Id).ToList();
|
|
|
|
List<Floor> floors = await _context.Floor.Where(f => idList.Contains(f.BuildingId) && f.TenantId == tenantId).ToListAsync();
|
|
idList = floors.Select(f => f.Id).ToList();
|
|
|
|
List<WorkArea> workAreas = await _context.WorkAreas.Where(a => idList.Contains(a.FloorId) && a.TenantId == tenantId).ToListAsync();
|
|
idList = workAreas.Select(a => a.Id).ToList();
|
|
|
|
List<WorkItem> workItems = await _context.WorkItems.Where(i => idList.Contains(i.WorkAreaId) && i.TenantId == tenantId).ToListAsync();
|
|
idList = workItems.Select(i => i.Id).ToList();
|
|
|
|
List<TaskAllocation> tasks = await _context.TaskAllocations.Where(t => idList.Contains(t.WorkItemId) && t.AssignmentDate.Date <= fromDate.Date && t.AssignmentDate.Date >= toDate.Date && t.TenantId == tenantId).ToListAsync();
|
|
if (project != null)
|
|
{
|
|
double flagDays = 0;
|
|
while (negativeDays < flagDays)
|
|
{
|
|
ProjectProgressionVM projectProgressionVM = new ProjectProgressionVM();
|
|
projectProgressionVM.ProjectId = projectId.Value;
|
|
projectProgressionVM.ProjectName = project.Name;
|
|
var date = fromDate.AddDays(flagDays);
|
|
if (date >= (firstTask != null ? firstTask.AssignmentDate.Date : null))
|
|
{
|
|
var todayTasks = tasks.Where(t => t.AssignmentDate.Date == date.Date).ToList();
|
|
double plannedTaks = 0;
|
|
double completedTasks = 0;
|
|
projectProgressionVM.Date = date;
|
|
|
|
foreach (var task in todayTasks)
|
|
{
|
|
plannedTaks += task.PlannedTask;
|
|
completedTasks += task.CompletedTask;
|
|
}
|
|
projectProgressionVM.PlannedTask = plannedTaks;
|
|
projectProgressionVM.CompletedTask = completedTasks;
|
|
|
|
projectProgressionVMs.Add(projectProgressionVM);
|
|
}
|
|
|
|
flagDays -= 1;
|
|
}
|
|
}
|
|
_logger.LogInfo("Project Progression for project {ProjectId} fetched successfully by employee {EmployeeId}", projectId, LoggedInEmployee.Id);
|
|
}
|
|
}
|
|
return Ok(ApiResponse<object>.SuccessResponse(projectProgressionVMs, "Success", 200));
|
|
}
|
|
|
|
[HttpGet("projects")]
|
|
public async Task<IActionResult> GetProjectCount()
|
|
{
|
|
var LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
|
|
|
|
var projects = await _context.Projects.Where(p => p.TenantId == tenantId).ToListAsync();
|
|
var projectStatus = await _context.StatusMasters.Where(s => s.Status == "Active" || s.Status == "In Progress").ToListAsync();
|
|
var projectStatusIds = projectStatus.Select(s => s.Id).ToList();
|
|
var ongoingProjects = projects.Where(p => projectStatusIds.Contains(p.ProjectStatusId)).ToList();
|
|
|
|
ProjectDashboardVM projectDashboardVM = new ProjectDashboardVM
|
|
{
|
|
TotalProjects = projects.Count(),
|
|
OngoingProjects = ongoingProjects.Count()
|
|
};
|
|
_logger.LogInfo("Number of total ongoing projects fetched by employee {EmployeeId}", LoggedInEmployee.Id);
|
|
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([FromQuery] Guid? projectId)
|
|
{
|
|
try
|
|
{
|
|
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
|
|
|
|
_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 _projectServices.GetMyProjectIdsAsync(tenantId, loggedInEmployee);
|
|
|
|
var accessibleActiveProjectIds = await _context.Projects
|
|
.Where(p => p.ProjectStatusId == ActiveId && projects.Contains(p.Id))
|
|
.Select(p => p.Id)
|
|
.ToListAsync();
|
|
|
|
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);
|
|
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(ex, "An unexpected error occurred in GetTotalEmployees for projectId {ProjectId}", projectId ?? Guid.Empty);
|
|
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 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);
|
|
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 accessibleProjectIds = await _projectServices.GetMyProjectIdsAsync(tenantId, loggedInEmployee);
|
|
|
|
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(ex, "An unexpected error occurred in GetTotalTasks for projectId {ProjectId}", projectId ?? Guid.Empty);
|
|
return StatusCode(500, ApiResponse<object>.ErrorResponse("An internal server error occurred.", null, 500));
|
|
}
|
|
}
|
|
|
|
[HttpGet("pending-attendance")]
|
|
public async Task<IActionResult> GetPendingAttendance()
|
|
{
|
|
var LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
|
|
|
|
var attendance = await _context.Attendes.Where(a => a.EmployeeId == LoggedInEmployee.Id && a.TenantId == tenantId).ToListAsync();
|
|
if (attendance.Any())
|
|
{
|
|
var pendingRegularization = attendance.Where(a => a.Activity == ATTENDANCE_MARK_TYPE.REQUEST_REGULARIZE).ToList().Count;
|
|
var pendingCheckOut = attendance.Where(a => a.OutTime == null).ToList().Count;
|
|
var response = new
|
|
{
|
|
PendingRegularization = pendingRegularization,
|
|
PendingCheckOut = pendingCheckOut
|
|
};
|
|
_logger.LogInfo("Number of pending regularization and pending check-out are fetched successfully for employee {EmployeeId}", LoggedInEmployee.Id);
|
|
return Ok(ApiResponse<object>.SuccessResponse(response, "Pending regularization and pending check-out are fetched successfully", 200));
|
|
}
|
|
_logger.LogWarning("No attendance entry was found for employee {EmployeeId}", LoggedInEmployee.Id);
|
|
return NotFound(ApiResponse<object>.ErrorResponse("No attendance entry was found for this employee", "No attendance entry was found for this employee", 404));
|
|
}
|
|
|
|
[HttpGet("project-attendance/{projectId}")]
|
|
public async Task<IActionResult> GetProjectAttendance(Guid projectId, [FromQuery] string? date)
|
|
{
|
|
var LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
|
|
|
|
DateTime currentDate = DateTime.UtcNow;
|
|
List<ProjectProgressionVM>? projectProgressionVMs = new List<ProjectProgressionVM>();
|
|
if (date != null && DateTime.TryParse(date, out currentDate) == false)
|
|
{
|
|
_logger.LogWarning($"user send invalid date");
|
|
return BadRequest(ApiResponse<object>.ErrorResponse("Invalid date.", "Invalid date.", 400));
|
|
|
|
}
|
|
Project? project = await _context.Projects.FirstOrDefaultAsync(p => p.Id == projectId);
|
|
if (project == null)
|
|
{
|
|
_logger.LogWarning("Employee {EmployeeId} was attempted to get project attendance for date {Date}, but project not found in database", LoggedInEmployee.Id, currentDate);
|
|
return NotFound(ApiResponse<object>.ErrorResponse("Project not found", "Project not found", 404));
|
|
}
|
|
List<ProjectAllocation>? projectAllocation = await _context.ProjectAllocations.Where(p => p.ProjectId == projectId && p.IsActive && p.TenantId == tenantId).ToListAsync();
|
|
var employeeIds = projectAllocation.Select(p => p.EmployeeId).Distinct().ToList();
|
|
List<Employee>? employees = await _context.Employees.Where(e => employeeIds.Contains(e.Id)).ToListAsync();
|
|
|
|
var attendances = await _context.Attendes.Where(a => employeeIds.Contains(a.EmployeeId) && a.ProjectID == projectId && a.InTime.HasValue && a.InTime.Value.Date == currentDate.Date).ToListAsync();
|
|
List<EmployeeAttendanceVM> employeeAttendanceVMs = new List<EmployeeAttendanceVM>();
|
|
foreach (var attendance in attendances)
|
|
{
|
|
|
|
Employee? employee = employees.FirstOrDefault(e => e.Id == attendance.EmployeeId);
|
|
if (employee != null)
|
|
{
|
|
EmployeeAttendanceVM employeeAttendanceVM = new EmployeeAttendanceVM
|
|
{
|
|
FirstName = employee.FirstName,
|
|
LastName = employee.LastName,
|
|
MiddleName = employee.MiddleName,
|
|
Comment = attendance.Comment,
|
|
InTime = attendance.InTime,
|
|
OutTime = attendance.OutTime
|
|
};
|
|
|
|
employeeAttendanceVMs.Add(employeeAttendanceVM);
|
|
}
|
|
}
|
|
ProjectAttendanceVM projectAttendanceVM = new ProjectAttendanceVM();
|
|
projectAttendanceVM.AttendanceTable = employeeAttendanceVMs;
|
|
projectAttendanceVM.CheckedInEmployee = attendances.Count;
|
|
projectAttendanceVM.AssignedEmployee = employeeIds.Count;
|
|
|
|
_logger.LogInfo($"Attendance record for project {projectId} for date {currentDate.Date} by employee {LoggedInEmployee.Id}");
|
|
return Ok(ApiResponse<object>.SuccessResponse(projectAttendanceVM, $"Attendance record for project {project.Name} for date {currentDate.Date}", 200));
|
|
}
|
|
|
|
[HttpGet("activities/{projectId}")]
|
|
public async Task<IActionResult> GetActivities(Guid projectId, [FromQuery] string? date)
|
|
{
|
|
var LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
|
|
|
|
DateTime currentDate = DateTime.UtcNow;
|
|
if (date != null && DateTime.TryParse(date, out currentDate) == false)
|
|
{
|
|
_logger.LogWarning($"user send invalid date");
|
|
return BadRequest(ApiResponse<object>.ErrorResponse("Invalid date.", "Invalid date.", 400));
|
|
|
|
}
|
|
Project? project = await _context.Projects.FirstOrDefaultAsync(p => p.Id == projectId);
|
|
if (project == null)
|
|
{
|
|
_logger.LogWarning("Employee {EmployeeId} was attempted to get activities performed for date {Date}, but project not found in database", LoggedInEmployee.Id, currentDate);
|
|
return NotFound(ApiResponse<object>.ErrorResponse("Project not found", "Project not found", 404));
|
|
}
|
|
|
|
var buildings = await _context.Buildings.Where(b => b.ProjectId == project.Id).ToListAsync();
|
|
var buildingIds = buildings.Select(b => b.Id).Distinct().ToList();
|
|
|
|
var floors = await _context.Floor.Where(f => buildingIds.Contains(f.BuildingId)).ToListAsync();
|
|
var floorIds = floors.Select(f => f.Id).Distinct().ToList();
|
|
|
|
var areas = await _context.WorkAreas.Where(a => floorIds.Contains(a.FloorId)).ToListAsync();
|
|
var areaIds = areas.Select(a => a.Id).Distinct().ToList();
|
|
|
|
var workItems = await _context.WorkItems.Include(i => i.ActivityMaster).Where(i => areaIds.Contains(i.WorkAreaId)).ToListAsync();
|
|
var itemIds = workItems.Select(i => i.Id).Distinct().ToList();
|
|
|
|
var tasks = await _context.TaskAllocations.Where(t => itemIds.Contains(t.WorkItemId) && t.AssignmentDate.Date == currentDate.Date).ToListAsync();
|
|
double totalPlannedTask = 0;
|
|
double totalCompletedTask = 0;
|
|
List<PerformedActivites> performedActivites = new List<PerformedActivites>();
|
|
|
|
foreach (var task in tasks)
|
|
{
|
|
totalPlannedTask += task.PlannedTask;
|
|
totalCompletedTask += task.CompletedTask;
|
|
|
|
WorkItem workItem = workItems.FirstOrDefault(i => i.Id == task.WorkItemId) ?? new WorkItem();
|
|
string activityName = (workItem.ActivityMaster != null ? workItem.ActivityMaster.ActivityName : "") ?? "";
|
|
|
|
WorkArea workArea = areas.FirstOrDefault(a => a.Id == workItem.WorkAreaId) ?? new WorkArea();
|
|
string areaName = workArea.AreaName ?? "";
|
|
|
|
Floor floor = floors.FirstOrDefault(f => f.Id == workArea.FloorId) ?? new Floor();
|
|
string floorName = floor.FloorName ?? "";
|
|
|
|
Building building = buildings.FirstOrDefault(b => b.Id == floor.BuildingId) ?? new Building();
|
|
string buildingName = building.Name ?? "";
|
|
|
|
PerformedActivites performedTask = new PerformedActivites
|
|
{
|
|
ActivityName = activityName,
|
|
BuldingName = buildingName,
|
|
FloorName = floorName,
|
|
WorkAreaName = areaName,
|
|
AssignedToday = task.PlannedTask,
|
|
CompletedToday = task.CompletedTask,
|
|
};
|
|
performedActivites.Add(performedTask);
|
|
}
|
|
var pendingReport = tasks.Where(t => t.ReportedDate == null).ToList().Count;
|
|
|
|
ActivityReport report = new ActivityReport
|
|
{
|
|
PerformedActivites = performedActivites,
|
|
TotalCompletedWork = totalCompletedTask,
|
|
TotalPlannedWork = totalPlannedTask,
|
|
ReportPending = pendingReport,
|
|
TodaysAssigned = tasks.Count
|
|
};
|
|
_logger.LogInfo($"Record of performed activities for project {projectId} for date {currentDate.Date} by employee {LoggedInEmployee.Id}");
|
|
return Ok(ApiResponse<object>.SuccessResponse(report, $"Record of performed activities for project {project.Name} for date {currentDate.Date}", 200));
|
|
}
|
|
|
|
[HttpGet("attendance-overview/{projectId}")]
|
|
public async Task<IActionResult> GetAttendanceOverView(Guid projectId, [FromQuery] string days)
|
|
{
|
|
_logger.LogInfo("GetAttendanceOverView called for ProjectId: {ProjectId}, Days: {Days}", projectId, days);
|
|
|
|
// Step 1: Validate project existence
|
|
var project = await _context.Projects.AsNoTracking().FirstOrDefaultAsync(p => p.Id == projectId);
|
|
if (project == null)
|
|
{
|
|
_logger.LogWarning("Project not found for ProjectId: {ProjectId}", projectId);
|
|
return BadRequest(ApiResponse<object>.ErrorResponse("Project not found", "Project not found", 400));
|
|
}
|
|
|
|
// Step 2: Permission check
|
|
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
|
|
bool hasAssigned = await _permissionServices.HasProjectPermission(loggedInEmployee, projectId);
|
|
|
|
if (!hasAssigned)
|
|
{
|
|
_logger.LogWarning("Unauthorized access attempt. EmployeeId: {EmployeeId}, ProjectId: {ProjectId}", loggedInEmployee.Id, projectId);
|
|
return StatusCode(403, ApiResponse<object>.ErrorResponse(
|
|
"You don't have permission to access this feature",
|
|
"You don't have permission to access this feature", 403));
|
|
}
|
|
|
|
// Step 3: Validate and parse days input
|
|
if (!int.TryParse(days, out int dayCount) || dayCount <= 0)
|
|
{
|
|
_logger.LogWarning("Invalid days input received: {Days}", days);
|
|
return BadRequest(ApiResponse<object>.ErrorResponse("Invalid number of days", "Days must be a positive integer", 400));
|
|
}
|
|
|
|
// Step 4: Define date range
|
|
DateTime today = DateTime.UtcNow.Date;
|
|
DateTime startDate = today.AddDays(-dayCount);
|
|
|
|
// Step 5: Load project allocations and related job roles
|
|
var allocations = await _context.ProjectAllocations
|
|
.Where(pa => pa.ProjectId == projectId)
|
|
.ToListAsync();
|
|
|
|
if (!allocations.Any())
|
|
{
|
|
_logger.LogInfo("No employee allocations found for project: {ProjectId}", projectId);
|
|
return Ok(ApiResponse<object>.SuccessResponse(new List<AttendanceOverviewVM>(), "No allocations found", 200));
|
|
}
|
|
|
|
var jobRoleIds = allocations.Select(pa => pa.JobRoleId).Distinct().ToList();
|
|
|
|
var jobRoles = await _context.JobRoles
|
|
.Where(jr => jobRoleIds.Contains(jr.Id))
|
|
.ToListAsync();
|
|
|
|
// Step 6: Load attendance records for given date range
|
|
var attendances = await _context.Attendes
|
|
.Where(a =>
|
|
a.ProjectID == projectId &&
|
|
a.InTime.HasValue &&
|
|
a.InTime.Value.Date >= startDate &&
|
|
a.InTime.Value.Date <= today)
|
|
.ToListAsync();
|
|
|
|
var overviewList = new List<AttendanceOverviewVM>();
|
|
|
|
// Step 7: Process attendance per date per role
|
|
for (DateTime date = today; date > startDate; date = date.AddDays(-1))
|
|
{
|
|
foreach (var jobRole in jobRoles)
|
|
{
|
|
var employeeIds = allocations
|
|
.Where(pa => pa.JobRoleId == jobRole.Id)
|
|
.Select(pa => pa.EmployeeId)
|
|
.ToList();
|
|
|
|
int presentCount = attendances
|
|
.Count(a => employeeIds.Contains(a.EmployeeId) && a.InTime!.Value.Date == date);
|
|
|
|
overviewList.Add(new AttendanceOverviewVM
|
|
{
|
|
Role = jobRole.Name,
|
|
Date = date.ToString("yyyy-MM-dd"),
|
|
Present = presentCount
|
|
});
|
|
}
|
|
}
|
|
|
|
// Step 8: Order result for consistent presentation
|
|
var sortedResult = overviewList
|
|
.OrderByDescending(r => r.Date)
|
|
.ThenByDescending(r => r.Present)
|
|
.ToList();
|
|
|
|
_logger.LogInfo("Attendance overview fetched. ProjectId: {ProjectId}, Records: {Count}", projectId, sortedResult.Count);
|
|
|
|
return Ok(ApiResponse<object>.SuccessResponse(sortedResult, $"{sortedResult.Count} records fetched for attendance overview", 200));
|
|
}
|
|
|
|
[HttpGet("expense/monthly")]
|
|
public async Task<IActionResult> GetExpenseReportByProjectsAsync([FromQuery] Guid? projectId, [FromQuery] Guid? categoryId, [FromQuery] int months)
|
|
{
|
|
|
|
try
|
|
{
|
|
// Read-only base filter with tenant scope and non-draft
|
|
var baseQuery = _context.Expenses
|
|
.AsNoTracking()
|
|
.Where(e =>
|
|
e.TenantId == tenantId
|
|
&& e.IsActive
|
|
&& e.StatusId != Draft); // [Server Filters]
|
|
|
|
if (months != 0)
|
|
{
|
|
months = 0 - months;
|
|
var end = DateTime.UtcNow.Date;
|
|
var start = end.AddMonths(months); // inclusive EOD
|
|
baseQuery = baseQuery.Where(e => e.TransactionDate >= start
|
|
&& e.TransactionDate <= end);
|
|
}
|
|
|
|
if (projectId.HasValue)
|
|
baseQuery = baseQuery.Where(e => e.ProjectId == projectId);
|
|
|
|
if (categoryId.HasValue)
|
|
baseQuery = baseQuery.Where(e => e.ExpensesTypeId == categoryId);
|
|
|
|
// Single server-side group/aggregate by project
|
|
var report = await baseQuery
|
|
.AsNoTracking()
|
|
.GroupBy(e => new { e.TransactionDate.Year, e.TransactionDate.Month })
|
|
.Select(g => new
|
|
{
|
|
Year = g.Key.Year,
|
|
Month = g.Key.Month,
|
|
Total = g.Sum(x => x.Amount),
|
|
Count = g.Count()
|
|
})
|
|
.OrderBy(x => x.Year).ThenBy(x => x.Month)
|
|
.ToListAsync();
|
|
|
|
var culture = CultureInfo.GetCultureInfo("en-IN"); // pick desired locale
|
|
|
|
var response = report
|
|
.Select(x => new
|
|
{
|
|
MonthName = culture.DateTimeFormat.GetMonthName(x.Month), // e.g., "January"
|
|
Year = x.Year,
|
|
Total = x.Total,
|
|
Count = x.Count
|
|
}).ToList();
|
|
|
|
_logger.LogInfo(
|
|
"GetExpenseReportByProjects completed. TenantId={TenantId}, Rows={Rows}",
|
|
tenantId, report.Count); // [Completion Log]
|
|
|
|
return Ok(ApiResponse<object>.SuccessResponse(response, "Expense report by project fetched successfully", 200)); // [Success Response]
|
|
}
|
|
catch (OperationCanceledException)
|
|
{
|
|
_logger.LogWarning("GetExpenseReportByProjects canceled by client. TenantId={TenantId}", tenantId); // [Cancel Log]
|
|
return StatusCode(499, ApiResponse<object>.ErrorResponse("Client has canceled the opration", "Client has canceled the opration", 499)); // [Cancel Response]
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex,
|
|
"GetExpenseReportByProjects failed. TenantId={TenantId}",
|
|
tenantId); // [Error Log]
|
|
return StatusCode(500,
|
|
ApiResponse<object>.ErrorResponse("An error occurred while fetching the expense report.", 500)); // [Error Response]
|
|
}
|
|
}
|
|
|
|
[HttpGet("expense/type")]
|
|
public async Task<IActionResult> GetExpenseReportByExpenseTypeAsync([FromQuery] Guid? projectId, [FromQuery] DateTime startDate, [FromQuery] DateTime endDate)
|
|
{
|
|
// Structured log: entering action with filters
|
|
_logger.LogDebug(
|
|
"GetExpenseReportByExpenseType started. TenantId={TenantId}, ProjectId={ProjectId}, StartDate={StartDate}, EndDate={EndDate}",
|
|
tenantId, projectId ?? Guid.Empty, startDate, endDate); // [Start Log] [memory:4][memory:1]
|
|
|
|
|
|
try
|
|
{
|
|
// Compose base query: push filters to DB, avoid client evaluation
|
|
IQueryable<Expenses> baseQuery = _context.Expenses
|
|
.AsNoTracking() // Reduce tracking overhead for read-only endpoint
|
|
.Where(e => e.TenantId == tenantId
|
|
&& e.IsActive
|
|
&& e.StatusId != Draft
|
|
&& e.TransactionDate >= startDate
|
|
&& e.TransactionDate <= endDate.AddDays(1).AddTicks(-1));
|
|
|
|
if (projectId.HasValue)
|
|
baseQuery = baseQuery.Where(e => e.ProjectId == projectId.Value); // [Filter] [memory:7]
|
|
|
|
// Project to a minimal shape before grouping to avoid loading navigation graphs
|
|
// Group by expense type name; adjust to the correct key if ExpensesCategory is an enum or navigation
|
|
var query = baseQuery
|
|
.Where(e => e.ExpensesType != null)
|
|
.Select(e => new
|
|
{
|
|
ExpenseTypeName = e.ExpensesType!.Name, // If enum, use e.ExpensesCategory.ToString()
|
|
Amount = e.Amount,
|
|
StatusId = e.StatusId
|
|
})
|
|
.GroupBy(x => x.ExpenseTypeName)
|
|
.Select(g => new
|
|
{
|
|
ProjectName = g.Key, // Original code used g.Key!.Name; here the grouping key is already a string
|
|
TotalApprovedAmount = g.Where(x => x.StatusId == Processed
|
|
|| x.StatusId == ProcessPending).Sum(x => x.Amount),
|
|
TotalPendingAmount = g.Where(x => x.StatusId != Processed
|
|
&& x.StatusId != RejectedByReviewer
|
|
&& x.StatusId != RejectedByApprover)
|
|
.Sum(x => x.Amount),
|
|
TotalRejectedAmount = g.Where(x => x.StatusId == RejectedByReviewer
|
|
|| x.StatusId == RejectedByApprover)
|
|
.Sum(x => x.Amount),
|
|
TotalProcessedAmount = g.Where(x => x.StatusId == Processed)
|
|
.Sum(x => x.Amount)
|
|
})
|
|
.OrderBy(r => r.ProjectName); // Server-side order [memory:7]
|
|
|
|
var report = await query.ToListAsync(); // Single round-trip [memory:7]
|
|
|
|
var response = new
|
|
{
|
|
Report = report,
|
|
TotalAmount = report.Sum(r => r.TotalApprovedAmount)
|
|
};
|
|
|
|
_logger.LogInfo(
|
|
"GetExpenseReportByExpenseType completed. TenantId={TenantId}, Filters: ProjectId={ProjectId}, StartDate={StartDate}, EndDate={EndDate}, Rows={RowCount}, TotalAmount={TotalAmount}",
|
|
tenantId, projectId ?? Guid.Empty, startDate, endDate, report.Count, response.TotalAmount); // [Completion Log] [memory:4]
|
|
|
|
return Ok(ApiResponse<object>.SuccessResponse(response, "Expense report by expense type fetched successfully", 200)); // [Success Response] [memory:1]
|
|
}
|
|
catch (OperationCanceledException)
|
|
{
|
|
_logger.LogWarning("GetExpenseReportByExpenseType canceled by client. TenantId={TenantId}", tenantId); // [Cancel Log] [memory:4]
|
|
return StatusCode(499, ApiResponse<object>.ErrorResponse("Client has canceled the opration", "Client has canceled the opration", 499)); // [Cancel Response] [memory:1]
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex,
|
|
"GetExpenseReportByExpenseType failed. TenantId={TenantId}, ProjectId={ProjectId}, StartDate={StartDate}, EndDate={EndDate}",
|
|
tenantId, projectId ?? Guid.Empty, startDate, endDate); // [Error Log] [memory:4]
|
|
return StatusCode(StatusCodes.Status500InternalServerError,
|
|
ApiResponse<object>.ErrorResponse("An error occurred while fetching the expense report.", 500)); // [Error Response] [memory:1]
|
|
}
|
|
}
|
|
|
|
[HttpGet("expense/pendings")]
|
|
public async Task<IActionResult> GetPendingExpenseListAsync([FromQuery] Guid? projectId)
|
|
{
|
|
// Start log with correlation fields
|
|
_logger.LogDebug(
|
|
"GetPendingExpenseListAsync started. Project={ProjectId} TenantId={TenantId}", projectId ?? Guid.Empty, tenantId); // [Start Log]
|
|
|
|
try
|
|
{
|
|
// Resolve current employee once; avoid using scoped services inside Task.Run
|
|
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); // [User Context]
|
|
|
|
// Resolve permission service from current scope once
|
|
using var scope = _serviceScopeFactory.CreateScope();
|
|
|
|
// Fire permission checks concurrently without Task.Run; these are async I/O methods
|
|
|
|
var hasReviewPermissionTask = Task.Run(async () =>
|
|
{
|
|
var _permission = scope.ServiceProvider.GetRequiredService<PermissionServices>();
|
|
return await _permission.HasPermission(PermissionsMaster.ExpenseReview, loggedInEmployee.Id);
|
|
});
|
|
|
|
var hasApprovePermissionTask = Task.Run(async () =>
|
|
{
|
|
var _permission = scope.ServiceProvider.GetRequiredService<PermissionServices>();
|
|
return await _permission.HasPermission(PermissionsMaster.ExpenseApprove, loggedInEmployee.Id);
|
|
});
|
|
|
|
var hasProcessPermissionTask = Task.Run(async () =>
|
|
{
|
|
var _permission = scope.ServiceProvider.GetRequiredService<PermissionServices>();
|
|
return await _permission.HasPermission(PermissionsMaster.ExpenseProcess, loggedInEmployee.Id);
|
|
});
|
|
|
|
var hasManagePermissionTask = Task.Run(async () =>
|
|
{
|
|
var _permission = scope.ServiceProvider.GetRequiredService<PermissionServices>();
|
|
return await _permission.HasPermission(PermissionsMaster.ExpenseManage, loggedInEmployee.Id);
|
|
});
|
|
|
|
await Task.WhenAll(hasReviewPermissionTask, hasApprovePermissionTask, hasProcessPermissionTask, hasManagePermissionTask); // [Parallel Await]
|
|
|
|
var hasReviewPermission = hasReviewPermissionTask.Result;
|
|
var hasApprovePermission = hasApprovePermissionTask.Result;
|
|
var hasProcessPermission = hasProcessPermissionTask.Result;
|
|
var hasManagePermission = hasManagePermissionTask.Result;
|
|
|
|
_logger.LogInfo(
|
|
"Permissions resolved: Review={Review}, Approve={Approve}, Process={Process}",
|
|
hasReviewPermission, hasApprovePermission, hasProcessPermission); // [Permissions Log]
|
|
|
|
// Build base query: read-only, tenant-scoped
|
|
var baseQuery = _context.Expenses
|
|
.Include(e => e.Status)
|
|
.AsNoTracking() // Reduce tracking overhead for read-only list
|
|
.Where(e => e.IsActive && e.TenantId == tenantId && e.StatusId != Processed && e.Status != null); // [Base Filter]
|
|
|
|
// Project to DTO in SQL to avoid heavy Include graph.
|
|
if (projectId.HasValue)
|
|
baseQuery = baseQuery.Where(e => e.ProjectId == projectId);
|
|
|
|
// Prefer ProjectTo when profiles exist; otherwise project minimal fields
|
|
var expenses = await baseQuery
|
|
.ToListAsync(); // Single round-trip; no Include needed for this shape
|
|
|
|
var draftExpenses = expenses.Where(e => e.StatusId == Draft && e.CreatedById == loggedInEmployee.Id).ToList();
|
|
var reviewExpenses = expenses.Where(e => (hasReviewPermission || e.CreatedById == loggedInEmployee.Id) && e.StatusId == Review).ToList();
|
|
var approveExpenses = expenses.Where(e => (hasApprovePermission || e.CreatedById == loggedInEmployee.Id) && e.StatusId == Approve).ToList();
|
|
var processPendingExpenses = expenses.Where(e => (hasProcessPermission || e.CreatedById == loggedInEmployee.Id) && e.StatusId == ProcessPending).ToList();
|
|
var submitedExpenses = expenses.Where(e => e.StatusId != Draft && e.CreatedById == loggedInEmployee.Id).ToList();
|
|
var totalAmount = expenses.Where(e => e.StatusId != Draft).Sum(e => e.Amount);
|
|
|
|
if (hasManagePermission)
|
|
{
|
|
var response = new
|
|
{
|
|
Draft = new
|
|
{
|
|
Count = draftExpenses.Count,
|
|
TotalAmount = draftExpenses.Sum(e => e.Amount)
|
|
},
|
|
ReviewPending = new
|
|
{
|
|
Count = reviewExpenses.Count,
|
|
TotalAmount = reviewExpenses.Sum(e => e.Amount)
|
|
},
|
|
ApprovePending = new
|
|
{
|
|
Count = approveExpenses.Count,
|
|
TotalAmount = approveExpenses.Sum(e => e.Amount)
|
|
},
|
|
ProcessPending = new
|
|
{
|
|
Count = processPendingExpenses.Count,
|
|
TotalAmount = processPendingExpenses.Sum(e => e.Amount)
|
|
},
|
|
Submited = new
|
|
{
|
|
Count = submitedExpenses.Count,
|
|
TotalAmount = submitedExpenses.Sum(e => e.Amount)
|
|
},
|
|
TotalAmount = totalAmount
|
|
};
|
|
_logger.LogInfo(
|
|
"GetPendingExpenseListAsync completed. TenantId={TenantId}",
|
|
tenantId); // [Completion Log]
|
|
|
|
return Ok(ApiResponse<object>.SuccessResponse(response, "Pending Expenses fetched successfully", 200)); // [Success Response]
|
|
}
|
|
else
|
|
{
|
|
var response = new
|
|
{
|
|
Draft = new
|
|
{
|
|
Count = draftExpenses.Count
|
|
},
|
|
ReviewPending = new
|
|
{
|
|
Count = reviewExpenses.Count
|
|
},
|
|
ApprovePending = new
|
|
{
|
|
Count = approveExpenses.Count
|
|
},
|
|
ProcessPending = new
|
|
{
|
|
Count = processPendingExpenses.Count
|
|
},
|
|
Submited = new
|
|
{
|
|
Count = submitedExpenses.Count
|
|
},
|
|
TotalAmount = totalCount
|
|
};
|
|
_logger.LogInfo(
|
|
"GetPendingExpenseListAsync completed. TenantId={TenantId}",
|
|
tenantId); // [Completion Log]
|
|
|
|
return Ok(ApiResponse<object>.SuccessResponse(response, "Pending Expenses fetched successfully", 200)); // [Success Response]
|
|
}
|
|
}
|
|
catch (OperationCanceledException)
|
|
{
|
|
_logger.LogWarning("GetPendingExpenseListAsync canceled by client. TenantId={TenantId}", tenantId); // [Cancel Log]
|
|
return StatusCode(499, ApiResponse<object>.ErrorResponse("Client has canceled the opration", "Client has canceled the opration", 499)); // [Cancel Response]
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "GetPendingExpenseListAsync failed. TenantId={TenantId}", tenantId); // [Error Log]
|
|
return StatusCode(500,
|
|
ApiResponse<object>.ErrorResponse("An error occurred while fetching pending expenses.", "An error occurred while fetching pending expenses.", 500)); // [Error Response]
|
|
}
|
|
}
|
|
|
|
}
|
|
}
|