Merge branch 'Purchase_Invoice_Management' of https://git.marcoaiot.com/admin/marco.pms.api into Response_Encryption
This commit is contained in:
commit
35c92183a5
@ -0,0 +1,10 @@
|
||||
namespace Marco.Pms.Model.ViewModels.DashBoard
|
||||
{
|
||||
public class ProjectAttendanceOverviewVM
|
||||
{
|
||||
public Guid ProjectId { get; set; }
|
||||
public string? ProjectName { get; set; }
|
||||
public int TeamCount { get; set; }
|
||||
public int AttendanceCount { get; set; }
|
||||
}
|
||||
}
|
||||
@ -175,7 +175,7 @@ namespace MarcoBMS.Services.Controllers
|
||||
|
||||
var hasTeamAttendancePermission = await _permission.HasPermission(PermissionsMaster.TeamAttendance, loggedInEmployee.Id);
|
||||
var hasSelfAttendancePermission = await _permission.HasPermission(PermissionsMaster.SelfAttendance, loggedInEmployee.Id);
|
||||
var hasProjectPermission = await _permission.HasProjectPermission(loggedInEmployee, projectId);
|
||||
var hasProjectPermission = await _permission.HasInfraProjectPermission(loggedInEmployee.Id, projectId);
|
||||
|
||||
if (!hasProjectPermission)
|
||||
{
|
||||
@ -354,7 +354,7 @@ namespace MarcoBMS.Services.Controllers
|
||||
return NotFound(ApiResponse<object>.ErrorResponse("Project not found."));
|
||||
}
|
||||
|
||||
if (!await _permission.HasProjectPermission(loggedInEmployee, projectId))
|
||||
if (!await _permission.HasInfraProjectPermission(loggedInEmployee.Id, projectId))
|
||||
{
|
||||
_logger.LogWarning("Unauthorized access attempt by EmployeeId: {EmployeeId} for ProjectId: {ProjectId}", loggedInEmployee.Id, projectId);
|
||||
return Unauthorized(ApiResponse<object>.ErrorResponse("You do not have permission to access this project."));
|
||||
@ -400,7 +400,7 @@ namespace MarcoBMS.Services.Controllers
|
||||
|
||||
Employee LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
|
||||
var result = new List<EmployeeAttendanceVM>();
|
||||
var hasProjectPermission = await _permission.HasProjectPermission(LoggedInEmployee, projectId);
|
||||
var hasProjectPermission = await _permission.HasInfraProjectPermission(LoggedInEmployee.Id, projectId);
|
||||
|
||||
if (!hasProjectPermission)
|
||||
{
|
||||
|
||||
@ -270,7 +270,7 @@ namespace Marco.Pms.Services.Controllers
|
||||
using var scope = _serviceScopeFactory.CreateScope();
|
||||
var _permission = scope.ServiceProvider.GetRequiredService<PermissionServices>();
|
||||
// Security Check: Ensure the requested project is in the user's accessible list.
|
||||
var hasPermission = await _permission.HasProjectPermission(loggedInEmployee, projectId.Value);
|
||||
var hasPermission = await _permission.HasInfraProjectPermission(loggedInEmployee.Id, projectId.Value);
|
||||
if (!hasPermission)
|
||||
{
|
||||
_logger.LogWarning("Access DENIED for user {UserId} on project {ProjectId} (not active or not accessible).", loggedInEmployee.Id, projectId.Value);
|
||||
@ -359,7 +359,7 @@ namespace Marco.Pms.Services.Controllers
|
||||
var _permission = scope.ServiceProvider.GetRequiredService<PermissionServices>();
|
||||
|
||||
// 2a. Security Check: Verify permission for the specific project.
|
||||
var hasPermission = await _permission.HasProjectPermission(loggedInEmployee, projectId.Value);
|
||||
var hasPermission = await _permission.HasInfraProjectPermission(loggedInEmployee.Id, projectId.Value);
|
||||
if (!hasPermission)
|
||||
{
|
||||
_logger.LogWarning("Access DENIED for user {UserId} on project {ProjectId}.", loggedInEmployee.Id, projectId.Value);
|
||||
@ -690,7 +690,7 @@ namespace Marco.Pms.Services.Controllers
|
||||
using var scope = _serviceScopeFactory.CreateScope();
|
||||
var _permission = scope.ServiceProvider.GetRequiredService<PermissionServices>();
|
||||
|
||||
bool hasPermission = await _permission.HasProjectPermission(loggedInEmployee!, projectId);
|
||||
bool hasPermission = await _permission.HasInfraProjectPermission(loggedInEmployee.Id, projectId);
|
||||
if (!hasPermission)
|
||||
{
|
||||
_logger.LogWarning("Unauthorized access by EmployeeId: {EmployeeId} to ProjectId: {ProjectId}", loggedInEmployee.Id, projectId);
|
||||
@ -1708,5 +1708,122 @@ namespace Marco.Pms.Services.Controllers
|
||||
|
||||
return Ok(ApiResponse<object>.SuccessResponse(response, "job progression fetched successfully", 200));
|
||||
}
|
||||
|
||||
[HttpGet("project/attendance-overview")]
|
||||
public async Task<IActionResult> GetProjectAttendanceOverViewAsync([FromQuery] DateTime? date, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. Validation and Setup
|
||||
if (tenantId == Guid.Empty)
|
||||
{
|
||||
_logger.LogWarning("GetProjectAttendanceOverView: Invalid request - TenantId is empty.");
|
||||
return BadRequest(ApiResponse<object>.ErrorResponse("Invalid TenantId", "Provided Invalid TenantId", 400));
|
||||
}
|
||||
|
||||
// Default to UTC Today if null, ensuring only Date component is used
|
||||
var targetDate = date?.Date ?? DateTime.UtcNow.Date;
|
||||
|
||||
_logger.LogInfo("GetProjectAttendanceOverView: Starting fetch for Tenant {TenantId} on Date {Date}", tenantId, targetDate);
|
||||
|
||||
try
|
||||
{
|
||||
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
|
||||
if (loggedInEmployee == null)
|
||||
{
|
||||
_logger.LogWarning("GetProjectAttendanceOverView: Employee not found for current user.");
|
||||
return Unauthorized(ApiResponse<object>.ErrorResponse("Unauthorized", "Employee profile not found.", 401));
|
||||
}
|
||||
|
||||
// 2. Permission Check
|
||||
using var scope = _serviceScopeFactory.CreateScope();
|
||||
var _permission = scope.ServiceProvider.GetRequiredService<PermissionServices>();
|
||||
|
||||
var hasPermission = await _permission.HasPermission(PermissionsMaster.ManageProject, loggedInEmployee.Id);
|
||||
|
||||
// 3. Determine Scope of Projects (Filtering Project IDs)
|
||||
// We select only the IDs first to keep the memory footprint low before aggregation
|
||||
var projectQuery = _context.ProjectAllocations
|
||||
.AsNoTracking()
|
||||
.Where(pa => pa.TenantId == tenantId && pa.IsActive);
|
||||
|
||||
if (!hasPermission)
|
||||
{
|
||||
// If no admin permission, restrict to projects the employee is allocated to
|
||||
projectQuery = projectQuery.Where(pa => pa.EmployeeId == loggedInEmployee.Id);
|
||||
}
|
||||
|
||||
var visibleProjectIds = await projectQuery
|
||||
.Select(pa => pa.ProjectId)
|
||||
.Distinct()
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
if (!visibleProjectIds.Any())
|
||||
{
|
||||
return Ok(ApiResponse<List<ProjectAttendanceOverviewVM>>.SuccessResponse(new List<ProjectAttendanceOverviewVM>(), "No projects found.", 200));
|
||||
}
|
||||
|
||||
// 4. Parallel Data Fetching (Optimization)
|
||||
// We fetch Project Details/Allocations AND Attendance counts separately to avoid complex Cartesian products in SQL
|
||||
|
||||
// Query A: Get Project Details and Total Allocation Counts
|
||||
var projectsTask = _context.ProjectAllocations
|
||||
.AsNoTracking()
|
||||
.Where(pa => pa.TenantId == tenantId &&
|
||||
pa.IsActive &&
|
||||
visibleProjectIds.Contains(pa.ProjectId) &&
|
||||
pa.Project != null)
|
||||
.GroupBy(pa => new { pa.ProjectId, pa.Project!.Name })
|
||||
.Select(g => new
|
||||
{
|
||||
ProjectId = g.Key.ProjectId,
|
||||
Name = g.Key.Name,
|
||||
TeamCount = g.Count()
|
||||
})
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
// Query B: Get Attendance Counts for the specific date
|
||||
await using var context = await _dbContextFactory.CreateDbContextAsync();
|
||||
var attendanceTask = context.Attendes
|
||||
.AsNoTracking()
|
||||
.Where(a => a.TenantId == tenantId &&
|
||||
visibleProjectIds.Contains(a.ProjectID) &&
|
||||
a.AttendanceDate.Date == targetDate)
|
||||
.GroupBy(a => a.ProjectID)
|
||||
.Select(g => new
|
||||
{
|
||||
ProjectId = g.Key,
|
||||
Count = g.Count()
|
||||
})
|
||||
.ToDictionaryAsync(k => k.ProjectId, v => v.Count, cancellationToken);
|
||||
|
||||
await Task.WhenAll(projectsTask, attendanceTask);
|
||||
|
||||
var projects = await projectsTask;
|
||||
var attendanceMap = await attendanceTask;
|
||||
|
||||
// 5. In-Memory Projection
|
||||
// Merging the two datasets efficiently
|
||||
var response = projects.Select(p => new ProjectAttendanceOverviewVM
|
||||
{
|
||||
ProjectId = p.ProjectId,
|
||||
ProjectName = p.Name,
|
||||
TeamCount = p.TeamCount,
|
||||
// O(1) Lookup from the dictionary
|
||||
AttendanceCount = attendanceMap.ContainsKey(p.ProjectId) ? attendanceMap[p.ProjectId] : 0
|
||||
})
|
||||
.OrderBy(p => p.ProjectName)
|
||||
.ToList();
|
||||
|
||||
_logger.LogInfo("GetProjectAttendanceOverView: Successfully fetched {Count} projects for Tenant {TenantId}", response.Count, tenantId);
|
||||
|
||||
return Ok(ApiResponse<List<ProjectAttendanceOverviewVM>>.SuccessResponse(response, "Attendance overview fetched successfully", 200));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "GetProjectAttendanceOverView: An unexpected error occurred for Tenant {TenantId}", tenantId);
|
||||
// Do not expose raw Exception details to client in production
|
||||
return StatusCode(500, ApiResponse<object>.ErrorResponse("Internal Server Error", "An error occurred while processing your request.", 500));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@ -97,7 +97,7 @@ namespace Marco.Pms.Services.Controllers
|
||||
// Project permission check
|
||||
if (ProjectEntity == entityTypeId)
|
||||
{
|
||||
var hasProjectPermission = await _permission.HasProjectPermission(loggedInEmployee, entityId);
|
||||
var hasProjectPermission = await _permission.HasInfraProjectPermission(loggedInEmployee.Id, entityId);
|
||||
if (!hasProjectPermission)
|
||||
{
|
||||
_logger.LogWarning("Employee {EmployeeId} does not have project access for ProjectId {ProjectId}", loggedInEmployee.Id, entityId);
|
||||
@ -1086,7 +1086,7 @@ namespace Marco.Pms.Services.Controllers
|
||||
entityExists = await _context.Projects.AnyAsync(p => p.Id == oldAttachment.EntityId && p.TenantId == tenantId);
|
||||
if (entityExists)
|
||||
{
|
||||
entityExists = await _permission.HasProjectPermission(loggedInEmployee, oldAttachment.EntityId);
|
||||
entityExists = await _permission.HasInfraProjectPermission(loggedInEmployee.Id, oldAttachment.EntityId);
|
||||
}
|
||||
}
|
||||
else
|
||||
|
||||
@ -154,7 +154,7 @@ namespace MarcoBMS.Services.Controllers
|
||||
return NotFound(ApiResponse<object>.ErrorResponse("Project not found", "Project not found", 404));
|
||||
}
|
||||
// Check if the logged-in employee has permission for the requested project
|
||||
var hasProjectPermission = await _permission.HasProjectPermission(loggedInEmployee, projectId);
|
||||
var hasProjectPermission = await _permission.HasInfraProjectPermission(loggedInEmployee.Id, projectId);
|
||||
if (!hasProjectPermission)
|
||||
{
|
||||
_logger.LogWarning("User {EmployeeId} attempts to get employees for project {ProjectId} without permission", loggedInEmployee.Id, projectId);
|
||||
@ -334,7 +334,7 @@ namespace MarcoBMS.Services.Controllers
|
||||
var employeeQuery = _context.Employees.Where(e => e.IsActive);
|
||||
if (projectId != null && projectId != Guid.Empty)
|
||||
{
|
||||
var hasProjectPermission = await _permission.HasProjectPermission(loggedInEmployee, projectId.Value);
|
||||
var hasProjectPermission = await _permission.HasInfraProjectPermission(loggedInEmployee.Id, projectId.Value);
|
||||
if (!hasProjectPermission)
|
||||
{
|
||||
_logger.LogWarning("User {EmployeeId} attempts to get employee for project {ProjectId}, but not have access to the project", loggedInEmployee.Id, projectId);
|
||||
@ -402,7 +402,7 @@ namespace MarcoBMS.Services.Controllers
|
||||
loggedInEmployee.Id, projectId);
|
||||
|
||||
// Validate project access permission
|
||||
var hasProjectPermission = await _permission.HasProjectPermission(loggedInEmployee, projectId.Value);
|
||||
var hasProjectPermission = await _permission.HasInfraProjectPermission(loggedInEmployee.Id, projectId.Value);
|
||||
if (!hasProjectPermission)
|
||||
{
|
||||
_logger.LogWarning("Access denied. EmployeeId: {EmployeeId} does not have permission for ProjectId: {ProjectId}",
|
||||
@ -608,202 +608,156 @@ namespace MarcoBMS.Services.Controllers
|
||||
return Ok(ApiResponse<object>.SuccessResponse("Success.", responsemessage, 200));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Manages employee creation or update operations within the current tenant context.
|
||||
/// Supports both new employee onboarding and existing employee profile modifications.
|
||||
/// Enforces tenant isolation, seat limits, email uniqueness, and application access validation.
|
||||
/// </summary>
|
||||
/// <param name="model">Employee data containing creation or update information.</param>
|
||||
/// <returns>Employee view model on success or structured error response.</returns>
|
||||
/// <response code="200">Employee created or updated successfully.</response>
|
||||
/// <response code="400">Invalid request data or business rule violations.</response>
|
||||
/// <response code="404">Employee not found for update operation.</response>
|
||||
/// <response code="409">Business constraint violation (duplicate email, seat limit exceeded).</response>
|
||||
/// <response code="500">Internal server error during database operations.</response>
|
||||
[HttpPost("manage")]
|
||||
public async Task<IActionResult> CreateEmployeeAsync([FromBody] CreateUserDto model)
|
||||
public async Task<IActionResult> ManageEmployeeFromWebAsync([FromBody] CreateUserDto model)
|
||||
{
|
||||
// Correlation and context capture for logs
|
||||
// Correlation ID for distributed tracing across services and logs
|
||||
var correlationId = HttpContext.TraceIdentifier;
|
||||
|
||||
_logger.LogInfo("ManageEmployeeFromWebAsync started. TenantId: {TenantId}, CorrelationId: {CorrelationId}, IsUpdate: {IsUpdate}",
|
||||
tenantId, correlationId, model.Id.HasValue);
|
||||
|
||||
// 1. EARLY GUARD CLAUSES - Fail fast with structured validation
|
||||
if (tenantId == Guid.Empty)
|
||||
{
|
||||
_logger.LogWarning("Invalid tenant context. TenantId: {TenantId}, CorrelationId: {CorrelationId}", tenantId, correlationId);
|
||||
return BadRequest(ApiResponse<object>.ErrorResponse("Invalid tenant", "Valid tenant context is required for employee management", 400));
|
||||
}
|
||||
|
||||
if (model == null)
|
||||
{
|
||||
_logger.LogWarning("Null model received. TenantId: {TenantId}, CorrelationId: {CorrelationId}", tenantId, correlationId);
|
||||
return BadRequest(ApiResponse<object>.ErrorResponse("Invalid payload", "Employee data is required in request body", 400));
|
||||
}
|
||||
|
||||
// Application access requires valid email (business rule enforcement)
|
||||
if (model.HasApplicationAccess && string.IsNullOrWhiteSpace(model.Email))
|
||||
{
|
||||
_logger.LogWarning("Application access requested without email. TenantId: {TenantId}, CorrelationId: {CorrelationId}", tenantId, correlationId);
|
||||
return BadRequest(ApiResponse<object>.ErrorResponse("Missing email", "Application users must have a valid email address", 400));
|
||||
}
|
||||
|
||||
// 2. AUTHENTICATED USER CONTEXT (single query, cached in UserHelper)
|
||||
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
|
||||
|
||||
// 3. ENTERPRISE-GRADE ERROR HANDLING WITH TRANSACTION SCOPING
|
||||
await using var transaction = await _context.Database.BeginTransactionAsync();
|
||||
try
|
||||
{
|
||||
if (model == null)
|
||||
ApiResponse<EmployeeVM> response;
|
||||
|
||||
if (model.Id.HasValue && model.Id != Guid.Empty)
|
||||
{
|
||||
_logger.LogWarning("Model is null in CreateEmployeeAsync");
|
||||
return BadRequest(ApiResponse<object>.ErrorResponse("Invalid payload", "Request body is required", 400));
|
||||
// UPDATE PATH: Validate existence first (AsNoTracking for read-only check)
|
||||
_logger.LogDebug("Processing employee update. EmployeeId: {EmployeeId}, TenantId: {TenantId}, CorrelationId: {CorrelationId}",
|
||||
model.Id, tenantId, correlationId);
|
||||
|
||||
var existingEmployee = await _context.Employees
|
||||
.AsNoTracking()
|
||||
.FirstOrDefaultAsync(e => e.Id == model.Id && e.TenantId == tenantId);
|
||||
|
||||
if (existingEmployee == null)
|
||||
{
|
||||
_logger.LogWarning("Employee not found for update. EmployeeId: {EmployeeId}, TenantId: {TenantId}, CorrelationId: {CorrelationId}",
|
||||
model.Id, tenantId, correlationId);
|
||||
return NotFound(ApiResponse<object>.ErrorResponse("Employee not found", $"Employee with ID {model.Id} not found in tenant {tenantId}", 404));
|
||||
}
|
||||
|
||||
// Track application access state change for audit
|
||||
bool oldHasApplicationAccess = existingEmployee.HasApplicationAccess;
|
||||
_mapper.Map(model, existingEmployee);
|
||||
|
||||
response = await UpdateEmployeeAsync(oldHasApplicationAccess, existingEmployee, loggedInEmployee);
|
||||
}
|
||||
else
|
||||
{
|
||||
// CREATE PATH: New employee onboarding
|
||||
_logger.LogDebug("Processing new employee creation. Email: {Email}, TenantId: {TenantId}, CorrelationId: {CorrelationId}",
|
||||
model.Email ?? "<null>", tenantId, correlationId);
|
||||
|
||||
var newEmployee = _mapper.Map<Employee>(model);
|
||||
newEmployee.IsSystem = false;
|
||||
newEmployee.IsActive = true;
|
||||
newEmployee.IsPrimary = false;
|
||||
|
||||
response = await CreateEmployeeAsync(newEmployee, loggedInEmployee);
|
||||
}
|
||||
|
||||
// Basic validation
|
||||
if (model.HasApplicationAccess && string.IsNullOrWhiteSpace(model.Email))
|
||||
{
|
||||
_logger.LogWarning("Application access requested but email is missing");
|
||||
return BadRequest(ApiResponse<object>.ErrorResponse("Invalid email", "Application users must have a valid email", 400));
|
||||
}
|
||||
|
||||
await using var transaction = await _context.Database.BeginTransactionAsync();
|
||||
try
|
||||
{
|
||||
// Load existing employee if updating, constrained by organization scope
|
||||
Employee? existingEmployee = null;
|
||||
if (model.Id.HasValue && model.Id.Value != Guid.Empty)
|
||||
{
|
||||
existingEmployee = await _context.Employees
|
||||
.FirstOrDefaultAsync(e => e.Id == model.Id);
|
||||
if (existingEmployee == null)
|
||||
{
|
||||
_logger.LogInfo("Employee not found for update. Id={EmployeeId}", model.Id);
|
||||
return NotFound(ApiResponse<object>.ErrorResponse("Employee not found", "Employee not found in database", 404));
|
||||
}
|
||||
}
|
||||
|
||||
// Identity user creation path (only if needed)
|
||||
ApplicationUser? identityUserToCreate = null;
|
||||
ApplicationUser? createdIdentityUser = null;
|
||||
|
||||
if (model.HasApplicationAccess)
|
||||
{
|
||||
// Only attempt identity resolution/creation if email supplied and either:
|
||||
// - Creating new employee, or
|
||||
// - Updating but existing employee does not have ApplicationUserId
|
||||
var needsIdentity = string.IsNullOrWhiteSpace(existingEmployee?.ApplicationUserId);
|
||||
if (needsIdentity && !string.IsNullOrWhiteSpace(model.Email))
|
||||
{
|
||||
var existingUser = await _userManager.FindByEmailAsync(model.Email);
|
||||
if (existingUser == null)
|
||||
{
|
||||
// Seat check only when provisioning a new identity user
|
||||
var isSeatsAvailable = await _generalHelper.CheckSeatsRemainingAsync(tenantId);
|
||||
if (!isSeatsAvailable)
|
||||
{
|
||||
_logger.LogWarning("Maximum users reached for Tenant {TenantId}. Cannot create identity user for {Email}", tenantId, model.Email);
|
||||
return BadRequest(ApiResponse<object>.ErrorResponse(
|
||||
"Maximum number of users reached. Cannot add new user",
|
||||
"Maximum number of users reached. Cannot add new user", 400));
|
||||
}
|
||||
|
||||
identityUserToCreate = new ApplicationUser
|
||||
{
|
||||
UserName = model.Email,
|
||||
Email = model.Email,
|
||||
EmailConfirmed = true
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
// If identity exists, re-use it; do not re-create
|
||||
createdIdentityUser = existingUser;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// For create path: enforce uniqueness of employee email if applicable to business rules
|
||||
// Consider adding a unique filtered index: (OrganizationId, Email) WHERE Email IS NOT NULL
|
||||
if (!model.Id.HasValue || model.Id == Guid.Empty)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(model.Email))
|
||||
{
|
||||
var emailExists = await _context.Employees
|
||||
.AnyAsync(e => e.Email == model.Email);
|
||||
if (emailExists)
|
||||
{
|
||||
_logger.LogInfo("Employee email already exists. Email={Email}", model.Email);
|
||||
return StatusCode(403, ApiResponse<object>.ErrorResponse(
|
||||
"Employee with email already exists",
|
||||
"Employee with this email already exists", 403));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create identity user if needed
|
||||
if (identityUserToCreate != null && !string.IsNullOrWhiteSpace(identityUserToCreate.Email))
|
||||
{
|
||||
var createResult = await _userManager.CreateAsync(identityUserToCreate, "User@123");
|
||||
if (!createResult.Succeeded)
|
||||
{
|
||||
_logger.LogWarning("Failed to create identity user for {Email}. Errors={Errors}",
|
||||
identityUserToCreate.Email,
|
||||
string.Join(", ", createResult.Errors.Select(e => $"{e.Code}:{e.Description}")));
|
||||
return BadRequest(ApiResponse<object>.ErrorResponse("Failed to create user", createResult.Errors, 400));
|
||||
}
|
||||
|
||||
createdIdentityUser = identityUserToCreate;
|
||||
_logger.LogInfo("Identity user created. IdentityUserId={UserId}, Email={Email}",
|
||||
createdIdentityUser.Id, createdIdentityUser.Email);
|
||||
}
|
||||
|
||||
|
||||
Guid employeeId;
|
||||
EmployeeVM employeeVM;
|
||||
string responseMessage;
|
||||
|
||||
if (existingEmployee != null)
|
||||
{
|
||||
// Update flow
|
||||
_mapper.Map(model, existingEmployee);
|
||||
|
||||
if (createdIdentityUser != null && !string.IsNullOrWhiteSpace(createdIdentityUser.Email))
|
||||
{
|
||||
existingEmployee.ApplicationUserId = createdIdentityUser.Id;
|
||||
await SendResetIfApplicableAsync(createdIdentityUser, existingEmployee.FirstName ?? "User");
|
||||
}
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
employeeId = existingEmployee.Id;
|
||||
employeeVM = _mapper.Map<EmployeeVM>(existingEmployee);
|
||||
responseMessage = "Employee Updated Successfully";
|
||||
|
||||
_logger.LogInfo("Employee updated. EmployeeId={EmployeeId}, Org={OrgId}", employeeId, existingEmployee.OrganizationId);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Create flow
|
||||
var newEmployee = _mapper.Map<Employee>(model);
|
||||
newEmployee.IsSystem = false;
|
||||
newEmployee.IsActive = true;
|
||||
newEmployee.IsPrimary = false;
|
||||
|
||||
if (createdIdentityUser != null && !string.IsNullOrWhiteSpace(createdIdentityUser.Email))
|
||||
{
|
||||
newEmployee.ApplicationUserId = createdIdentityUser.Id;
|
||||
await SendResetIfApplicableAsync(createdIdentityUser, newEmployee.FirstName ?? "User");
|
||||
}
|
||||
|
||||
await _context.Employees.AddAsync(newEmployee);
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
employeeId = newEmployee.Id;
|
||||
employeeVM = _mapper.Map<EmployeeVM>(newEmployee);
|
||||
responseMessage = "Employee Created Successfully";
|
||||
|
||||
_logger.LogInfo("Employee created. EmployeeId={EmployeeId}, Org={OrgId}", employeeId, newEmployee.OrganizationId);
|
||||
}
|
||||
|
||||
await transaction.CommitAsync();
|
||||
|
||||
// SignalR notification
|
||||
var notification = new
|
||||
{
|
||||
LoggedInUserId = loggedInEmployee.Id,
|
||||
Keyword = "Employee",
|
||||
EmployeeId = employeeId
|
||||
};
|
||||
|
||||
// Consider broadcasting to tenant/organization group instead of Clients.All to avoid cross-tenant leaks:
|
||||
// await _signalR.Clients.Group($"org:{model.OrganizationId}").SendAsync("NotificationEventHandler", notification);
|
||||
await _signalR.Clients.All.SendAsync("NotificationEventHandler", notification);
|
||||
|
||||
_logger.LogInfo("Notification broadcasted for EmployeeId={EmployeeId}", employeeId);
|
||||
|
||||
return Ok(ApiResponse<object>.SuccessResponse(employeeVM, responseMessage, 200));
|
||||
}
|
||||
catch (DbUpdateException dbEx)
|
||||
// 4. BUSINESS LOGIC SUCCESS VALIDATION
|
||||
if (!response.Success)
|
||||
{
|
||||
_logger.LogWarning("Business logic failed during employee management. TenantId: {TenantId}, CorrelationId: {CorrelationId}, Error: {Error}",
|
||||
tenantId, correlationId, response.Message);
|
||||
await transaction.RollbackAsync();
|
||||
_logger.LogError(dbEx, "Database exception occurred while managing employee");
|
||||
return StatusCode(500, ApiResponse<object>.ErrorResponse(
|
||||
"Internal exception occurred",
|
||||
"Internal database exception has occurred", 500));
|
||||
return StatusCode(response.StatusCode, response);
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
||||
// 5. COMMIT TRANSACTION AND TENANT-SCOPED NOTIFICATION
|
||||
await transaction.CommitAsync();
|
||||
|
||||
_logger.LogInfo("Employee operation completed successfully. EmployeeId: {EmployeeId}, Operation: {Operation}, TenantId: {TenantId}, CorrelationId: {CorrelationId}",
|
||||
response.Data.Id, (model.Id.HasValue ? "Update" : "Create"), tenantId, correlationId);
|
||||
|
||||
// Tenant-scoped SignalR notification (prevents cross-tenant leakage)
|
||||
var notification = new
|
||||
{
|
||||
await transaction.RollbackAsync();
|
||||
_logger.LogError(ex, "Unhandled exception occurred while managing employee");
|
||||
return StatusCode(500, ApiResponse<object>.ErrorResponse(
|
||||
"Internal exception occurred",
|
||||
"Internal exception has occurred", 500));
|
||||
}
|
||||
TenantId = tenantId,
|
||||
LoggedInUserId = loggedInEmployee.Id,
|
||||
Keyword = "Employee",
|
||||
EmployeeId = response.Data.Id
|
||||
};
|
||||
|
||||
await _signalR.Clients.All
|
||||
.SendAsync("NotificationEventHandler", notification);
|
||||
|
||||
|
||||
_logger.LogDebug("Tenant-scoped SignalR notification sent. EmployeeId: {EmployeeId}, TenantId: {TenantId}, CorrelationId: {CorrelationId}",
|
||||
response.Data.Id, tenantId, correlationId);
|
||||
|
||||
return StatusCode(response.StatusCode, response);
|
||||
}
|
||||
catch (DbUpdateConcurrencyException dbConcurrencyEx)
|
||||
{
|
||||
await transaction.RollbackAsync();
|
||||
_logger.LogError(dbConcurrencyEx, "Concurrency conflict during employee management. TenantId: {TenantId}, CorrelationId: {CorrelationId}",
|
||||
tenantId, correlationId);
|
||||
return Conflict(ApiResponse<object>.ErrorResponse("Concurrency conflict", "Employee data was modified by another user. Please refresh and try again.", 409));
|
||||
}
|
||||
catch (DbUpdateException dbEx)
|
||||
{
|
||||
await transaction.RollbackAsync();
|
||||
_logger.LogError(dbEx, "Database constraint violation during employee management. TenantId: {TenantId}, CorrelationId: {CorrelationId}",
|
||||
tenantId, correlationId);
|
||||
return StatusCode(500, ApiResponse<object>.ErrorResponse("Database error", "Database constraint violation occurred. Please check data and try again.", 500));
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
_logger.LogWarning("Request cancelled during employee management. TenantId: {TenantId}, CorrelationId: {CorrelationId}",
|
||||
tenantId, correlationId);
|
||||
return StatusCode(499, ApiResponse<object>.ErrorResponse("Request cancelled", "Request was cancelled by client", 499));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
await transaction.RollbackAsync();
|
||||
_logger.LogError(ex, "Unexpected error during employee management. TenantId: {TenantId}, CorrelationId: {CorrelationId}",
|
||||
tenantId, correlationId);
|
||||
return StatusCode(500, ApiResponse<object>.ErrorResponse("Internal server error", "An unexpected error occurred. Please contact support if issue persists.", 500));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
[HttpPost("manage-mobile")]
|
||||
public async Task<IActionResult> CreateUserMoblie([FromBody] MobileUserManageDto model)
|
||||
{
|
||||
@ -1265,5 +1219,149 @@ namespace MarcoBMS.Services.Controllers
|
||||
await _emailSender.SendResetPasswordEmailOnRegister(u.Email ?? "", firstName, resetLink);
|
||||
_logger.LogInfo("Reset password email queued. Email={Email}", u.Email ?? "");
|
||||
}
|
||||
|
||||
private static string? CapitalizeFirst(string? text)
|
||||
{
|
||||
if (string.IsNullOrEmpty(text))
|
||||
return text;
|
||||
|
||||
return char.ToUpper(text[0]) + text.Substring(1);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates an employee in the database.
|
||||
/// </summary>
|
||||
/// <param name="employee">The employee to create.</param>
|
||||
/// <param name="loggedInEmployee">The currently logged in employee.</param>
|
||||
/// <returns>An ApiResponse containing the created employee or an error response.</returns>
|
||||
private async Task<ApiResponse<EmployeeVM>> CreateEmployeeAsync(Employee employee, Employee loggedInEmployee)
|
||||
{
|
||||
// Check if the employee has application access and email is provided
|
||||
if (employee.HasApplicationAccess && !string.IsNullOrWhiteSpace(employee.Email))
|
||||
{
|
||||
// Check if the email already exists in the database
|
||||
var emailExists = await _context.Employees.AsNoTracking().AnyAsync(e => e.Email == employee.Email && e.HasApplicationAccess);
|
||||
if (emailExists)
|
||||
{
|
||||
_logger.LogWarning("Email already exists in database. Email={Email}", employee.Email);
|
||||
return ApiResponse<EmployeeVM>.ErrorResponse("Email already exists", "Email already exists in database", 409);
|
||||
}
|
||||
|
||||
// Check if the user with the email already exists in the identity system
|
||||
var user = await _userManager.FindByEmailAsync(employee.Email);
|
||||
if (user == null)
|
||||
{
|
||||
// Create a new identity user if the user does not exist
|
||||
var newUser = new ApplicationUser
|
||||
{
|
||||
UserName = employee.Email,
|
||||
Email = employee.Email,
|
||||
EmailConfirmed = true
|
||||
};
|
||||
|
||||
var createResult = await _userManager.CreateAsync(newUser, "User@123");
|
||||
if (!createResult.Succeeded)
|
||||
{
|
||||
_logger.LogWarning("Failed to create identity user for {Email}. Errors={Errors}",
|
||||
newUser.Email!,
|
||||
string.Join(", ", createResult.Errors.Select(e => $"{e.Code}:{e.Description}")));
|
||||
return ApiResponse<EmployeeVM>.ErrorResponse("Failed to create user", createResult.Errors, 400);
|
||||
}
|
||||
user = newUser;
|
||||
}
|
||||
|
||||
if (user == null)
|
||||
{
|
||||
_logger.LogWarning("User not found for {Email}", employee.Email ?? "<null>");
|
||||
return ApiResponse<EmployeeVM>.ErrorResponse("User not found", "User not found in database", 400);
|
||||
}
|
||||
|
||||
// Set the application user ID for the employee
|
||||
employee.ApplicationUserId = user.Id;
|
||||
|
||||
// Send a password reset if applicable
|
||||
await SendResetIfApplicableAsync(user, employee.FirstName ?? "User");
|
||||
}
|
||||
|
||||
// Add the employee to the database
|
||||
await _context.Employees.AddAsync(employee);
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
// Map the employee to a view model
|
||||
var employeeVM = _mapper.Map<EmployeeVM>(employee);
|
||||
|
||||
// Return a success response with the created employee
|
||||
return ApiResponse<EmployeeVM>.SuccessResponse(employeeVM, "Employee Created Successfully", 201);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates an employee in the database.
|
||||
/// </summary>
|
||||
/// <param name="oldHasApplicationAccess">Whether the employee previously had application access.</param>
|
||||
/// <param name="employee">The employee to update.</param>
|
||||
/// <param name="loggedInEmployee">The currently logged in employee.</param>
|
||||
/// <returns>An ApiResponse containing the updated employee view model.</returns>
|
||||
private async Task<ApiResponse<EmployeeVM>> UpdateEmployeeAsync(bool oldHasApplicationAccess, Employee employee, Employee loggedInEmployee)
|
||||
{
|
||||
// Check if the employee is gaining application access and has an email
|
||||
if (!oldHasApplicationAccess && employee.HasApplicationAccess && !string.IsNullOrWhiteSpace(employee.Email))
|
||||
{
|
||||
// Check if the email already exists in the database
|
||||
var emailExists = await _context.Employees.AsNoTracking().FirstOrDefaultAsync(e => e.Email == employee.Email && e.Id != employee.Id && e.HasApplicationAccess);
|
||||
|
||||
if (emailExists != null)
|
||||
{
|
||||
// Log warning and return error response if email already exists
|
||||
_logger.LogWarning("Email already exists in database. Email={Email}", employee.Email ?? "<null>");
|
||||
return ApiResponse<EmployeeVM>.ErrorResponse("Email already exists", "Email already exists in database", 409);
|
||||
}
|
||||
|
||||
// Check if the user with the email already exists in the identity system
|
||||
var user = await _userManager.FindByEmailAsync(employee.Email);
|
||||
if (user == null)
|
||||
{
|
||||
// Create a new user with the email in the identity system
|
||||
var newUser = new ApplicationUser
|
||||
{
|
||||
UserName = employee.Email,
|
||||
Email = employee.Email,
|
||||
EmailConfirmed = true
|
||||
};
|
||||
|
||||
var createResult = await _userManager.CreateAsync(newUser, "User@123");
|
||||
if (!createResult.Succeeded)
|
||||
{
|
||||
// Log warning and return error response if user creation failed
|
||||
_logger.LogWarning("Failed to create identity user for {Email}. Errors={Errors}",
|
||||
newUser.Email!,
|
||||
string.Join(", ", createResult.Errors.Select(e => $"{e.Code}:{e.Description}")));
|
||||
return ApiResponse<EmployeeVM>.ErrorResponse("Failed to create user", createResult.Errors, 400);
|
||||
}
|
||||
user = newUser;
|
||||
}
|
||||
|
||||
if (user == null)
|
||||
{
|
||||
// Log warning and return error response if user not found
|
||||
_logger.LogWarning("User not found for {Email}", employee.Email ?? "<null>");
|
||||
return ApiResponse<EmployeeVM>.ErrorResponse("User not found", "User not found in database", 400);
|
||||
}
|
||||
|
||||
// Set the application user id on the employee
|
||||
employee.ApplicationUserId = user.Id;
|
||||
|
||||
// Send a password reset email if applicable
|
||||
await SendResetIfApplicableAsync(user, employee.FirstName ?? "User");
|
||||
}
|
||||
|
||||
// Update the employee in the database
|
||||
_context.Employees.Update(employee);
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
// Map the employee to a view model and return a success response
|
||||
var employeeVM = _mapper.Map<EmployeeVM>(employee);
|
||||
return ApiResponse<EmployeeVM>.SuccessResponse(employeeVM, "Employee Updated Successfully", 200);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@ -64,7 +64,7 @@ namespace Marco.Pms.Services.Controllers
|
||||
}
|
||||
|
||||
// Step 2: Check project access permission
|
||||
var hasPermission = await _permission.HasProjectPermission(loggedInEmployee, projectId);
|
||||
var hasPermission = await _permission.HasInfraProjectPermission(loggedInEmployee.Id, projectId);
|
||||
if (!hasPermission)
|
||||
{
|
||||
_logger.LogWarning("[GetImageList] Access denied for EmployeeId: {EmployeeId} on ProjectId: {ProjectId}", loggedInEmployee.Id, projectId);
|
||||
|
||||
@ -258,10 +258,10 @@ namespace Marco.Pms.Services.Controllers
|
||||
|
||||
[HttpGet]
|
||||
[Route("activities")]
|
||||
public async Task<IActionResult> GetActivitiesMaster([FromQuery] Guid? activityGroupId)
|
||||
public async Task<IActionResult> GetActivitiesMaster([FromQuery] Guid? activityGroupId, [FromQuery] string? searchString)
|
||||
{
|
||||
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
|
||||
var response = await _masterService.GetActivitiesMasterAsync(activityGroupId, loggedInEmployee, tenantId);
|
||||
var response = await _masterService.GetActivitiesMasterAsync(activityGroupId, searchString, loggedInEmployee, tenantId);
|
||||
return StatusCode(response.StatusCode, response);
|
||||
}
|
||||
|
||||
|
||||
@ -153,6 +153,11 @@ namespace Marco.Pms.Services.Service
|
||||
.Include(e => e.Currency)
|
||||
.Where(e => e.TenantId == tenantId); // Always filter by TenantId first.
|
||||
|
||||
//using var scope = _serviceScopeFactory.CreateScope();
|
||||
//var _projectServices = scope.ServiceProvider.GetRequiredService<IProjectServices>();
|
||||
|
||||
//var allprojectIds = await _projectServices.GetBothProjectIdsAsync(loggedInEmployee.Id, tenantId);
|
||||
|
||||
if (cacheList == null)
|
||||
{
|
||||
//await _cache.AddExpensesListToCache(expenses: await expensesQuery.ToListAsync(), tenantId);
|
||||
@ -594,90 +599,6 @@ namespace Marco.Pms.Services.Service
|
||||
}
|
||||
}
|
||||
|
||||
#region Helper Methods (Private)
|
||||
|
||||
/// <summary>
|
||||
/// Fetches the list of possible next statuses based on current status.
|
||||
/// </summary>
|
||||
private async Task<List<ExpensesStatusMaster>> GetNextStatusesAsync(Guid? currentStatusId, Guid tenantId)
|
||||
{
|
||||
if (!currentStatusId.HasValue) return new List<ExpensesStatusMaster>();
|
||||
|
||||
return await _context.ExpensesStatusMapping
|
||||
.AsNoTracking()
|
||||
.Include(esm => esm.NextStatus).ThenInclude(s => s!.StatusPermissionMappings)
|
||||
.Where(esm => esm.StatusId == currentStatusId && esm.Status != null)
|
||||
.Select(esm => esm.NextStatus!) // Select only the NextStatus entity
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Filters statuses by permission and reorders specific actions (e.g., Reject).
|
||||
/// </summary>
|
||||
private List<ExpensesStatusMasterVM> ProcessNextStatuses(List<ExpensesStatusMaster> nextStatuses, Guid createdById, Guid loggedInEmployeeId, List<Guid> userPermissionIds)
|
||||
{
|
||||
if (nextStatuses == null || !nextStatuses.Any()) return new List<ExpensesStatusMasterVM>();
|
||||
|
||||
// Business Logic: Move "Reject" to the top
|
||||
var rejectStatus = nextStatuses.FirstOrDefault(ns => ns.DisplayName == "Reject");
|
||||
if (rejectStatus != null)
|
||||
{
|
||||
nextStatuses.Remove(rejectStatus);
|
||||
nextStatuses.Insert(0, rejectStatus);
|
||||
}
|
||||
|
||||
var resultVMs = new List<ExpensesStatusMasterVM>();
|
||||
|
||||
foreach (var item in nextStatuses)
|
||||
{
|
||||
var vm = _mapper.Map<ExpensesStatusMasterVM>(item);
|
||||
|
||||
// Case 1: If Creator is viewing and status is Review (Assuming Review is a constant GUID or Enum)
|
||||
if (item.Id == Review && createdById == loggedInEmployeeId)
|
||||
{
|
||||
resultVMs.Add(vm);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Case 2: Standard Permission Check
|
||||
bool hasPermission = vm.PermissionIds.Any(pid => userPermissionIds.Contains(pid));
|
||||
|
||||
// Exclude "Done" status (Assuming Done is a constant GUID)
|
||||
if (hasPermission && item.Id != Done)
|
||||
{
|
||||
resultVMs.Add(vm);
|
||||
}
|
||||
}
|
||||
|
||||
return resultVMs.Distinct().ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to fetch project details from Projects table, falling back to ServiceProjects.
|
||||
/// </summary>
|
||||
private async Task<BasicProjectVM?> GetProjectDetailsAsync(Guid? projectId, Guid tenantId)
|
||||
{
|
||||
if (!projectId.HasValue) return null;
|
||||
|
||||
// Check Infrastructure Projects
|
||||
var infraProject = await _context.Projects
|
||||
.AsNoTracking()
|
||||
.Where(p => p.Id == projectId && p.TenantId == tenantId)
|
||||
.ProjectTo<BasicProjectVM>(_mapper.ConfigurationProvider) // Optimized: Project directly to VM inside SQL
|
||||
.FirstOrDefaultAsync();
|
||||
|
||||
if (infraProject != null) return infraProject;
|
||||
|
||||
// Fallback to Service Projects
|
||||
return await _context.ServiceProjects
|
||||
.AsNoTracking()
|
||||
.Where(sp => sp.Id == projectId && sp.TenantId == tenantId)
|
||||
.ProjectTo<BasicProjectVM>(_mapper.ConfigurationProvider)
|
||||
.FirstOrDefaultAsync();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
public async Task<ApiResponse<object>> GetSupplerNameListAsync(Employee loggedInEmployee, Guid tenantId)
|
||||
{
|
||||
try
|
||||
@ -4367,6 +4288,85 @@ namespace Marco.Pms.Services.Service
|
||||
return CreateExpenseAttachmentEntities(batchId, expense.Id, employeeId, tenantId, objectKey, attachment);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fetches the list of possible next statuses based on current status.
|
||||
/// </summary>
|
||||
private async Task<List<ExpensesStatusMaster>> GetNextStatusesAsync(Guid? currentStatusId, Guid tenantId)
|
||||
{
|
||||
if (!currentStatusId.HasValue) return new List<ExpensesStatusMaster>();
|
||||
|
||||
return await _context.ExpensesStatusMapping
|
||||
.AsNoTracking()
|
||||
.Include(esm => esm.NextStatus).ThenInclude(s => s!.StatusPermissionMappings)
|
||||
.Where(esm => esm.StatusId == currentStatusId && esm.Status != null)
|
||||
.Select(esm => esm.NextStatus!) // Select only the NextStatus entity
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Filters statuses by permission and reorders specific actions (e.g., Reject).
|
||||
/// </summary>
|
||||
private List<ExpensesStatusMasterVM> ProcessNextStatuses(List<ExpensesStatusMaster> nextStatuses, Guid createdById, Guid loggedInEmployeeId, List<Guid> userPermissionIds)
|
||||
{
|
||||
if (nextStatuses == null || !nextStatuses.Any()) return new List<ExpensesStatusMasterVM>();
|
||||
|
||||
// Business Logic: Move "Reject" to the top
|
||||
var rejectStatus = nextStatuses.FirstOrDefault(ns => ns.DisplayName == "Reject");
|
||||
if (rejectStatus != null)
|
||||
{
|
||||
nextStatuses.Remove(rejectStatus);
|
||||
nextStatuses.Insert(0, rejectStatus);
|
||||
}
|
||||
|
||||
var resultVMs = new List<ExpensesStatusMasterVM>();
|
||||
|
||||
foreach (var item in nextStatuses)
|
||||
{
|
||||
var vm = _mapper.Map<ExpensesStatusMasterVM>(item);
|
||||
|
||||
// Case 1: If Creator is viewing and status is Review (Assuming Review is a constant GUID or Enum)
|
||||
if (item.Id == Review && createdById == loggedInEmployeeId)
|
||||
{
|
||||
resultVMs.Add(vm);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Case 2: Standard Permission Check
|
||||
bool hasPermission = vm.PermissionIds.Any(pid => userPermissionIds.Contains(pid));
|
||||
|
||||
// Exclude "Done" status (Assuming Done is a constant GUID)
|
||||
if (hasPermission && item.Id != Done)
|
||||
{
|
||||
resultVMs.Add(vm);
|
||||
}
|
||||
}
|
||||
|
||||
return resultVMs.Distinct().ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to fetch project details from Projects table, falling back to ServiceProjects.
|
||||
/// </summary>
|
||||
private async Task<BasicProjectVM?> GetProjectDetailsAsync(Guid? projectId, Guid tenantId)
|
||||
{
|
||||
if (!projectId.HasValue) return null;
|
||||
|
||||
// Check Infrastructure Projects
|
||||
var infraProject = await _context.Projects
|
||||
.AsNoTracking()
|
||||
.Where(p => p.Id == projectId && p.TenantId == tenantId)
|
||||
.ProjectTo<BasicProjectVM>(_mapper.ConfigurationProvider) // Optimized: Project directly to VM inside SQL
|
||||
.FirstOrDefaultAsync();
|
||||
|
||||
if (infraProject != null) return infraProject;
|
||||
|
||||
// Fallback to Service Projects
|
||||
return await _context.ServiceProjects
|
||||
.AsNoTracking()
|
||||
.Where(sp => sp.Id == projectId && sp.TenantId == tenantId)
|
||||
.ProjectTo<BasicProjectVM>(_mapper.ConfigurationProvider)
|
||||
.FirstOrDefaultAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A private static helper method to create Document and BillAttachment entities.
|
||||
|
||||
@ -521,6 +521,7 @@ namespace Marco.Pms.Services.Service
|
||||
var services = await _context.ServiceMasters
|
||||
.Where(s => s.TenantId == tenantId && s.IsActive)
|
||||
.Select(s => _mapper.Map<ServiceMasterVM>(s))
|
||||
.OrderBy(s => s.Name)
|
||||
.ToListAsync();
|
||||
|
||||
_logger.LogInfo("Fetched {Count} service records for tenantId: {TenantId}", services.Count, tenantId);
|
||||
@ -628,11 +629,11 @@ namespace Marco.Pms.Services.Service
|
||||
IsSystem = a.IsSystem,
|
||||
CheckLists = _mapper.Map<List<CheckListVM>>(checklistForActivity)
|
||||
};
|
||||
}).ToList()
|
||||
}).ToList()
|
||||
}).OrderBy(a => a.ActivityName).ToList()
|
||||
}).OrderBy(ag => ag.Name).ToList()
|
||||
};
|
||||
return response;
|
||||
}).ToList();
|
||||
}).OrderBy(s => s.Name).ToList();
|
||||
|
||||
_logger.LogInfo("Successfully processed and mapped {ServiceCount} services for TenantId: {TenantId}", Vm.Count, tenantId);
|
||||
|
||||
@ -836,6 +837,7 @@ namespace Marco.Pms.Services.Service
|
||||
|
||||
var activityGroups = await activityGroupQuery
|
||||
.Select(ag => _mapper.Map<ActivityGroupMasterVM>(ag))
|
||||
.OrderBy(ag => ag.Name)
|
||||
.ToListAsync();
|
||||
|
||||
_logger.LogInfo("{Count} activity group(s) fetched for tenantId: {TenantId}", activityGroups.Count, tenantId);
|
||||
@ -1032,7 +1034,7 @@ namespace Marco.Pms.Services.Service
|
||||
#endregion
|
||||
|
||||
#region =================================================================== Activity APIs ===================================================================
|
||||
public async Task<ApiResponse<object>> GetActivitiesMasterAsync(Guid? activityGroupId, Employee loggedInEmployee, Guid tenantId)
|
||||
public async Task<ApiResponse<object>> GetActivitiesMasterAsync(Guid? activityGroupId, string? searchString, Employee loggedInEmployee, Guid tenantId)
|
||||
{
|
||||
_logger.LogInfo("GetActivitiesMaster called");
|
||||
|
||||
@ -1050,6 +1052,11 @@ namespace Marco.Pms.Services.Service
|
||||
activityQuery = activityQuery.Where(a => a.ActivityGroupId == activityGroupId);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(searchString))
|
||||
{
|
||||
activityQuery = activityQuery.Where(a => a.ActivityName.Contains(searchString));
|
||||
}
|
||||
|
||||
var activities = await activityQuery
|
||||
.ToListAsync();
|
||||
|
||||
@ -1081,7 +1088,7 @@ namespace Marco.Pms.Services.Service
|
||||
response.CheckLists = _mapper.Map<List<CheckListVM>>(checklistForActivity);
|
||||
return response;
|
||||
|
||||
}).ToList();
|
||||
}).OrderBy(a => a.ActivityName).ToList();
|
||||
|
||||
_logger.LogInfo("{Count} activity records fetched successfully for tenantId: {TenantId}", activityVMs.Count, tenantId);
|
||||
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
using Marco.Pms.DataAccess.Data;
|
||||
using Marco.Pms.Model.Employees;
|
||||
using Marco.Pms.Model.Entitlements;
|
||||
using Marco.Pms.Services.Helpers;
|
||||
using MarcoBMS.Services.Helpers;
|
||||
@ -50,29 +49,28 @@ namespace Marco.Pms.Services.Service
|
||||
var hasPermission = toCheckPermissionIds.Any(f => realPermissionIds.Contains(f));
|
||||
return hasPermission;
|
||||
}
|
||||
public async Task<bool> HasProjectPermission(Employee LoggedInEmployee, Guid projectId)
|
||||
public async Task<bool> HasInfraProjectPermission(Guid loggedInEmployeeId, Guid projectId)
|
||||
{
|
||||
var employeeId = LoggedInEmployee.Id;
|
||||
var projectIds = await _cache.GetProjects(employeeId, tenantId);
|
||||
var projectIds = await _cache.GetProjects(loggedInEmployeeId, tenantId);
|
||||
|
||||
if (projectIds == null)
|
||||
{
|
||||
var hasPermission = await HasPermission(PermissionsMaster.ManageProject, employeeId);
|
||||
var hasPermission = await HasPermission(PermissionsMaster.ManageProject, loggedInEmployeeId);
|
||||
if (hasPermission)
|
||||
{
|
||||
var projects = await _context.Projects.AsNoTracking().Where(c => c.TenantId == LoggedInEmployee.TenantId).ToListAsync();
|
||||
var projects = await _context.Projects.AsNoTracking().Where(c => c.TenantId == tenantId).ToListAsync();
|
||||
projectIds = projects.Select(p => p.Id).ToList();
|
||||
}
|
||||
else
|
||||
{
|
||||
var allocation = await _context.ProjectAllocations.AsNoTracking().Where(c => c.EmployeeId == employeeId && c.IsActive).ToListAsync();
|
||||
var allocation = await _context.ProjectAllocations.AsNoTracking().Where(c => c.EmployeeId == loggedInEmployeeId && c.IsActive).ToListAsync();
|
||||
if (!allocation.Any())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
projectIds = allocation.Select(c => c.ProjectId).Distinct().ToList();
|
||||
}
|
||||
await _cache.AddProjects(LoggedInEmployee.Id, projectIds, tenantId);
|
||||
await _cache.AddProjects(loggedInEmployeeId, projectIds, tenantId);
|
||||
}
|
||||
return projectIds.Contains(projectId);
|
||||
}
|
||||
|
||||
@ -54,89 +54,144 @@ namespace Marco.Pms.Services.Service
|
||||
|
||||
#region =================================================================== Project Get APIs ===================================================================\
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves a combined list of basic infrastructure and active service projects accessible by the logged-in employee within a tenant.
|
||||
/// </summary>
|
||||
/// <param name="searchString">Optional search term to filter projects by name (if implemented).</param>
|
||||
/// <param name="loggedInEmployee">Authenticated employee requesting the data.</param>
|
||||
/// <param name="tenantId">Tenant identifier to ensure multi-tenant data isolation.</param>
|
||||
/// <returns>Returns an ApiResponse containing the distinct combined list of basic project view models or an error response.</returns>
|
||||
public async Task<ApiResponse<object>> GetBothProjectBasicListAsync(Guid? id, string? searchString, Employee loggedInEmployee, Guid tenantId)
|
||||
public async Task<ApiResponse<List<BasicProjectVM>>> GetBothProjectBasicListAsync(Guid? id, string? searchString, Employee loggedInEmployee, Guid tenantId)
|
||||
{
|
||||
// 1. Validation and Context Checks
|
||||
if (tenantId == Guid.Empty)
|
||||
{
|
||||
_logger.LogWarning("GetBothProjectBasicListAsync called with invalid tenant context by EmployeeId {EmployeeId}", loggedInEmployee.Id);
|
||||
return ApiResponse<object>.ErrorResponse("Access Denied", "Invalid tenant context.", 403);
|
||||
_logger.LogWarning("Security Alert: GetBothProjectBasicListAsync called with empty TenantId by Employee: {EmployeeId}", loggedInEmployee.Id);
|
||||
return ApiResponse<List<BasicProjectVM>>.ErrorResponse("Access Denied", "Invalid tenant context provided.", 403);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// Retrieve list of project IDs accessible by the employee for tenant isolation and security
|
||||
var accessibleProjectIds = await GetMyProjects(loggedInEmployee, tenantId);
|
||||
_logger.LogInfo("Initiating project fetch for Tenant: {TenantId}, User: {UserId}. Search: {Search}", tenantId, loggedInEmployee.Id, searchString ?? "None");
|
||||
|
||||
// Fetch infrastructure projects concurrently filtered by accessible IDs and tenant
|
||||
var infraProjectTask = Task.Run(async () =>
|
||||
// 2. Check Permissions (Global check, do not block parallel tasks if possible, but needed for logic)
|
||||
// usage of scoped service for permission check
|
||||
using var scope = _serviceScopeFactory.CreateScope();
|
||||
var permissionService = scope.ServiceProvider.GetRequiredService<PermissionServices>();
|
||||
bool hasManagePermission = await permissionService.HasPermission(PermissionsMaster.ManageProject, loggedInEmployee.Id);
|
||||
|
||||
// 3. Define Parallel Tasks
|
||||
// We use DbContextFactory to create short-lived contexts for safe parallel execution.
|
||||
|
||||
// --- TASK A: Infrastructure Projects ---
|
||||
var infraTask = Task.Run(async () =>
|
||||
{
|
||||
await using var context = await _dbContextFactory.CreateDbContextAsync();
|
||||
var infraProjectsQuery = context.Projects
|
||||
.Where(p => accessibleProjectIds.Contains(p.Id) && p.TenantId == tenantId);
|
||||
using var context = await _dbContextFactory.CreateDbContextAsync();
|
||||
|
||||
// Base Query
|
||||
var query = context.Projects.AsNoTracking()
|
||||
.Where(p => p.TenantId == tenantId);
|
||||
|
||||
// Apply Filters
|
||||
if (id.HasValue)
|
||||
{
|
||||
query = query.Where(p => p.Id == id.Value);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(searchString))
|
||||
{
|
||||
var normalized = searchString.Trim().ToLowerInvariant();
|
||||
infraProjectsQuery = infraProjectsQuery
|
||||
.Where(p => p.Name.ToLower().Contains(normalized) ||
|
||||
(!string.IsNullOrWhiteSpace(p.ShortName) && p.ShortName.ToLower().Contains(normalized)));
|
||||
}
|
||||
if (id.HasValue)
|
||||
{
|
||||
infraProjectsQuery = infraProjectsQuery.Where(p => p.Id == id.Value);
|
||||
// Normalize search term. NOTE: Check if your DB Collation is already Case Insensitive (CI).
|
||||
// If DB is CI, you don't need ToLower(). Assuming standard needed:
|
||||
string normalized = searchString.Trim();
|
||||
query = query.Where(p => p.Name.Contains(normalized) ||
|
||||
(p.ShortName != null && p.ShortName.Contains(normalized)));
|
||||
}
|
||||
|
||||
var infraProjects = await infraProjectsQuery.ToListAsync();
|
||||
return infraProjects.Select(p => _mapper.Map<BasicProjectVM>(p)).ToList();
|
||||
// Apply Security (Row Level Access)
|
||||
if (!hasManagePermission)
|
||||
{
|
||||
// Optimization: Use Any() to create an EXISTS clause in SQL rather than fetching IDs to memory
|
||||
query = query.Where(p => context.ProjectAllocations.Any(pa =>
|
||||
pa.ProjectId == p.Id &&
|
||||
pa.EmployeeId == loggedInEmployee.Id &&
|
||||
pa.IsActive &&
|
||||
pa.TenantId == tenantId));
|
||||
}
|
||||
|
||||
// Projection: Select only what is needed before hitting DB
|
||||
return await query
|
||||
.ProjectTo<BasicProjectVM>(_mapper.ConfigurationProvider)
|
||||
.ToListAsync();
|
||||
});
|
||||
|
||||
// Fetch active service projects concurrently with tenant isolation
|
||||
var serviceProjectTask = Task.Run(async () =>
|
||||
|
||||
// --- TASK B: Service Projects ---
|
||||
var serviceTask = Task.Run(async () =>
|
||||
{
|
||||
await using var context = await _dbContextFactory.CreateDbContextAsync();
|
||||
var serviceProjectsQuery = context.ServiceProjects
|
||||
using var context = await _dbContextFactory.CreateDbContextAsync();
|
||||
|
||||
var query = context.ServiceProjects.AsNoTracking()
|
||||
.Where(sp => sp.TenantId == tenantId && sp.IsActive);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(searchString))
|
||||
{
|
||||
var normalized = searchString.Trim().ToLowerInvariant();
|
||||
serviceProjectsQuery = serviceProjectsQuery
|
||||
.Where(sp => sp.Name.ToLower().Contains(normalized) ||
|
||||
(!string.IsNullOrWhiteSpace(sp.ShortName) && sp.ShortName.ToLower().Contains(normalized)));
|
||||
}
|
||||
|
||||
if (id.HasValue)
|
||||
{
|
||||
serviceProjectsQuery = serviceProjectsQuery.Where(sp => sp.Id == id.Value);
|
||||
query = query.Where(sp => sp.Id == id.Value);
|
||||
}
|
||||
|
||||
var serviceProjects = await serviceProjectsQuery.ToListAsync();
|
||||
return serviceProjects.Select(sp => _mapper.Map<BasicProjectVM>(sp)).ToList();
|
||||
if (!string.IsNullOrWhiteSpace(searchString))
|
||||
{
|
||||
string normalized = searchString.Trim();
|
||||
query = query.Where(sp => sp.Name.Contains(normalized) ||
|
||||
(sp.ShortName != null && sp.ShortName.Contains(normalized)));
|
||||
}
|
||||
|
||||
if (!hasManagePermission)
|
||||
{
|
||||
// Optimization: Complex security filter pushed to DB
|
||||
// User has access if: Allocated directly OR Mapped via JobTicket
|
||||
query = query.Where(sp =>
|
||||
// Condition 1: Direct Allocation
|
||||
context.ServiceProjectAllocations.Any(spa =>
|
||||
spa.ProjectId == sp.Id &&
|
||||
spa.EmployeeId == loggedInEmployee.Id &&
|
||||
spa.TenantId == tenantId &&
|
||||
spa.IsActive)
|
||||
||
|
||||
// Condition 2: Job Ticket Mapping
|
||||
context.JobEmployeeMappings.Any(jem =>
|
||||
jem.JobTicket != null &&
|
||||
jem.JobTicket.ProjectId == sp.Id &&
|
||||
jem.AssigneeId == loggedInEmployee.Id &&
|
||||
jem.TenantId == tenantId)
|
||||
);
|
||||
}
|
||||
|
||||
return await query
|
||||
.ProjectTo<BasicProjectVM>(_mapper.ConfigurationProvider)
|
||||
.ToListAsync();
|
||||
});
|
||||
|
||||
// Wait for both concurrent tasks to complete
|
||||
await Task.WhenAll(infraProjectTask, serviceProjectTask);
|
||||
// 4. Await Completion
|
||||
await Task.WhenAll(infraTask, serviceTask);
|
||||
|
||||
// Combine, remove duplicates, and prepare response list
|
||||
var combinedProjects = infraProjectTask.Result.Concat(serviceProjectTask.Result).OrderBy(p => p.Name).Distinct().ToList();
|
||||
// 5. Aggregate Results
|
||||
// DistinctBy is available in .NET 6+. If older, use GroupBy or distinct comparer.
|
||||
var infraResults = await infraTask;
|
||||
var serviceResults = await serviceTask;
|
||||
|
||||
_logger.LogInfo("GetBothProjectBasicListAsync returning {Count} projects for tenant {TenantId} by EmployeeId {EmployeeId}",
|
||||
combinedProjects.Count, tenantId, loggedInEmployee.Id);
|
||||
var combinedProjects = infraResults
|
||||
.Concat(serviceResults)
|
||||
.DistinctBy(p => p.Id) // Ensure no duplicate IDs if cross-contamination exists
|
||||
.OrderBy(p => p.Name)
|
||||
.ToList();
|
||||
|
||||
return ApiResponse<object>.SuccessResponse(combinedProjects, "Service and infrastructure projects fetched successfully.", 200);
|
||||
_logger.LogInfo("Successfully fetched {Count} projects ({InfraCount} Infra, {ServiceCount} Service) for User {UserId}.",
|
||||
combinedProjects.Count, infraResults.Count, serviceResults.Count, loggedInEmployee.Id);
|
||||
|
||||
return ApiResponse<List<BasicProjectVM>>.SuccessResponse(combinedProjects, "Projects retrieved successfully.", 200);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
_logger.LogWarning("Project fetch operation was canceled by the client.");
|
||||
return ApiResponse<List<BasicProjectVM>>.ErrorResponse("Request Canceled", "The operation was canceled.", 499);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Unexpected error in GetBothProjectBasicListAsync for tenant {TenantId} by EmployeeId {EmployeeId}",
|
||||
tenantId, loggedInEmployee.Id);
|
||||
return ApiResponse<object>.ErrorResponse("Internal Server Error", "An error occurred while fetching projects.", 500);
|
||||
_logger.LogError(ex, "CRITICAL: Failed to fetch projects for Tenant {TenantId}. Error: {Message}", tenantId, ex.Message);
|
||||
return ApiResponse<List<BasicProjectVM>>.ErrorResponse("Internal Server Error", "An unexpected error occurred while processing your request.", 500);
|
||||
}
|
||||
}
|
||||
public async Task<ApiResponse<object>> GetAllProjectsBasicAsync(bool provideAll, Employee loggedInEmployee, Guid tenantId)
|
||||
@ -249,7 +304,9 @@ namespace Marco.Pms.Services.Service
|
||||
responseVms = responseVms
|
||||
.OrderBy(p => p.Name)
|
||||
.Skip((pageNumber - 1) * pageSize)
|
||||
.Take(pageSize).ToList();
|
||||
.Take(pageSize)
|
||||
.OrderBy(p => p.ShortName)
|
||||
.ToList();
|
||||
|
||||
// --- Step 4: Return the combined result ---
|
||||
|
||||
@ -278,7 +335,7 @@ namespace Marco.Pms.Services.Service
|
||||
var _permission = scope.ServiceProvider.GetRequiredService<PermissionServices>();
|
||||
// --- Step 1: Run independent operations in PARALLEL ---
|
||||
// We can check permissions and fetch data at the same time to reduce latency.
|
||||
var permissionTask = _permission.HasProjectPermission(loggedInEmployee, id);
|
||||
var permissionTask = _permission.HasInfraProjectPermission(loggedInEmployee.Id, id);
|
||||
|
||||
// This helper method encapsulates the "cache-first, then database" logic.
|
||||
var projectDataTask = GetProjectDataAsync(id, tenantId);
|
||||
@ -333,7 +390,7 @@ namespace Marco.Pms.Services.Service
|
||||
}
|
||||
|
||||
// Step 2: Check permission for this specific project
|
||||
var hasProjectPermission = await _permission.HasProjectPermission(loggedInEmployee, id);
|
||||
var hasProjectPermission = await _permission.HasInfraProjectPermission(loggedInEmployee.Id, id);
|
||||
if (!hasProjectPermission)
|
||||
{
|
||||
_logger.LogWarning("Project-specific access denied. EmployeeId: {EmployeeId}, ProjectId: {ProjectId}", loggedInEmployee.Id, id);
|
||||
@ -649,7 +706,7 @@ namespace Marco.Pms.Services.Service
|
||||
}
|
||||
|
||||
// 1c. Security Check
|
||||
var hasPermission = await _permission.HasProjectPermission(loggedInEmployee, id);
|
||||
var hasPermission = await _permission.HasInfraProjectPermission(loggedInEmployee.Id, id);
|
||||
if (!hasPermission)
|
||||
{
|
||||
_logger.LogWarning("Access DENIED for user {UserId} attempting to update project {ProjectId}.", loggedInEmployee.Id, id);
|
||||
@ -743,7 +800,7 @@ namespace Marco.Pms.Services.Service
|
||||
// --- CRITICAL: Security Check ---
|
||||
// Before fetching data, you MUST verify the user has permission to see it.
|
||||
// This is a placeholder for your actual permission logic.
|
||||
var hasProjectPermission = await _permission.HasProjectPermission(loggedInEmployee, projectId);
|
||||
var hasProjectPermission = await _permission.HasInfraProjectPermission(loggedInEmployee.Id, projectId);
|
||||
var hasAllEmployeePermission = await _permission.HasPermission(PermissionsMaster.ViewAllEmployees, loggedInEmployee.Id);
|
||||
var hasviewTeamPermission = await _permission.HasPermission(PermissionsMaster.ViewTeamMembers, loggedInEmployee.Id, projectId);
|
||||
|
||||
@ -824,7 +881,7 @@ namespace Marco.Pms.Services.Service
|
||||
// --- Step 2: Security and Existence Checks ---
|
||||
// Before fetching data, you MUST verify the user has permission to see it.
|
||||
// This is a placeholder for your actual permission logic.
|
||||
var hasPermission = await _permission.HasProjectPermission(loggedInEmployee, projectId);
|
||||
var hasPermission = await _permission.HasInfraProjectPermission(loggedInEmployee.Id, projectId);
|
||||
if (!hasPermission)
|
||||
{
|
||||
_logger.LogWarning("Access DENIED for user {UserId} on project {ProjectId}.", loggedInEmployee.Id, projectId);
|
||||
@ -1333,7 +1390,7 @@ namespace Marco.Pms.Services.Service
|
||||
}
|
||||
|
||||
// Check if the logged-in employee has permission for the requested project
|
||||
var hasProjectPermission = await _permission.HasProjectPermission(loggedInEmployee, projectId);
|
||||
var hasProjectPermission = await _permission.HasInfraProjectPermission(loggedInEmployee.Id, projectId);
|
||||
if (!hasProjectPermission)
|
||||
{
|
||||
_logger.LogWarning("User {EmployeeId} attempts to get employees for project {ProjectId} without permission", loggedInEmployee.Id, projectId);
|
||||
@ -1379,8 +1436,7 @@ namespace Marco.Pms.Services.Service
|
||||
|
||||
return ApiResponse<object>.SuccessResponse(result, "Employee list fetched successfully", 200);
|
||||
}
|
||||
public async Task<ApiResponse<object>> GetProjectTeamByServiceAndOrganizationAsync(
|
||||
Guid projectId, Guid? serviceId, Guid? organizationId, Employee loggedInEmployee, Guid tenantId)
|
||||
public async Task<ApiResponse<object>> GetProjectTeamByServiceAndOrganizationAsync(Guid projectId, Guid? serviceId, Guid? organizationId, Employee loggedInEmployee, Guid tenantId)
|
||||
{
|
||||
_logger.LogDebug("Started fetching project team. ProjectId: {ProjectId}, ServiceId: {ServiceId}, OrganizationId: {OrganizationId}, TenantId: {TenantId}, EmployeeId: {EmployeeId}",
|
||||
projectId, serviceId ?? Guid.Empty, organizationId ?? Guid.Empty, tenantId, loggedInEmployee.Id);
|
||||
@ -1416,7 +1472,7 @@ namespace Marco.Pms.Services.Service
|
||||
}
|
||||
|
||||
// Check permission to view project team
|
||||
var hasProjectPermission = await _permission.HasProjectPermission(loggedInEmployee, projectId);
|
||||
var hasProjectPermission = await _permission.HasInfraProjectPermission(loggedInEmployee.Id, projectId);
|
||||
if (!hasProjectPermission)
|
||||
{
|
||||
_logger.LogWarning("Access denied: User {EmployeeId} tried to get team for Project {ProjectId}", loggedInEmployee.Id, projectId);
|
||||
@ -1458,7 +1514,8 @@ namespace Marco.Pms.Services.Service
|
||||
.ThenInclude(e => e!.JobRole)
|
||||
.Where(pa => pa.ProjectId == projectId
|
||||
&& pa.Employee != null
|
||||
&& organizationIds.Contains(pa.Employee.OrganizationId));
|
||||
&& organizationIds.Contains(pa.Employee.OrganizationId)
|
||||
&& pa.IsActive);
|
||||
|
||||
if (serviceId.HasValue)
|
||||
{
|
||||
@ -1475,6 +1532,8 @@ namespace Marco.Pms.Services.Service
|
||||
var employeeList = projectAllocations
|
||||
.Select(pa => _mapper.Map<EmployeeVM>(pa.Employee))
|
||||
.Distinct()
|
||||
.OrderBy(e => e.FirstName)
|
||||
.ThenBy(e => e.LastName)
|
||||
.ToList();
|
||||
|
||||
_logger.LogInfo("Fetched {EmployeeCount} employees for Project {ProjectId}.", employeeList.Count, projectId);
|
||||
@ -1507,7 +1566,7 @@ namespace Marco.Pms.Services.Service
|
||||
{
|
||||
var _permission = scope.ServiceProvider.GetRequiredService<PermissionServices>();
|
||||
// --- Step 1: Run independent permission checks in PARALLEL ---
|
||||
var projectPermissionTask = _permission.HasProjectPermission(loggedInEmployee, projectId);
|
||||
var projectPermissionTask = _permission.HasInfraProjectPermission(loggedInEmployee.Id, projectId);
|
||||
var viewInfraPermissionTask = Task.Run(async () =>
|
||||
{
|
||||
using var newScope = _serviceScopeFactory.CreateScope();
|
||||
@ -1658,7 +1717,7 @@ namespace Marco.Pms.Services.Service
|
||||
{
|
||||
using var taskScope = _serviceScopeFactory.CreateScope();
|
||||
var permission = taskScope.ServiceProvider.GetRequiredService<PermissionServices>();
|
||||
return await permission.HasProjectPermission(loggedInEmployee, projectId);
|
||||
return await permission.HasInfraProjectPermission(loggedInEmployee.Id, projectId);
|
||||
});
|
||||
var hasGenericViewInfraPermissionTask = Task.Run(async () =>
|
||||
{
|
||||
@ -2511,7 +2570,7 @@ namespace Marco.Pms.Services.Service
|
||||
}
|
||||
|
||||
// Verify logged-in employee has permission on the project
|
||||
var hasPermission = await permissionService.HasProjectPermission(loggedInEmployee, projectId);
|
||||
var hasPermission = await permissionService.HasInfraProjectPermission(loggedInEmployee.Id, projectId);
|
||||
if (!hasPermission)
|
||||
{
|
||||
_logger.LogWarning("Access DENIED for user {UserId} attempting to access project {ProjectId}.", loggedInEmployee.Id, projectId);
|
||||
@ -2601,7 +2660,7 @@ namespace Marco.Pms.Services.Service
|
||||
}
|
||||
|
||||
// Validate permission for logged-in employee to assign services to project
|
||||
var hasPermission = await permissionService.HasProjectPermission(loggedInEmployee, model.ProjectId);
|
||||
var hasPermission = await permissionService.HasInfraProjectPermission(loggedInEmployee.Id, model.ProjectId);
|
||||
if (!hasPermission)
|
||||
{
|
||||
_logger.LogWarning("Access DENIED for user {UserId} attempting to assign services to project {ProjectId}.", loggedInEmployee.Id, model.ProjectId);
|
||||
@ -2703,7 +2762,7 @@ namespace Marco.Pms.Services.Service
|
||||
}
|
||||
|
||||
// Verify permission to update project
|
||||
var hasPermission = await permissionService.HasProjectPermission(loggedInEmployee, model.ProjectId);
|
||||
var hasPermission = await permissionService.HasInfraProjectPermission(loggedInEmployee.Id, model.ProjectId);
|
||||
if (!hasPermission)
|
||||
{
|
||||
_logger.LogWarning("Access DENIED for user {UserId} trying to deassign services from project {ProjectId}.", loggedInEmployee.Id, model.ProjectId);
|
||||
@ -2801,7 +2860,7 @@ namespace Marco.Pms.Services.Service
|
||||
}
|
||||
|
||||
// Check if the logged in employee has permission to access the project
|
||||
var hasPermission = await permissionService.HasProjectPermission(loggedInEmployee, projectId);
|
||||
var hasPermission = await permissionService.HasInfraProjectPermission(loggedInEmployee.Id, projectId);
|
||||
if (!hasPermission)
|
||||
{
|
||||
_logger.LogWarning("Access denied for user {UserId} on project {ProjectId}", loggedInEmployee.Id, projectId);
|
||||
@ -2970,7 +3029,7 @@ namespace Marco.Pms.Services.Service
|
||||
}
|
||||
|
||||
// Check if the logged in employee has permission to access the project
|
||||
var hasPermission = await permissionService.HasProjectPermission(loggedInEmployee, projectId);
|
||||
var hasPermission = await permissionService.HasInfraProjectPermission(loggedInEmployee.Id, projectId);
|
||||
if (!hasPermission)
|
||||
{
|
||||
_logger.LogWarning("Access denied for user {UserId} on project {ProjectId}", loggedInEmployee.Id, projectId);
|
||||
@ -3210,7 +3269,7 @@ namespace Marco.Pms.Services.Service
|
||||
}
|
||||
}
|
||||
|
||||
return finalViewModels;
|
||||
return finalViewModels.OrderBy(p => p.Name).ToList();
|
||||
}
|
||||
private async Task<ProjectDetailsVM> GetProjectViewModel(Guid? id, Project project)
|
||||
{
|
||||
@ -3575,6 +3634,208 @@ namespace Marco.Pms.Services.Service
|
||||
return featurePermissionIds;
|
||||
}
|
||||
|
||||
public async Task<List<Guid>> GetBothProjectIds(Guid loggedInEmployeeId, Guid tenantId)
|
||||
{
|
||||
if (tenantId == Guid.Empty)
|
||||
{
|
||||
_logger.LogWarning("Security Alert: GetBothProjectBasicListAsync called with empty TenantId by Employee: {EmployeeId}", loggedInEmployeeId);
|
||||
return new List<Guid>();
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
using var scope = _serviceScopeFactory.CreateScope();
|
||||
var permissionService = scope.ServiceProvider.GetRequiredService<PermissionServices>();
|
||||
bool hasManagePermission = await permissionService.HasPermission(PermissionsMaster.ManageProject, loggedInEmployeeId);
|
||||
|
||||
var infraTask = Task.Run(async () =>
|
||||
{
|
||||
await using var context = await _dbContextFactory.CreateDbContextAsync();
|
||||
|
||||
if (hasManagePermission)
|
||||
{
|
||||
return await context.Projects.AsNoTracking()
|
||||
.Where(p => p.TenantId == tenantId)
|
||||
.Select(p => p.Id)
|
||||
.ToListAsync();
|
||||
}
|
||||
else
|
||||
{
|
||||
return await context.ProjectAllocations.AsNoTracking()
|
||||
.Where(pa => pa.TenantId == tenantId && pa.EmployeeId == loggedInEmployeeId && pa.IsActive)
|
||||
.Select(pa => pa.ProjectId)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
|
||||
});
|
||||
|
||||
var serviceTask = Task.Run(async () =>
|
||||
{
|
||||
await using var context = await _dbContextFactory.CreateDbContextAsync();
|
||||
|
||||
if (hasManagePermission)
|
||||
{
|
||||
return await context.ServiceProjects.AsNoTracking()
|
||||
.Where(sp => sp.TenantId == tenantId && sp.IsActive)
|
||||
.Select(sp => sp.Id)
|
||||
.ToListAsync();
|
||||
}
|
||||
else
|
||||
{
|
||||
var serviceProjectIds = await context.ServiceProjectAllocations.AsNoTracking()
|
||||
.Where(spa => spa.TenantId == tenantId && spa.EmployeeId == loggedInEmployeeId && spa.IsActive)
|
||||
.Select(spa => spa.ProjectId)
|
||||
.ToListAsync();
|
||||
|
||||
var jobServiceProjectIds = await context.JobEmployeeMappings.AsNoTracking()
|
||||
.Include(jem => jem.JobTicket)
|
||||
.Where(jem => jem.TenantId == tenantId && jem.JobTicket != null && jem.JobTicket.IsActive && !jem.JobTicket.IsArchive && jem.AssigneeId == loggedInEmployeeId)
|
||||
.Select(jem => jem.JobTicket!.ProjectId)
|
||||
.ToListAsync();
|
||||
serviceProjectIds.Concat(jobServiceProjectIds).Distinct().ToList();
|
||||
return serviceProjectIds;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
await Task.WhenAll(infraTask, serviceTask);
|
||||
|
||||
|
||||
var infraResults = await infraTask;
|
||||
var serviceResults = await serviceTask;
|
||||
|
||||
var combinedProjects = infraResults
|
||||
.Concat(serviceResults)
|
||||
.Distinct()
|
||||
.ToList();
|
||||
|
||||
_logger.LogInfo("Successfully fetched {Count} projects ({InfraCount} Infra, {ServiceCount} Service) for User {UserId}.",
|
||||
combinedProjects.Count, infraResults.Count, serviceResults.Count, loggedInEmployeeId);
|
||||
|
||||
return combinedProjects;
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
_logger.LogWarning("Project fetch operation was canceled by the client.");
|
||||
return new List<Guid>();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "CRITICAL: Failed to fetch projects for Tenant {TenantId}. Error: {Message}", tenantId, ex.Message);
|
||||
return new List<Guid>();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<List<Guid>> GetBothProjectIdsAsync(Guid loggedInEmployeeId, Guid tenantId)
|
||||
{
|
||||
// 1. Guard Clause: Fast exit for invalid inputs
|
||||
if (tenantId == Guid.Empty)
|
||||
{
|
||||
_logger.LogWarning("Security Alert: GetBothProjectIdsAsync invoked with empty TenantId. User: {EmployeeId}", loggedInEmployeeId);
|
||||
return new List<Guid>();
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
_logger.LogInfo("Starting parallel fetch of Project IDs for Tenant: {TenantId}, User: {UserId}", tenantId, loggedInEmployeeId);
|
||||
|
||||
// 2. Permission Check (Scoped)
|
||||
using var scope = _serviceScopeFactory.CreateScope();
|
||||
var permissionService = scope.ServiceProvider.GetRequiredService<PermissionServices>();
|
||||
bool hasManagePermission = await permissionService.HasPermission(PermissionsMaster.ManageProject, loggedInEmployeeId);
|
||||
|
||||
// 3. Parallel Execution: Infrastructure Projects
|
||||
var infraTask = Task.Run(async () =>
|
||||
{
|
||||
// Create isolated context for thread safety
|
||||
using var context = await _dbContextFactory.CreateDbContextAsync();
|
||||
|
||||
if (hasManagePermission)
|
||||
{
|
||||
// Admin: Fetch all IDs for tenant
|
||||
return await context.Projects.AsNoTracking()
|
||||
.Where(p => p.TenantId == tenantId)
|
||||
.Select(p => p.Id)
|
||||
.ToListAsync();
|
||||
}
|
||||
else
|
||||
{
|
||||
// User: Fetch only allocated IDs
|
||||
return await context.ProjectAllocations.AsNoTracking()
|
||||
.Where(pa => pa.TenantId == tenantId && pa.EmployeeId == loggedInEmployeeId && pa.IsActive)
|
||||
.Select(pa => pa.ProjectId)
|
||||
.ToListAsync();
|
||||
}
|
||||
});
|
||||
|
||||
// 4. Parallel Execution: Service Projects
|
||||
var serviceTask = Task.Run(async () =>
|
||||
{
|
||||
using var context = await _dbContextFactory.CreateDbContextAsync();
|
||||
|
||||
if (hasManagePermission)
|
||||
{
|
||||
// Admin: Fetch all active Service Projects
|
||||
return await context.ServiceProjects.AsNoTracking()
|
||||
.Where(sp => sp.TenantId == tenantId && sp.IsActive)
|
||||
.Select(sp => sp.Id)
|
||||
.ToListAsync();
|
||||
}
|
||||
else
|
||||
{
|
||||
// User: Complex Logic (Direct Allocation OR Job Ticket Mapping)
|
||||
|
||||
// Query A: Direct Allocations
|
||||
var directAllocationsQuery = context.ServiceProjectAllocations.AsNoTracking()
|
||||
.Where(spa => spa.TenantId == tenantId && spa.EmployeeId == loggedInEmployeeId && spa.IsActive)
|
||||
.Select(spa => spa.ProjectId);
|
||||
|
||||
// Query B: Via Job Tickets
|
||||
// Note: Removed .Include(); EF Core automatically joins when we access jem.JobTicket.ProjectId
|
||||
var jobMappingsQuery = context.JobEmployeeMappings.AsNoTracking()
|
||||
.Where(jem => jem.TenantId == tenantId
|
||||
&& jem.AssigneeId == loggedInEmployeeId
|
||||
&& jem.JobTicket != null
|
||||
&& jem.JobTicket.IsActive
|
||||
&& !jem.JobTicket.IsArchive)
|
||||
.Select(jem => jem.JobTicket!.ProjectId);
|
||||
|
||||
// OPTIMIZATION: Use LINQ Union to combine queries into ONE SQL statement.
|
||||
// This performs the DISTINCT operation in the database, not in memory.
|
||||
return await directAllocationsQuery
|
||||
.Union(jobMappingsQuery)
|
||||
.ToListAsync();
|
||||
}
|
||||
});
|
||||
|
||||
// 5. Await both tasks
|
||||
await Task.WhenAll(infraTask, serviceTask);
|
||||
|
||||
// 6. Merge Results
|
||||
// Distinct() ensures that if a Project ID exists in both Infra and Service (rare, but possible by ID collision or bad data), we don't duplicate.
|
||||
var combinedIds = (await infraTask)
|
||||
.Concat(await serviceTask)
|
||||
.Distinct()
|
||||
.ToList();
|
||||
|
||||
_logger.LogInfo("Completed Project ID fetch. Total: {Count} (Infra: {InfraCount}, Service: {ServiceCount}) for User {UserId}",
|
||||
combinedIds.Count, infraTask.Result.Count, serviceTask.Result.Count, loggedInEmployeeId);
|
||||
|
||||
return combinedIds;
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
_logger.LogWarning("GetBothProjectIdsAsync operation canceled by client request.");
|
||||
return new List<Guid>();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "CRITICAL failure in GetBothProjectIdsAsync for Tenant {TenantId}. Message: {Message}", tenantId, ex.Message);
|
||||
return new List<Guid>();
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
||||
@ -26,10 +26,12 @@ namespace Marco.Pms.Services.Service.ServiceInterfaces
|
||||
Task<ApiResponse<object>> GetPurchaseInvoiceStatusAsync(Employee loggedInEmployee, CancellationToken cancellationToken);
|
||||
|
||||
#endregion
|
||||
|
||||
#region =================================================================== Invoice Attachment Type APIs ===================================================================
|
||||
Task<ApiResponse<object>> GetInvoiceAttachmentTypeAsync(Employee loggedInEmployee, CancellationToken cancellationToken);
|
||||
|
||||
#endregion
|
||||
|
||||
#region =================================================================== Currency APIs ===================================================================
|
||||
Task<ApiResponse<object>> GetCurrencyAsync(Employee loggedInEmployee, Guid tenantId);
|
||||
|
||||
@ -58,7 +60,7 @@ namespace Marco.Pms.Services.Service.ServiceInterfaces
|
||||
#endregion
|
||||
|
||||
#region =================================================================== Activity APIs ===================================================================
|
||||
Task<ApiResponse<object>> GetActivitiesMasterAsync(Guid? activityGroupId, Employee loggedInEmployee, Guid tenantId);
|
||||
Task<ApiResponse<object>> GetActivitiesMasterAsync(Guid? activityGroupId, string? searchString, Employee loggedInEmployee, Guid tenantId);
|
||||
Task<ApiResponse<object>> CreateActivityAsync(CreateActivityMasterDto createActivity, Employee loggedInEmployee, Guid tenantId);
|
||||
Task<ApiResponse<object>> UpdateActivityAsync(Guid id, CreateActivityMasterDto createActivity, Employee loggedInEmployee, Guid tenantId);
|
||||
Task<ApiResponse<object>> DeleteActivityAsync(Guid id, bool isActive, Employee loggedInEmployee, Guid tenantId);
|
||||
|
||||
@ -10,7 +10,7 @@ namespace Marco.Pms.Services.Service.ServiceInterfaces
|
||||
{
|
||||
public interface IProjectServices
|
||||
{
|
||||
Task<ApiResponse<object>> GetBothProjectBasicListAsync(Guid? id, string? searchString, Employee loggedInEmployee, Guid tenantId);
|
||||
Task<ApiResponse<List<BasicProjectVM>>> GetBothProjectBasicListAsync(Guid? id, string? searchString, Employee loggedInEmployee, Guid tenantId);
|
||||
Task<ApiResponse<object>> GetAllProjectsBasicAsync(bool provideAll, Employee loggedInEmployee, Guid tenantId);
|
||||
Task<ApiResponse<object>> GetAllProjectsAsync(string? searchString, int pageNumber, int pageSize, Employee loggedInEmployee, Guid tenantId);
|
||||
Task<ApiResponse<object>> GetProjectAsync(Guid id, Employee loggedInEmployee, Guid tenantId);
|
||||
@ -51,6 +51,7 @@ namespace Marco.Pms.Services.Service.ServiceInterfaces
|
||||
|
||||
Task<ApiResponse<object>> GetAssignedOrganizationsToProjectAsync(Guid projectId, Employee loggedInEmployee, Guid tenantId);
|
||||
Task<ApiResponse<object>> GetAssignedOrganizationsToProjectForDropdownAsync(Guid projectId, Employee loggedInEmployee, Guid tenantId);
|
||||
Task<List<Guid>> GetBothProjectIdsAsync(Guid loggedInEmployeeId, Guid tenantId);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user