using Marco.Pms.Model.Dtos.Project; using Marco.Pms.Model.Dtos.Projects; using Marco.Pms.Model.Dtos.Util; using Marco.Pms.Model.Employees; using Marco.Pms.Model.Utilities; using Marco.Pms.Services.Service.ServiceInterfaces; using MarcoBMS.Services.Helpers; using MarcoBMS.Services.Service; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.CodeAnalysis; using MongoDB.Driver; namespace MarcoBMS.Services.Controllers { [Route("api/[controller]")] [ApiController] [Authorize] public class ProjectController : ControllerBase { private readonly IProjectServices _projectServices; private readonly UserHelper _userHelper; private readonly ILoggingService _logger; private readonly ISignalRService _signalR; private readonly Guid tenantId; public ProjectController( UserHelper userHelper, ILoggingService logger, ISignalRService signalR, IProjectServices projectServices) { _userHelper = userHelper; _logger = logger; _signalR = signalR; _projectServices = projectServices; tenantId = userHelper.GetTenantId(); } #region =================================================================== Project Get APIs =================================================================== [HttpGet("list/basic")] public async Task GetAllProjectsBasic() { // Get the current user var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); var response = await _projectServices.GetAllProjectsBasicAsync(tenantId, loggedInEmployee); return StatusCode(response.StatusCode, response); } /// /// Retrieves a list of projects accessible to the current user, including aggregated details. /// This method is optimized to use a cache-first approach. If data is not in the cache, /// it fetches and aggregates data efficiently from the database in parallel. /// /// An ApiResponse containing a list of projects or an error. [HttpGet("list")] public async Task GetAllProjects() { // --- Input Validation and Initial Setup --- if (!ModelState.IsValid) { var errors = ModelState.Values .SelectMany(v => v.Errors) .Select(e => e.ErrorMessage) .ToList(); _logger.LogWarning("GetAllProjects called with invalid model state. Errors: {Errors}", string.Join(", ", errors)); return BadRequest(ApiResponse.ErrorResponse("Invalid request data provided.", errors, 400)); } var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); var response = await _projectServices.GetAllProjectsAsync(tenantId, loggedInEmployee); return StatusCode(response.StatusCode, response); } /// /// Retrieves details for a specific project by its ID. /// This endpoint is optimized with a cache-first strategy and parallel permission checks. /// /// The unique identifier of the project. /// An ApiResponse containing the project details or an appropriate error. [HttpGet("get/{id}")] public async Task GetProject([FromRoute] Guid id) { // --- Step 1: Input Validation --- if (!ModelState.IsValid) { var errors = ModelState.Values.SelectMany(v => v.Errors).Select(e => e.ErrorMessage).ToList(); _logger.LogWarning("Get project called with invalid model state for ID {ProjectId}. Errors: {Errors}", id, string.Join(", ", errors)); return BadRequest(ApiResponse.ErrorResponse("Invalid request data provided.", errors, 400)); } var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); var response = await _projectServices.GetProjectAsync(id, tenantId, loggedInEmployee); return StatusCode(response.StatusCode, response); } [HttpGet("details/{id}")] public async Task GetProjectDetails([FromRoute] Guid id) { // Step 1: Validate model state if (!ModelState.IsValid) { var errors = ModelState.Values .SelectMany(v => v.Errors) .Select(e => e.ErrorMessage) .ToList(); _logger.LogWarning("Invalid model state in Details endpoint. Errors: {@Errors}", errors); return BadRequest(ApiResponse.ErrorResponse("Invalid data", errors, 400)); } // Step 2: Get logged-in employee var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); var response = await _projectServices.GetProjectDetailsAsync(id, tenantId, loggedInEmployee); return StatusCode(response.StatusCode, response); } [HttpGet("details-old/{id}")] public async Task GetProjectDetailsOld([FromRoute] Guid id) { // ProjectDetailsVM vm = new ProjectDetailsVM(); if (!ModelState.IsValid) { var errors = ModelState.Values .SelectMany(v => v.Errors) .Select(e => e.ErrorMessage) .ToList(); return BadRequest(ApiResponse.ErrorResponse("Invalid data", errors, 400)); } var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); var response = await _projectServices.GetProjectDetailsOldAsync(id, tenantId, loggedInEmployee); return StatusCode(response.StatusCode, response); } #endregion #region =================================================================== Project Manage APIs =================================================================== [HttpPost] public async Task CreateProject([FromBody] CreateProjectDto projectDto) { // 1. Validate input first (early exit) if (!ModelState.IsValid) { var errors = ModelState.Values.SelectMany(v => v.Errors).Select(e => e.ErrorMessage).ToList(); return BadRequest(ApiResponse.ErrorResponse("Invalid data", errors, 400)); } // 2. Prepare data without I/O Employee loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); var response = await _projectServices.CreateProjectAsync(projectDto, tenantId, loggedInEmployee); if (response.Success) { var notification = new { LoggedInUserId = loggedInEmployee.Id, Keyword = "Create_Project", Response = response.Data }; await _signalR.SendNotificationAsync(notification); } return StatusCode(response.StatusCode, response); } /// /// Updates an existing project's details. /// This endpoint is secure, handles concurrency, and performs non-essential tasks in the background. /// /// The ID of the project to update. /// The data to update the project with. /// An ApiResponse confirming the update or an appropriate error. [HttpPut("update/{id}")] public async Task UpdateProject([FromRoute] Guid id, [FromBody] UpdateProjectDto updateProjectDto) { // --- Step 1: Input Validation --- if (!ModelState.IsValid) { var errors = ModelState.Values.SelectMany(v => v.Errors).Select(e => e.ErrorMessage).ToList(); _logger.LogWarning("Update project called with invalid model state for ID {ProjectId}. Errors: {Errors}", id, string.Join(", ", errors)); return BadRequest(ApiResponse.ErrorResponse("Invalid request data provided.", errors, 400)); } // --- Step 2: Prepare data without I/O --- Employee loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); var response = await _projectServices.UpdateProjectAsync(id, updateProjectDto, tenantId, loggedInEmployee); if (response.Success) { var notification = new { LoggedInUserId = loggedInEmployee.Id, Keyword = "Update_Project", Response = response.Data }; await _signalR.SendNotificationAsync(notification); } return StatusCode(response.StatusCode, response); } #endregion #region =================================================================== Project Allocation APIs =================================================================== [HttpGet("employees/get/{projectid?}/{includeInactive?}")] public async Task GetEmployeeByProjectId(Guid? projectId, bool includeInactive = false) { // --- Step 1: Input Validation --- if (!ModelState.IsValid) { var errors = ModelState.Values.SelectMany(v => v.Errors).Select(e => e.ErrorMessage).ToList(); _logger.LogWarning("Get employee list by ProjectId called with invalid model state \n Errors: {Errors}", string.Join(", ", errors)); return BadRequest(ApiResponse.ErrorResponse("Invalid request data provided.", errors, 400)); } // --- Step 2: Prepare data without I/O --- Employee loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); var response = await _projectServices.GetEmployeeByProjectIdAsync(projectId, includeInactive, tenantId, loggedInEmployee); return StatusCode(response.StatusCode, response); } [HttpGet("allocation/{projectId}")] public async Task GetProjectAllocation(Guid? projectId) { // --- Step 1: Input Validation --- if (!ModelState.IsValid) { var errors = ModelState.Values.SelectMany(v => v.Errors).Select(e => e.ErrorMessage).ToList(); _logger.LogWarning("Get employee list by ProjectId called with invalid model state \n Errors: {Errors}", string.Join(", ", errors)); return BadRequest(ApiResponse.ErrorResponse("Invalid request data provided.", errors, 400)); } // --- Step 2: Prepare data without I/O --- Employee loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); var response = await _projectServices.GetProjectAllocationAsync(projectId, tenantId, loggedInEmployee); return StatusCode(response.StatusCode, response); } [HttpPost("allocation")] public async Task ManageAllocation([FromBody] List projectAllocationDot) { // --- Step 1: Input Validation --- if (!ModelState.IsValid) { var errors = ModelState.Values.SelectMany(v => v.Errors).Select(e => e.ErrorMessage).ToList(); _logger.LogWarning("project Alocation called with invalid model state for list of projects. Errors: {Errors}", string.Join(", ", errors)); return BadRequest(ApiResponse.ErrorResponse("Invalid request data provided.", errors, 400)); } // --- Step 2: Prepare data without I/O --- Employee loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); var response = await _projectServices.ManageAllocationAsync(projectAllocationDot, tenantId, loggedInEmployee); if (response.Success) { List employeeIds = response.Data.Select(pa => pa.EmployeeId).ToList(); List projectIds = response.Data.Select(pa => pa.ProjectId).ToList(); var notification = new { LoggedInUserId = loggedInEmployee.Id, Keyword = "Assign_Project", ProjectIds = projectIds, EmployeeList = employeeIds }; await _signalR.SendNotificationAsync(notification); } return StatusCode(response.StatusCode, response); } [HttpGet("assigned-projects/{employeeId}")] public async Task GetProjectsByEmployee([FromRoute] Guid employeeId) { // --- Step 1: Input Validation --- if (!ModelState.IsValid) { var errors = ModelState.Values.SelectMany(v => v.Errors).Select(e => e.ErrorMessage).ToList(); _logger.LogWarning("Get project list by employee Id called with invalid model state \n Errors: {Errors}", string.Join(", ", errors)); return BadRequest(ApiResponse.ErrorResponse("Invalid request data provided.", errors, 400)); } // --- Step 2: Prepare data without I/O --- Employee loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); var response = await _projectServices.GetProjectsByEmployeeAsync(employeeId, tenantId, loggedInEmployee); return StatusCode(response.StatusCode, response); } [HttpGet("allocation-histery/{employeeId}")] public async Task GetProjectByEmployeeBasic([FromRoute] Guid employeeId) { // --- Step 1: Input Validation --- if (!ModelState.IsValid) { var errors = ModelState.Values.SelectMany(v => v.Errors).Select(e => e.ErrorMessage).ToList(); _logger.LogWarning("Get project list by employee Id called with invalid model state \n Errors: {Errors}", string.Join(", ", errors)); return BadRequest(ApiResponse.ErrorResponse("Invalid request data provided.", errors, 400)); } // --- Step 2: Prepare data without I/O --- Employee loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); var response = await _projectServices.GetProjectByEmployeeBasicAsync(employeeId, tenantId, loggedInEmployee); return StatusCode(response.StatusCode, response); } [HttpPost("assign-projects/{employeeId}")] public async Task AssigneProjectsToEmployee([FromBody] List projectAllocationDtos, [FromRoute] Guid employeeId) { // --- Step 1: Input Validation --- if (!ModelState.IsValid) { var errors = ModelState.Values.SelectMany(v => v.Errors).Select(e => e.ErrorMessage).ToList(); _logger.LogWarning("project Alocation called with invalid model state for list of projects. Errors: {Errors}", string.Join(", ", errors)); return BadRequest(ApiResponse.ErrorResponse("Invalid request data provided.", errors, 400)); } // --- Step 2: Prepare data without I/O --- Employee loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); var response = await _projectServices.AssigneProjectsToEmployeeAsync(projectAllocationDtos, employeeId, tenantId, loggedInEmployee); if (response.Success) { List projectIds = response.Data.Select(pa => pa.ProjectId).ToList(); var notification = new { LoggedInUserId = loggedInEmployee.Id, Keyword = "Assign_Project", ProjectIds = projectIds, EmployeeId = employeeId }; await _signalR.SendNotificationAsync(notification); } return StatusCode(response.StatusCode, response); } #endregion #region =================================================================== Project InfraStructure Get APIs =================================================================== [HttpGet("infra-details/{projectId}")] public async Task GetInfraDetails(Guid projectId) { // --- Step 1: Input Validation --- if (!ModelState.IsValid) { var errors = ModelState.Values.SelectMany(v => v.Errors).Select(e => e.ErrorMessage).ToList(); _logger.LogWarning("Get Project Infrastructure by ProjectId called with invalid model state \n Errors: {Errors}", string.Join(", ", errors)); return BadRequest(ApiResponse.ErrorResponse("Invalid request data provided.", errors, 400)); } // --- Step 2: Prepare data without I/O --- Employee loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); var response = await _projectServices.GetInfraDetailsAsync(projectId, tenantId, loggedInEmployee); return StatusCode(response.StatusCode, response); } [HttpGet("tasks/{workAreaId}")] public async Task GetWorkItems(Guid workAreaId) { // --- Step 1: Input Validation --- if (!ModelState.IsValid) { var errors = ModelState.Values.SelectMany(v => v.Errors).Select(e => e.ErrorMessage).ToList(); _logger.LogWarning("Get Work Items by WorkAreaId called with invalid model state \n Errors: {Errors}", string.Join(", ", errors)); return BadRequest(ApiResponse.ErrorResponse("Invalid request data provided.", errors, 400)); } // --- Step 2: Prepare data without I/O --- Employee loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); var response = await _projectServices.GetWorkItemsAsync(workAreaId, tenantId, loggedInEmployee); return StatusCode(response.StatusCode, response); } [HttpGet("tasks-employee/{employeeId}")] public async Task GetTasksByEmployee(Guid employeeId, [FromQuery] DateTime? fromDate, DateTime? toDate) { // --- Step 1: Input Validation --- if (!ModelState.IsValid) { var errors = ModelState.Values.SelectMany(v => v.Errors).Select(e => e.ErrorMessage).ToList(); _logger.LogWarning("Get Work Items by employeeId called with invalid model state \n Errors: {Errors}", string.Join(", ", errors)); return BadRequest(ApiResponse.ErrorResponse("Invalid request data provided.", errors, 400)); } if (!toDate.HasValue) toDate = DateTime.UtcNow; if (!fromDate.HasValue) fromDate = toDate.Value.AddDays(-7); // --- Step 2: Prepare data without I/O --- Employee loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); var response = await _projectServices.GetTasksByEmployeeAsync(employeeId, fromDate.Value, toDate.Value, tenantId, loggedInEmployee); return StatusCode(response.StatusCode, response); } #endregion #region =================================================================== Project Infrastructre Manage APIs =================================================================== [HttpPost("manage-infra")] public async Task ManageProjectInfra(List infraDtos) { // --- Step 1: Input Validation --- if (!ModelState.IsValid) { var errors = ModelState.Values.SelectMany(v => v.Errors).Select(e => e.ErrorMessage).ToList(); _logger.LogWarning("project Alocation called with invalid model state for list of projects. Errors: {Errors}", string.Join(", ", errors)); return BadRequest(ApiResponse.ErrorResponse("Invalid request data provided.", errors, 400)); } // --- Step 2: Prepare data without I/O --- Employee loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); var serviceResponse = await _projectServices.ManageProjectInfraAsync(infraDtos, tenantId, loggedInEmployee); var response = serviceResponse.Response; var notification = serviceResponse.Notification; if (notification != null) { await _signalR.SendNotificationAsync(notification); } return StatusCode(response.StatusCode, response); } [HttpPost("task")] public async Task CreateProjectTask([FromBody] List workItemDtos) { // --- Step 1: Input Validation --- if (!ModelState.IsValid) { var errors = ModelState.Values.SelectMany(v => v.Errors).Select(e => e.ErrorMessage).ToList(); _logger.LogWarning("project Alocation called with invalid model state for list of projects. Errors: {Errors}", string.Join(", ", errors)); return BadRequest(ApiResponse.ErrorResponse("Invalid request data provided.", errors, 400)); } // --- Step 2: Prepare data without I/O --- Employee loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); var response = await _projectServices.CreateProjectTaskAsync(workItemDtos, tenantId, loggedInEmployee); if (response.Success) { List workAreaIds = response.Data.Select(pa => pa.WorkItem?.WorkAreaId ?? Guid.Empty).ToList(); string message = response.Message; var notification = new { LoggedInUserId = loggedInEmployee.Id, Keyword = "WorkItem", WorkAreaIds = workAreaIds, Message = message }; await _signalR.SendNotificationAsync(notification); } return StatusCode(response.StatusCode, response); } [HttpDelete("task/{id}")] public async Task DeleteProjectTask(Guid id) { // --- Step 1: Input Validation --- if (!ModelState.IsValid) { var errors = ModelState.Values.SelectMany(v => v.Errors).Select(e => e.ErrorMessage).ToList(); _logger.LogWarning("project Alocation called with invalid model state for list of projects. Errors: {Errors}", string.Join(", ", errors)); return BadRequest(ApiResponse.ErrorResponse("Invalid request data provided.", errors, 400)); } // --- Step 2: Prepare data without I/O --- Employee loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); var serviceResponse = await _projectServices.DeleteProjectTaskAsync(id, tenantId, loggedInEmployee); var response = serviceResponse.Response; var notification = serviceResponse.Notification; if (notification != null) { await _signalR.SendNotificationAsync(notification); } return StatusCode(response.StatusCode, response); } #endregion #region =================================================================== Project-Level Permission APIs =================================================================== [HttpPost("assign/project-level-permission")] public async Task ManageProjectLevelPermission([FromBody] ProjctLevelPermissionDto model) { Employee loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); var response = await _projectServices.ManageProjectLevelPermissionAsync(model, tenantId, loggedInEmployee); return StatusCode(response.StatusCode, response); } [HttpGet("get/project-level-permission/employee/{employeeId}/project/{projectId}")] public async Task GetAssignedProjectLevelPermission(Guid employeeId, Guid projectId) { Employee loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); var response = await _projectServices.GetAssignedProjectLevelPermissionAsync(employeeId, projectId, tenantId, loggedInEmployee); return StatusCode(response.StatusCode, response); } [HttpGet("get/all/project-level-permission/{projectId}")] public async Task GetAllPermissionFroProject(Guid projectId) { Employee loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); var response = await _projectServices.GetAllPermissionFroProjectAsync(projectId, loggedInEmployee, tenantId); return StatusCode(response.StatusCode, response); } [HttpGet("get/proejct-level/modules")] public async Task AssignProjectLevelModules() { Employee loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); var response = await _projectServices.AssignProjectLevelModulesAsync(tenantId, loggedInEmployee); return StatusCode(response.StatusCode, response); } [HttpGet("get/proejct-level/employees/{projectId}")] public async Task GetEmployeeToWhomProjectLevelAssigned(Guid projectId) { Employee loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); var response = await _projectServices.GetEmployeeToWhomProjectLevelAssignedAsync(projectId, tenantId, loggedInEmployee); return StatusCode(response.StatusCode, response); } #endregion #region =================================================================== Assign Service APIs =================================================================== [HttpPost("assign/service")] public async Task AssignServiceToProject([FromBody] AssignServiceDto model) { Employee loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); var response = await _projectServices.AssignServiceToProjectAsync(model, tenantId, loggedInEmployee); if (response.Success) { string message = response.Message; var notification = new { LoggedInUserId = loggedInEmployee.Id, Keyword = "Assigned_Services", data = response.Data, Message = message }; await _signalR.SendNotificationAsync(notification); } return StatusCode(response.StatusCode, response); } [HttpPost("deassign/service")] public async Task DeassignServiceToProject([FromBody] DeassignServiceDto model) { Employee loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); var response = await _projectServices.DeassignServiceToProjectAsync(model, tenantId, loggedInEmployee); if (response.Success) { string message = response.Message; var notification = new { LoggedInUserId = loggedInEmployee.Id, Keyword = "Deassigned_Services", data = response.Data, Message = message }; await _signalR.SendNotificationAsync(notification); } return StatusCode(response.StatusCode, response); } #endregion } };