Compare commits
43 Commits
main
...
OnFieldWor
Author | SHA1 | Date | |
---|---|---|---|
663d94093c | |||
dc83aa72a6 | |||
ef597b2bb7 | |||
4acc61f03a | |||
c47b0da7b8 | |||
d180d5d60e | |||
60eefa78c0 | |||
b1af96b923 | |||
629c4541d6 | |||
e93408c00d | |||
16e509ccbd | |||
8a4a056c2d | |||
f6e8a0d5e2 | |||
0561e356d8 | |||
e2ee3f325c | |||
6441103e30 | |||
d380dfebd2 | |||
658fa8cd23 | |||
3afdad29b2 | |||
8f463ce90d | |||
590476a8aa | |||
548e714ea9 | |||
91f4305995 | |||
7e15c517ac | |||
9332d9cc0b | |||
541ed28bd2 | |||
0e1d20156f | |||
87c5de87a1 | |||
5eda1773b7 | |||
b3f54962ab | |||
040e7df32b | |||
0066e20c43 | |||
7659eadd00 | |||
824381bb49 | |||
207a44acd7 | |||
7775f58d69 | |||
91be729b41 | |||
0bd57d29d8 | |||
b442bb4bbc | |||
ca3e47c1e6 | |||
061512d501 | |||
71cc442054 | |||
bab03a8e47 |
@ -12,7 +12,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
namespace Marco.Pms.DataAccess.Migrations
|
||||
{
|
||||
[DbContext(typeof(ApplicationDbContext))]
|
||||
[Migration("20251004112239_Added_ExpenceUID_In_Expense_Table")]
|
||||
[Migration("20251003093145_Added_ExpenceUID_In_Expense_Table")]
|
||||
partial class Added_ExpenceUID_In_Expense_Table
|
||||
{
|
||||
/// <inheritdoc />
|
@ -12,7 +12,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
namespace Marco.Pms.DataAccess.Migrations
|
||||
{
|
||||
[DbContext(typeof(ApplicationDbContext))]
|
||||
[Migration("20251010062100_Added_Requested_In_Attendance_Table")]
|
||||
[Migration("20251008121556_Added_Requested_In_Attendance_Table")]
|
||||
partial class Added_Requested_In_Attendance_Table
|
||||
{
|
||||
/// <inheritdoc />
|
@ -212,48 +212,6 @@ namespace Marco.Pms.Helpers.CacheHelper
|
||||
|
||||
return true;
|
||||
}
|
||||
public async Task<bool> ClearAllEmployeesFromCacheByOnlyEmployeeId(Guid employeeId)
|
||||
{
|
||||
var employeeIdString = employeeId.ToString();
|
||||
|
||||
try
|
||||
{
|
||||
var filter = Builders<EmployeePermissionMongoDB>.Filter.Eq(e => e.Id, employeeIdString);
|
||||
|
||||
var result = await _collection.DeleteManyAsync(filter);
|
||||
|
||||
if (result.DeletedCount == 0)
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error occured while deleting employee profile");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
public async Task<bool> ClearAllEmployeesFromCacheByTenantId(Guid tenantId)
|
||||
{
|
||||
var tenantIdString = tenantId.ToString();
|
||||
|
||||
try
|
||||
{
|
||||
var filter = Builders<EmployeePermissionMongoDB>.Filter.Eq(e => e.TenantId, tenantIdString);
|
||||
|
||||
var result = await _collection.DeleteManyAsync(filter);
|
||||
|
||||
if (result.DeletedCount == 0)
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error occured while deleting employee profile");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
public async Task<bool> ClearAllEmployeesFromCacheByEmployeeIds(List<string> employeeIds, Guid tenantId)
|
||||
{
|
||||
var tenantIdString = tenantId.ToString();
|
||||
|
@ -19,6 +19,8 @@ namespace Marco.Pms.Model.AttendanceModule
|
||||
public Guid ProjectID { get; set; }
|
||||
|
||||
public DateTime AttendanceDate { get; set; }
|
||||
public DateTime? RequestedAt { get; set; }
|
||||
public DateTime? ApprovedAt { get; set; }
|
||||
public DateTime? InTime { get; set; }
|
||||
public DateTime? OutTime { get; set; }
|
||||
public bool IsApproved { get; set; }
|
||||
@ -29,8 +31,6 @@ namespace Marco.Pms.Model.AttendanceModule
|
||||
[ForeignKey("ApprovedById")]
|
||||
[ValidateNever]
|
||||
public Employee? Approver { get; set; }
|
||||
public DateTime? RequestedAt { get; set; }
|
||||
public DateTime? ApprovedAt { get; set; }
|
||||
public Guid? RequestedById { get; set; }
|
||||
|
||||
[ForeignKey("RequestedById")]
|
||||
|
@ -2,7 +2,7 @@
|
||||
{
|
||||
public class CreateWorkStatusMasterDto
|
||||
{
|
||||
public required string Name { get; set; }
|
||||
public required string Description { get; set; }
|
||||
public string? Name { get; set; }
|
||||
public string? Description { get; set; }
|
||||
}
|
||||
}
|
||||
|
@ -3,7 +3,7 @@
|
||||
public class UpdateWorkStatusMasterDto
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public required string Name { get; set; }
|
||||
public required string Description { get; set; }
|
||||
public string? Name { get; set; }
|
||||
public string? Description { get; set; }
|
||||
}
|
||||
}
|
||||
|
@ -19,7 +19,7 @@
|
||||
public string? EmergencyPhoneNumber { get; set; }
|
||||
public string? EmergencyContactPerson { get; set; }
|
||||
public Guid JobRoleId { get; set; }
|
||||
public required Guid OrganizationId { get; set; }
|
||||
public Guid? OrganizationId { get; set; }
|
||||
public required bool HasApplicationAccess { get; set; }
|
||||
}
|
||||
public class MobileUserManageDto
|
||||
@ -33,7 +33,7 @@
|
||||
public required string Gender { get; set; }
|
||||
public Guid JobRoleId { get; set; }
|
||||
public string? ProfileImage { get; set; }
|
||||
public required Guid OrganizationId { get; set; }
|
||||
public Guid? OrganizationId { get; set; }
|
||||
public required bool HasApplicationAccess { get; set; }
|
||||
}
|
||||
|
||||
|
@ -2,7 +2,7 @@
|
||||
{
|
||||
public class CreateContactCategoryDto
|
||||
{
|
||||
public required string Name { get; set; }
|
||||
public required string Description { get; set; }
|
||||
public string? Name { get; set; }
|
||||
public string? Description { get; set; }
|
||||
}
|
||||
}
|
||||
|
@ -2,7 +2,7 @@
|
||||
{
|
||||
public class CreateContactTagDto
|
||||
{
|
||||
public required string Name { get; set; }
|
||||
public required string Description { get; set; }
|
||||
public string? Name { get; set; }
|
||||
public string? Description { get; set; }
|
||||
}
|
||||
}
|
||||
|
@ -2,8 +2,8 @@
|
||||
{
|
||||
public class UpdateContactCategoryDto
|
||||
{
|
||||
public required Guid Id { get; set; }
|
||||
public required string Name { get; set; }
|
||||
public required string Description { get; set; }
|
||||
public Guid Id { get; set; }
|
||||
public string? Name { get; set; }
|
||||
public string? Description { get; set; }
|
||||
}
|
||||
}
|
||||
|
@ -3,7 +3,7 @@
|
||||
public class UpdateContactTagDto
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public required string Name { get; set; }
|
||||
public required string Description { get; set; }
|
||||
public string? Name { get; set; }
|
||||
public string? Description { get; set; }
|
||||
}
|
||||
}
|
||||
|
@ -27,7 +27,7 @@ namespace Marco.Pms.Model.Dtos.Project
|
||||
[DisplayName("Project Status")]
|
||||
[Required(ErrorMessage = "Project Status is required!")]
|
||||
public required Guid ProjectStatusId { get; set; }
|
||||
public required Guid PromoterId { get; set; }
|
||||
public required Guid PMCId { get; set; }
|
||||
public Guid? PromoterId { get; set; }
|
||||
public Guid? PMCId { get; set; }
|
||||
}
|
||||
}
|
||||
|
@ -5,7 +5,7 @@
|
||||
public Guid EmployeeId { get; set; }
|
||||
public Guid JobRoleId { get; set; }
|
||||
public Guid ProjectId { get; set; }
|
||||
public Guid? ServiceId { get; set; }
|
||||
public Guid ServiceId { get; set; }
|
||||
public bool Status { get; set; }
|
||||
}
|
||||
|
||||
@ -14,7 +14,7 @@
|
||||
{
|
||||
public Guid ProjectId { get; set; }
|
||||
public Guid JobRoleId { get; set; }
|
||||
public Guid? ServiceId { get; set; }
|
||||
public Guid ServiceId { get; set; }
|
||||
public bool Status { get; set; }
|
||||
}
|
||||
}
|
||||
|
@ -27,7 +27,7 @@ namespace Marco.Pms.Model.Dtos.Project
|
||||
[DisplayName("Project Status")]
|
||||
[Required(ErrorMessage = "Project Status is required!")]
|
||||
public required Guid ProjectStatusId { get; set; }
|
||||
public required Guid PromoterId { get; set; }
|
||||
public required Guid PMCId { get; set; }
|
||||
public Guid? PromoterId { get; set; }
|
||||
public Guid? PMCId { get; set; }
|
||||
}
|
||||
}
|
||||
|
@ -6,6 +6,7 @@
|
||||
public List<Guid>? StatusIds { get; set; }
|
||||
public List<Guid>? CreatedByIds { get; set; }
|
||||
public List<Guid>? PaidById { get; set; }
|
||||
public List<Guid>? ExpenseTypeIds { get; set; }
|
||||
public bool IsTransactionDate { get; set; } = false;
|
||||
public DateTime? StartDate { get; set; }
|
||||
public DateTime? EndDate { get; set; }
|
||||
|
@ -19,8 +19,8 @@ namespace Marco.Pms.Model.MongoDBModels.Expenses
|
||||
public DateTime CreatedAt { get; set; }
|
||||
public DateTime ExpireAt { get; set; } = DateTime.UtcNow.Date.AddDays(1);
|
||||
public string SupplerName { get; set; } = string.Empty;
|
||||
public string? ExpenseUId { get; set; }
|
||||
public double Amount { get; set; }
|
||||
public string? ExpenseUId { get; set; }
|
||||
public ExpensesStatusMasterMongoDB Status { get; set; } = new ExpensesStatusMasterMongoDB();
|
||||
public List<ExpensesStatusMasterMongoDB> NextStatus { get; set; } = new List<ExpensesStatusMasterMongoDB>();
|
||||
public bool PreApproved { get; set; } = false;
|
||||
|
@ -19,7 +19,6 @@ namespace Marco.Pms.Model.ViewModels.Expenses
|
||||
public DateTime TransactionDate { get; set; }
|
||||
public DateTime CreatedAt { get; set; }
|
||||
public string SupplerName { get; set; } = string.Empty;
|
||||
public string? ExpenseUId { get; set; }
|
||||
public double Amount { get; set; }
|
||||
public ExpensesStatusMasterVM? Status { get; set; }
|
||||
public List<ExpensesStatusMasterVM>? NextStatus { get; set; }
|
||||
@ -27,6 +26,7 @@ namespace Marco.Pms.Model.ViewModels.Expenses
|
||||
public string? TransactionId { get; set; }
|
||||
public string Description { get; set; } = string.Empty;
|
||||
public string? Location { get; set; }
|
||||
public string? ExpenseUId { get; set; }
|
||||
public List<BasicDocumentVM> Documents { get; set; } = new List<BasicDocumentVM>();
|
||||
public List<ExpenseLogVM> ExpenseLogs { get; set; } = new List<ExpenseLogVM>();
|
||||
public string? GSTNumber { get; set; }
|
||||
|
@ -11,7 +11,6 @@ namespace Marco.Pms.Model.ViewModels.Organization
|
||||
public string? ContactPerson { get; set; }
|
||||
public double SPRID { get; set; }
|
||||
public string? logoImage { get; set; }
|
||||
public string? OrganizationType { get; set; }
|
||||
public DateTime AssignedDate { get; set; }
|
||||
public BasicEmployeeVM? AssignedBy { get; set; }
|
||||
public ServiceMasterVM? Service { get; set; }
|
||||
|
@ -7,18 +7,15 @@
|
||||
public required string TimeStamp { get; set; }
|
||||
public int TodaysAttendances { get; set; }
|
||||
public int TotalEmployees { get; set; }
|
||||
public double AttendancePercentage { get; set; }
|
||||
public int RegularizationPending { get; set; }
|
||||
public int CheckoutPending { get; set; }
|
||||
public double TotalPlannedWork { get; set; }
|
||||
public double TotalCompletedWork { get; set; }
|
||||
public double CompletionStatus { get; set; }
|
||||
public double TotalPlannedTask { get; set; }
|
||||
public double TotalCompletedTask { get; set; }
|
||||
public double TaskPercentage { get; set; }
|
||||
public double CompletionStatus { get; set; }
|
||||
public int ReportPending { get; set; }
|
||||
public int TodaysAssignTasks { get; set; }
|
||||
public int TodaysCompletedTasks { get; set; }
|
||||
public List<TeamOnSite> TeamOnSite { get; set; } = new List<TeamOnSite>();
|
||||
public List<PerformedTask> PerformedTasks { get; set; } = new List<PerformedTask>();
|
||||
public List<PerformedAttendance> PerformedAttendance { get; set; } = new List<PerformedAttendance>();
|
||||
|
@ -470,6 +470,19 @@ namespace Marco.Pms.Services.Controllers
|
||||
|
||||
foreach (var item in menu.Items)
|
||||
{
|
||||
if (item.Text == "Projects")
|
||||
{
|
||||
allowedItems.Add(new MenuItem
|
||||
{
|
||||
Text = "Projects",
|
||||
Icon = "bx bx-building-house",
|
||||
Available = true,
|
||||
Link = "/projects",
|
||||
PermissionIds = new List<string>(),
|
||||
Submenu = new List<SubMenuItem>()
|
||||
});
|
||||
continue;
|
||||
}
|
||||
// --- Item permission check ---
|
||||
if (!item.PermissionIds.Any())
|
||||
{
|
||||
@ -608,6 +621,20 @@ namespace Marco.Pms.Services.Controllers
|
||||
var featureIds = await generalHelper.GetFeatureIdsByTenentIdAsync(tenantId);
|
||||
_logger.LogInfo("Enabled features for TenantId: {TenantId} -> {FeatureIds}", tenantId, string.Join(",", featureIds));
|
||||
|
||||
if (!(featureIds?.Any() ?? false))
|
||||
{
|
||||
featureIds = new List<Guid>
|
||||
{
|
||||
new Guid("a4e25142-449b-4334-a6e5-22f70e4732d7"), // Expense Management feature
|
||||
new Guid("81ab8a87-8ccd-4015-a917-0627cee6a100"), // Employee Management feature
|
||||
new Guid("52c9cf54-1eb2-44d2-81bb-524cf29c0a94"), // Attendance Management feature
|
||||
new Guid("a8cf4331-8f04-4961-8360-a3f7c3cc7462"), // Document Management feature
|
||||
new Guid("be3b3afc-6ccf-4566-b9b6-aafcb65546be"), // Masters Management feature
|
||||
new Guid("39e66f81-efc6-446c-95bd-46bff6cfb606"), // Directory Management feature
|
||||
new Guid("6d4c82d6-dbce-48ab-b8b8-f785f4d8c914") // Organization Management feature
|
||||
};
|
||||
}
|
||||
|
||||
// Aggregate menus based on enabled features
|
||||
var response = featureIds
|
||||
.Where(id => featureMenus.ContainsKey(id))
|
||||
|
@ -5,7 +5,6 @@ using Marco.Pms.Model.Dtos.Attendance;
|
||||
using Marco.Pms.Model.Employees;
|
||||
using Marco.Pms.Model.Entitlements;
|
||||
using Marco.Pms.Model.Mapper;
|
||||
using Marco.Pms.Model.Projects;
|
||||
using Marco.Pms.Model.Utilities;
|
||||
using Marco.Pms.Model.ViewModels.Activities;
|
||||
using Marco.Pms.Model.ViewModels.AttendanceVM;
|
||||
@ -55,6 +54,7 @@ namespace MarcoBMS.Services.Controllers
|
||||
}
|
||||
|
||||
[HttpGet("log/attendance/{attendanceid}")]
|
||||
|
||||
public async Task<IActionResult> GetAttendanceLogById(Guid attendanceid)
|
||||
{
|
||||
using var scope = _serviceScopeFactory.CreateScope();
|
||||
@ -158,29 +158,13 @@ namespace MarcoBMS.Services.Controllers
|
||||
/// <returns></returns>
|
||||
|
||||
[HttpGet("project/log")]
|
||||
public async Task<IActionResult> EmployeeAttendanceByDateRange([FromQuery] Guid projectId, [FromQuery] Guid? organizationId, [FromQuery] string? dateFrom = null, [FromQuery] string? dateTo = null)
|
||||
|
||||
public async Task<IActionResult> EmployeeAttendanceByDateRange([FromQuery] Guid? projectId, [FromQuery] Guid? organizationId, [FromQuery] string? dateFrom = null, [FromQuery] string? dateTo = null)
|
||||
{
|
||||
using var scope = _serviceScopeFactory.CreateScope();
|
||||
var _projectServices = scope.ServiceProvider.GetRequiredService<IProjectServices>();
|
||||
var LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
|
||||
|
||||
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
|
||||
|
||||
var project = await _context.Projects.AsNoTracking().FirstOrDefaultAsync(p => p.Id == projectId && p.TenantId == tenantId);
|
||||
if (project == null)
|
||||
{
|
||||
_logger.LogWarning("Project {ProjectId} not found in database", projectId);
|
||||
return NotFound(ApiResponse<object>.ErrorResponse("Project not found."));
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
if (!hasProjectPermission)
|
||||
{
|
||||
_logger.LogWarning("Employee {EmployeeId} tries to access attendance of project {ProjectId}, but don't have access", loggedInEmployee.Id, projectId);
|
||||
return Unauthorized(ApiResponse<object>.ErrorResponse("Unauthorized access", "Unauthorized access", 404));
|
||||
}
|
||||
var hasTeamAttendancePermission = await _permission.HasPermission(PermissionsMaster.TeamAttendance, LoggedInEmployee.Id);
|
||||
var hasSelfAttendancePermission = await _permission.HasPermission(PermissionsMaster.SelfAttendance, LoggedInEmployee.Id);
|
||||
|
||||
DateTime fromDate = new DateTime();
|
||||
DateTime toDate = new DateTime();
|
||||
@ -196,32 +180,46 @@ namespace MarcoBMS.Services.Controllers
|
||||
return BadRequest(ApiResponse<object>.ErrorResponse("Invalid Date", "Invalid Date", 400));
|
||||
}
|
||||
|
||||
if (projectId == Guid.Empty)
|
||||
{
|
||||
_logger.LogWarning("The project Id sent by user is less than or equal to zero");
|
||||
return BadRequest(ApiResponse<object>.ErrorResponse("Project ID is required and must be greater than zero.", "Project ID is required and must be greater than zero.", 400));
|
||||
}
|
||||
|
||||
var result = new List<EmployeeAttendanceVM>();
|
||||
//Attendance? attendance = null;
|
||||
ProjectAllocation? teamMember = null;
|
||||
|
||||
if (dateFrom == null) fromDate = DateTime.UtcNow.Date;
|
||||
if (dateTo == null && dateFrom != null) toDate = fromDate.AddDays(-1);
|
||||
|
||||
var lstAttendanceQuery = _context.Attendes
|
||||
.Include(a => a.Employee)
|
||||
.ThenInclude(e => e!.JobRole)
|
||||
.Include(a => a.Employee)
|
||||
.ThenInclude(e => e!.JobRole)
|
||||
.Include(a => a.Employee)
|
||||
.ThenInclude(e => e!.Organization)
|
||||
.Include(a => a.Employee)
|
||||
.ThenInclude(e => e!.JobRole)
|
||||
.Where(a =>
|
||||
a.AttendanceDate.Date >= fromDate.Date &&
|
||||
a.AttendanceDate.Date <= toDate.Date &&
|
||||
a.TenantId == tenantId &&
|
||||
a.Employee != null &&
|
||||
a.Employee.Organization != null &&
|
||||
a.Employee.JobRole != null);
|
||||
|
||||
if (organizationId.HasValue)
|
||||
{
|
||||
lstAttendanceQuery = lstAttendanceQuery.Where(a => a.Employee != null && a.Employee.OrganizationId == organizationId);
|
||||
}
|
||||
|
||||
if (projectId.HasValue)
|
||||
{
|
||||
lstAttendanceQuery = lstAttendanceQuery.Where(a => a.ProjectID == projectId);
|
||||
}
|
||||
|
||||
if (hasTeamAttendancePermission)
|
||||
{
|
||||
List<Attendance> lstAttendance = await _context.Attendes
|
||||
.Include(a => a.RequestedBy)
|
||||
.ThenInclude(e => e!.JobRole)
|
||||
.Include(a => a.Approver)
|
||||
.ThenInclude(e => e!.JobRole)
|
||||
.Where(c => c.ProjectID == projectId && c.AttendanceDate.Date >= fromDate.Date && c.AttendanceDate.Date <= toDate.Date && c.TenantId == tenantId)
|
||||
.ToListAsync();
|
||||
List<Attendance> lstAttendance = await lstAttendanceQuery.ToListAsync();
|
||||
|
||||
var projectIds = lstAttendance.Select(a => a.ProjectID).ToList();
|
||||
var projects = await _context.Projects.Where(p => projectIds.Contains(p.Id) && p.TenantId == tenantId).ToListAsync();
|
||||
|
||||
List<ProjectAllocation> projectteam = await _projectServices.GetTeamByProject(tenantId, projectId, organizationId, true);
|
||||
var jobRole = await _context.JobRoles.ToListAsync();
|
||||
foreach (Attendance? attendance in lstAttendance)
|
||||
{
|
||||
var result1 = new EmployeeAttendanceVM()
|
||||
@ -230,91 +228,52 @@ namespace MarcoBMS.Services.Controllers
|
||||
CheckInTime = attendance.InTime,
|
||||
CheckOutTime = attendance.OutTime,
|
||||
Activity = attendance.Activity,
|
||||
ApprovedAt = attendance.ApprovedAt,
|
||||
Approver = _mapper.Map<BasicEmployeeVM>(attendance.Approver),
|
||||
EmployeeId = attendance.EmployeeId,
|
||||
FirstName = attendance.Employee?.FirstName,
|
||||
LastName = attendance.Employee?.LastName,
|
||||
JobRoleName = attendance.Employee?.JobRole?.Name,
|
||||
ProjectId = attendance.ProjectID,
|
||||
ProjectName = projects.Where(p => p.Id == attendance.ProjectID).Select(p => p.Name).FirstOrDefault(),
|
||||
OrganizationName = attendance.Employee?.Organization?.Name,
|
||||
RequestedAt = attendance.RequestedAt,
|
||||
RequestedBy = _mapper.Map<BasicEmployeeVM>(attendance.RequestedBy)
|
||||
RequestedBy = _mapper.Map<BasicEmployeeVM>(attendance.RequestedBy),
|
||||
ApprovedAt = attendance.ApprovedAt,
|
||||
Approver = _mapper.Map<BasicEmployeeVM>(attendance.Approver)
|
||||
};
|
||||
teamMember = projectteam.Find(x => x.EmployeeId == attendance.EmployeeId);
|
||||
if (teamMember != null)
|
||||
{
|
||||
result1.EmployeeAvatar = null;
|
||||
result1.EmployeeId = teamMember.EmployeeId;
|
||||
if (teamMember.Employee != null)
|
||||
{
|
||||
result1.FirstName = teamMember.Employee.FirstName;
|
||||
result1.LastName = teamMember.Employee.LastName;
|
||||
result1.JobRoleName = teamMember.Employee.JobRole != null ? teamMember.Employee.JobRole.Name : null;
|
||||
result1.OrganizationName = teamMember.Employee.Organization?.Name;
|
||||
result1.ProjectId = projectId;
|
||||
result1.ProjectName = teamMember.Project?.Name;
|
||||
}
|
||||
else
|
||||
{
|
||||
result1.FirstName = null;
|
||||
result1.LastName = null;
|
||||
result1.JobRoleName = null;
|
||||
result1.OrganizationName = null;
|
||||
}
|
||||
|
||||
result.Add(result1);
|
||||
}
|
||||
|
||||
result.Add(result1);
|
||||
}
|
||||
}
|
||||
else if (hasSelfAttendancePermission)
|
||||
{
|
||||
List<Attendance> lstAttendances = await _context.Attendes
|
||||
.Include(a => a.RequestedBy)
|
||||
.ThenInclude(e => e!.JobRole)
|
||||
.Include(a => a.Approver)
|
||||
.ThenInclude(e => e!.JobRole)
|
||||
.Where(c => c.ProjectID == projectId && c.EmployeeId == loggedInEmployee.Id && c.AttendanceDate.Date >= fromDate.Date &&
|
||||
c.AttendanceDate.Date <= toDate.Date && c.TenantId == tenantId)
|
||||
.ToListAsync();
|
||||
|
||||
var projectAllocationQuery = _context.ProjectAllocations
|
||||
.Include(pa => pa.Project)
|
||||
.Include(pa => pa.Employee)
|
||||
.ThenInclude(e => e!.Organization)
|
||||
.Include(pa => pa.Employee)
|
||||
.ThenInclude(e => e!.JobRole)
|
||||
.Where(pa => pa.EmployeeId == loggedInEmployee.Id && pa.TenantId == tenantId && pa.IsActive &&
|
||||
pa.ProjectId == projectId && pa.Project != null &&
|
||||
pa.Employee != null && pa.Employee.Organization != null && pa.Employee.JobRole != null);
|
||||
var lstAttendances = await lstAttendanceQuery.Where(a => a.EmployeeId == LoggedInEmployee.Id).ToListAsync();
|
||||
|
||||
if (organizationId.HasValue)
|
||||
{
|
||||
projectAllocationQuery = projectAllocationQuery.Where(pa => pa.Employee != null && pa.Employee.OrganizationId == organizationId);
|
||||
}
|
||||
var projectIds = lstAttendances.Select(a => a.ProjectID).ToList();
|
||||
var projects = await _context.Projects.Where(p => projectIds.Contains(p.Id) && p.TenantId == tenantId).ToListAsync();
|
||||
|
||||
var projectAllocation = await projectAllocationQuery.FirstOrDefaultAsync();
|
||||
|
||||
foreach (var attendance in lstAttendances)
|
||||
{
|
||||
if (projectAllocation != null)
|
||||
EmployeeAttendanceVM result1 = new EmployeeAttendanceVM
|
||||
{
|
||||
EmployeeAttendanceVM result1 = new EmployeeAttendanceVM
|
||||
{
|
||||
Id = attendance.Id,
|
||||
EmployeeAvatar = null,
|
||||
EmployeeId = projectAllocation.EmployeeId,
|
||||
FirstName = projectAllocation.Employee?.FirstName,
|
||||
LastName = projectAllocation.Employee?.LastName,
|
||||
JobRoleName = projectAllocation.Employee?.JobRole?.Name,
|
||||
OrganizationName = projectAllocation.Employee?.Organization?.Name,
|
||||
ProjectId = attendance.ProjectID,
|
||||
ProjectName = projectAllocation.Project?.Name,
|
||||
CheckInTime = attendance.InTime,
|
||||
CheckOutTime = attendance.OutTime,
|
||||
Activity = attendance.Activity,
|
||||
ApprovedAt = attendance.ApprovedAt,
|
||||
Approver = _mapper.Map<BasicEmployeeVM>(attendance.Approver),
|
||||
RequestedAt = attendance.RequestedAt,
|
||||
RequestedBy = _mapper.Map<BasicEmployeeVM>(attendance.RequestedBy)
|
||||
};
|
||||
result.Add(result1);
|
||||
}
|
||||
Id = attendance.Id,
|
||||
EmployeeAvatar = null,
|
||||
EmployeeId = attendance.EmployeeId,
|
||||
FirstName = attendance.Employee?.FirstName,
|
||||
LastName = attendance.Employee?.LastName,
|
||||
JobRoleName = attendance.Employee?.JobRole?.Name,
|
||||
ProjectId = attendance.ProjectID,
|
||||
ProjectName = projects.Where(p => p.Id == attendance.ProjectID).Select(p => p.Name).FirstOrDefault(),
|
||||
OrganizationName = attendance.Employee?.Organization?.Name,
|
||||
CheckInTime = attendance.InTime,
|
||||
CheckOutTime = attendance.OutTime,
|
||||
Activity = attendance.Activity,
|
||||
RequestedAt = attendance.RequestedAt,
|
||||
RequestedBy = _mapper.Map<BasicEmployeeVM>(attendance.RequestedBy),
|
||||
ApprovedAt = attendance.ApprovedAt,
|
||||
Approver = _mapper.Map<BasicEmployeeVM>(attendance.Approver)
|
||||
};
|
||||
result.Add(result1);
|
||||
}
|
||||
}
|
||||
_logger.LogInfo("{count} Attendance records fetched successfully", result.Count);
|
||||
@ -322,6 +281,7 @@ namespace MarcoBMS.Services.Controllers
|
||||
}
|
||||
|
||||
[HttpGet("project/team")]
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves employee attendance records for a specified project and date.
|
||||
/// The result is filtered based on the logged-in employee's permissions (Team or Self).
|
||||
@ -331,12 +291,12 @@ namespace MarcoBMS.Services.Controllers
|
||||
/// <param name="includeInactive">Optional. Includes inactive employees in the team list if true.</param>
|
||||
/// <param name="date">Optional. The date for which to fetch attendance, in "yyyy-MM-dd" format. Defaults to the current UTC date.</param>
|
||||
/// <returns>An IActionResult containing a list of employee attendance records or an error response.</returns>
|
||||
public async Task<IActionResult> EmployeeAttendanceByProjectAsync([FromQuery] Guid projectId, [FromQuery] Guid? organizationId, [FromQuery] bool includeInactive, [FromQuery] string? date = null)
|
||||
public async Task<IActionResult> EmployeeAttendanceByProjectAsync([FromQuery] Guid? projectId, [FromQuery] Guid? organizationId, [FromQuery] bool includeInactive, [FromQuery] string? date = null)
|
||||
{
|
||||
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
|
||||
|
||||
// --- 1. Initial Validation and Permission Checks ---
|
||||
_logger.LogInfo("Fetching attendance for ProjectId: {ProjectId}, TenantId: {TenantId}", projectId, tenantId);
|
||||
_logger.LogInfo("Fetching attendance for ProjectId: {ProjectId}, TenantId: {TenantId}", projectId ?? Guid.Empty, tenantId);
|
||||
|
||||
// Validate date format
|
||||
if (!DateTime.TryParse(date, out var forDate))
|
||||
@ -344,20 +304,6 @@ namespace MarcoBMS.Services.Controllers
|
||||
forDate = DateTime.UtcNow.Date; // Default to today's date
|
||||
}
|
||||
|
||||
// Check if the project exists and if the employee has access
|
||||
var project = await _context.Projects.AsNoTracking().FirstOrDefaultAsync(p => p.Id == projectId && p.TenantId == tenantId);
|
||||
if (project == null)
|
||||
{
|
||||
_logger.LogWarning("Project {ProjectId} not found in database", projectId);
|
||||
return NotFound(ApiResponse<object>.ErrorResponse("Project not found."));
|
||||
}
|
||||
|
||||
if (!await _permission.HasProjectPermission(loggedInEmployee, 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."));
|
||||
}
|
||||
|
||||
// --- 2. Delegate to Specific Logic Based on Permissions ---
|
||||
try
|
||||
{
|
||||
@ -366,13 +312,17 @@ namespace MarcoBMS.Services.Controllers
|
||||
|
||||
if (hasTeamAttendancePermission)
|
||||
{
|
||||
if (!organizationId.HasValue)
|
||||
{
|
||||
organizationId = loggedInEmployee.OrganizationId;
|
||||
}
|
||||
_logger.LogInfo("EmployeeId: {EmployeeId} has Team Attendance permission. Fetching team attendance.", loggedInEmployee.Id);
|
||||
result = await GetTeamAttendanceAsync(tenantId, projectId, organizationId, forDate, includeInactive);
|
||||
result = await GetTeamAttendanceAsync(tenantId, projectId, organizationId.Value, forDate, includeInactive);
|
||||
}
|
||||
else if (await _permission.HasPermission(PermissionsMaster.SelfAttendance, loggedInEmployee.Id))
|
||||
{
|
||||
_logger.LogInfo("EmployeeId: {EmployeeId} has Self Attendance permission. Fetching self attendance.", loggedInEmployee.Id);
|
||||
result = await GetSelfAttendanceAsync(tenantId, projectId, loggedInEmployee.Id, organizationId, forDate);
|
||||
result = await GetSelfAttendanceAsync(tenantId, projectId, loggedInEmployee.Id, forDate);
|
||||
}
|
||||
else
|
||||
{
|
||||
@ -380,70 +330,68 @@ namespace MarcoBMS.Services.Controllers
|
||||
return StatusCode(403, ApiResponse<object>.ErrorResponse("You do not have permission to view attendance.", new { }, 403));
|
||||
}
|
||||
|
||||
_logger.LogInfo("Successfully fetched {Count} attendance records for ProjectId: {ProjectId}", result.Count, projectId);
|
||||
_logger.LogInfo("Successfully fetched {Count} attendance records for ProjectId: {ProjectId}", result.Count, projectId ?? Guid.Empty);
|
||||
return Ok(ApiResponse<object>.SuccessResponse(result, $"{result.Count} attendance records fetched successfully."));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "An error occurred while fetching attendance for ProjectId: {ProjectId}", projectId);
|
||||
_logger.LogError(ex, "An error occurred while fetching attendance for ProjectId: {ProjectId}", projectId ?? Guid.Empty);
|
||||
return StatusCode(500, ApiResponse<object>.ErrorResponse("An internal server error occurred."));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
[HttpGet("regularize")]
|
||||
public async Task<IActionResult> GetRequestRegularizeAttendance([FromQuery] Guid projectId, [FromQuery] Guid? organizationId, [FromQuery] bool IncludeInActive)
|
||||
public async Task<IActionResult> GetRequestRegularizeAttendance([FromQuery] Guid? projectId, [FromQuery] Guid? organizationId, [FromQuery] bool IncludeInActive)
|
||||
{
|
||||
using var scope = _serviceScopeFactory.CreateScope();
|
||||
var _projectServices = scope.ServiceProvider.GetRequiredService<IProjectServices>();
|
||||
|
||||
Employee LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
|
||||
var result = new List<EmployeeAttendanceVM>();
|
||||
var hasProjectPermission = await _permission.HasProjectPermission(LoggedInEmployee, projectId);
|
||||
|
||||
if (!hasProjectPermission)
|
||||
{
|
||||
_logger.LogWarning("Employee {EmployeeId} tries to access attendance of project {ProjectId}, but don't have access", LoggedInEmployee.Id, projectId);
|
||||
return Unauthorized(ApiResponse<object>.ErrorResponse("Unauthorized access", "Unauthorized access", 404));
|
||||
}
|
||||
|
||||
List<Attendance> lstAttendance = await _context.Attendes
|
||||
var lstAttendanceQuery = _context.Attendes
|
||||
.Include(a => a.RequestedBy)
|
||||
.ThenInclude(e => e!.JobRole)
|
||||
.Include(a => a.Approver)
|
||||
.Include(a => a.Employee)
|
||||
.ThenInclude(e => e!.Organization)
|
||||
.Include(a => a.Employee)
|
||||
.ThenInclude(e => e!.JobRole)
|
||||
.Where(c => c.ProjectID == projectId && c.Activity == ATTENDANCE_MARK_TYPE.REQUEST_REGULARIZE && c.TenantId == tenantId)
|
||||
.ToListAsync();
|
||||
|
||||
List<ProjectAllocation> projectteam = await _projectServices.GetTeamByProject(tenantId, projectId, organizationId, true);
|
||||
var idList = projectteam.Select(p => p.EmployeeId).ToList();
|
||||
.Where(c => c.Activity == ATTENDANCE_MARK_TYPE.REQUEST_REGULARIZE && c.Employee != null && c.Employee.JobRole != null && c.TenantId == tenantId);
|
||||
|
||||
if (organizationId.HasValue)
|
||||
{
|
||||
lstAttendanceQuery = lstAttendanceQuery.Where(a => a.Employee != null && a.Employee.OrganizationId == organizationId);
|
||||
}
|
||||
|
||||
if (projectId.HasValue)
|
||||
{
|
||||
lstAttendanceQuery = lstAttendanceQuery.Where(a => a.ProjectID == projectId);
|
||||
}
|
||||
|
||||
List<Attendance> lstAttendance = await lstAttendanceQuery.ToListAsync();
|
||||
|
||||
var projectIds = lstAttendance.Select(a => a.ProjectID).ToList();
|
||||
var projects = await _context.Projects.Where(p => projectIds.Contains(p.Id) && p.TenantId == tenantId).ToListAsync();
|
||||
|
||||
foreach (Attendance attende in lstAttendance)
|
||||
{
|
||||
var teamMember = projectteam.Find(m => m.EmployeeId == attende.EmployeeId);
|
||||
if (teamMember != null && teamMember.Employee != null && teamMember.Employee.JobRole != null)
|
||||
var result1 = new EmployeeAttendanceVM()
|
||||
{
|
||||
var result1 = new EmployeeAttendanceVM()
|
||||
{
|
||||
Id = attende.Id,
|
||||
CheckInTime = attende.InTime,
|
||||
CheckOutTime = attende.OutTime,
|
||||
Activity = attende.Activity,
|
||||
EmployeeAvatar = null,
|
||||
EmployeeId = attende.EmployeeId,
|
||||
FirstName = teamMember.Employee.FirstName,
|
||||
LastName = teamMember.Employee.LastName,
|
||||
JobRoleName = teamMember.Employee.JobRole.Name,
|
||||
OrganizationName = teamMember.Employee.Organization?.Name,
|
||||
ProjectId = projectId,
|
||||
ProjectName = teamMember.Project?.Name,
|
||||
ApprovedAt = attende.ApprovedAt,
|
||||
Approver = _mapper.Map<BasicEmployeeVM>(attende.Approver),
|
||||
RequestedAt = attende.RequestedAt,
|
||||
RequestedBy = _mapper.Map<BasicEmployeeVM>(attende.RequestedBy)
|
||||
};
|
||||
result.Add(result1);
|
||||
}
|
||||
Id = attende.Id,
|
||||
CheckInTime = attende.InTime,
|
||||
CheckOutTime = attende.OutTime,
|
||||
Activity = attende.Activity,
|
||||
EmployeeAvatar = null,
|
||||
EmployeeId = attende.EmployeeId,
|
||||
ProjectId = attende.ProjectID,
|
||||
FirstName = attende.Employee?.FirstName,
|
||||
ProjectName = projects.Where(p => p.Id == attende.ProjectID).Select(p => p.Name).FirstOrDefault(),
|
||||
LastName = attende.Employee?.LastName,
|
||||
JobRoleName = attende.Employee?.JobRole?.Name,
|
||||
OrganizationName = attende.Employee?.Organization?.Name,
|
||||
RequestedAt = attende.RequestedAt,
|
||||
RequestedBy = _mapper.Map<BasicEmployeeVM>(attende.RequestedBy)
|
||||
};
|
||||
result.Add(result1);
|
||||
|
||||
|
||||
}
|
||||
@ -473,11 +421,10 @@ namespace MarcoBMS.Services.Controllers
|
||||
using var scope = _serviceScopeFactory.CreateScope();
|
||||
var _signalR = scope.ServiceProvider.GetRequiredService<IHubContext<MarcoHub>>();
|
||||
var _employeeHelper = scope.ServiceProvider.GetRequiredService<EmployeeHelper>();
|
||||
var _firebase = scope.ServiceProvider.GetRequiredService<IFirebaseService>();
|
||||
|
||||
var currentEmployee = await _userHelper.GetCurrentEmployeeAsync();
|
||||
using var transaction = await _context.Database.BeginTransactionAsync();
|
||||
|
||||
using var transaction = await _context.Database.BeginTransactionAsync();
|
||||
try
|
||||
{
|
||||
Attendance? attendance = await _context.Attendes.FirstOrDefaultAsync(a => a.Id == recordAttendanceDot.Id && a.TenantId == tenantId); ;
|
||||
@ -508,18 +455,22 @@ namespace MarcoBMS.Services.Controllers
|
||||
{
|
||||
attendance.IsApproved = true;
|
||||
attendance.Activity = ATTENDANCE_MARK_TYPE.REGULARIZE;
|
||||
|
||||
|
||||
//string timeString = "10:30 PM"; // Format: "hh:mm tt"
|
||||
|
||||
attendance.OutTime = finalDateTime;
|
||||
}
|
||||
else if (recordAttendanceDot.Action == ATTENDANCE_MARK_TYPE.REQUEST_REGULARIZE)
|
||||
{
|
||||
DateTime date = attendance.AttendanceDate;
|
||||
finalDateTime = GetDateFromTimeStamp(date.Date, recordAttendanceDot.MarkTime);
|
||||
if (attendance.InTime < finalDateTime)
|
||||
if (attendance.InTime <= finalDateTime)
|
||||
{
|
||||
attendance.OutTime = finalDateTime;
|
||||
attendance.Activity = ATTENDANCE_MARK_TYPE.REQUEST_REGULARIZE;
|
||||
attendance.RequestedById = currentEmployee.Id;
|
||||
attendance.RequestedAt = DateTime.UtcNow;
|
||||
attendance.RequestedById = currentEmployee.Id;
|
||||
}
|
||||
else
|
||||
{
|
||||
@ -608,6 +559,7 @@ namespace MarcoBMS.Services.Controllers
|
||||
CheckOutTime = attendance.OutTime,
|
||||
EmployeeAvatar = null,
|
||||
EmployeeId = recordAttendanceDot.EmployeeID,
|
||||
ProjectId = attendance.ProjectID,
|
||||
FirstName = employee.FirstName,
|
||||
LastName = employee.LastName,
|
||||
Id = attendance.Id,
|
||||
@ -628,7 +580,7 @@ namespace MarcoBMS.Services.Controllers
|
||||
// --- Push Notification Section ---
|
||||
// This section attempts to send a test push notification to the user's device.
|
||||
// It's designed to fail gracefully and handle invalid Firebase Cloud Messaging (FCM) tokens.
|
||||
|
||||
var _firebase = scope.ServiceProvider.GetRequiredService<IFirebaseService>();
|
||||
var name = $"{vm.FirstName} {vm.LastName}";
|
||||
|
||||
await _firebase.SendAttendanceMessageAsync(attendance.ProjectID, name, recordAttendanceDot.Action, attendance.EmployeeId, tenantId);
|
||||
@ -669,9 +621,9 @@ namespace MarcoBMS.Services.Controllers
|
||||
|
||||
using var scope = _serviceScopeFactory.CreateScope();
|
||||
var _s3Service = scope.ServiceProvider.GetRequiredService<S3UploadService>();
|
||||
var _signalR = scope.ServiceProvider.GetRequiredService<IHubContext<MarcoHub>>();
|
||||
var _employeeHelper = scope.ServiceProvider.GetRequiredService<EmployeeHelper>();
|
||||
var _firebase = scope.ServiceProvider.GetRequiredService<IFirebaseService>();
|
||||
var _signalR = scope.ServiceProvider.GetRequiredService<IHubContext<MarcoHub>>();
|
||||
|
||||
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
|
||||
var batchId = Guid.NewGuid();
|
||||
@ -736,8 +688,8 @@ namespace MarcoBMS.Services.Controllers
|
||||
{
|
||||
attendance.OutTime = finalDateTime;
|
||||
attendance.Activity = ATTENDANCE_MARK_TYPE.REQUEST_REGULARIZE;
|
||||
attendance.RequestedById = loggedInEmployee.Id;
|
||||
attendance.RequestedAt = DateTime.UtcNow;
|
||||
attendance.RequestedById = loggedInEmployee.Id;
|
||||
}
|
||||
else
|
||||
{
|
||||
@ -748,14 +700,14 @@ namespace MarcoBMS.Services.Controllers
|
||||
case ATTENDANCE_MARK_TYPE.REGULARIZE:
|
||||
attendance.IsApproved = true;
|
||||
attendance.Activity = ATTENDANCE_MARK_TYPE.REGULARIZE;
|
||||
attendance.ApprovedById = loggedInEmployee.Id;
|
||||
attendance.ApprovedAt = DateTime.UtcNow;
|
||||
attendance.ApprovedById = loggedInEmployee.Id;
|
||||
break;
|
||||
case ATTENDANCE_MARK_TYPE.REGULARIZE_REJECT:
|
||||
attendance.IsApproved = false;
|
||||
attendance.Activity = ATTENDANCE_MARK_TYPE.REGULARIZE_REJECT;
|
||||
attendance.ApprovedById = loggedInEmployee.Id;
|
||||
attendance.ApprovedAt = DateTime.UtcNow;
|
||||
attendance.ApprovedById = loggedInEmployee.Id;
|
||||
break;
|
||||
}
|
||||
|
||||
@ -821,6 +773,7 @@ namespace MarcoBMS.Services.Controllers
|
||||
{
|
||||
Id = attendance.Id,
|
||||
EmployeeId = employee.Id,
|
||||
ProjectId = attendance.ProjectID,
|
||||
FirstName = employee.FirstName,
|
||||
LastName = employee.LastName,
|
||||
CheckInTime = attendance.InTime,
|
||||
@ -883,65 +836,54 @@ namespace MarcoBMS.Services.Controllers
|
||||
/// <summary>
|
||||
/// Fetches attendance for an entire project team using a single, optimized database query.
|
||||
/// </summary>
|
||||
private async Task<List<EmployeeAttendanceVM>> GetTeamAttendanceAsync(Guid tenantId, Guid projectId, Guid? organizationId, DateTime forDate, bool includeInactive)
|
||||
private async Task<List<EmployeeAttendanceVM>> GetTeamAttendanceAsync(Guid tenantId, Guid? projectId, Guid organizationId, DateTime forDate, bool includeInactive)
|
||||
{
|
||||
// This single query joins ProjectAllocations with Employees and performs a LEFT JOIN with Attendances.
|
||||
// This is far more efficient than fetching collections and joining them in memory.
|
||||
var query = _context.ProjectAllocations
|
||||
.Include(pa => pa.Project)
|
||||
.Include(pa => pa.Employee)
|
||||
.ThenInclude(e => e!.Organization)
|
||||
.Include(pa => pa.Employee)
|
||||
.ThenInclude(e => e!.JobRole)
|
||||
.Where(pa => pa.TenantId == tenantId && pa.ProjectId == projectId);
|
||||
var query = _context.Employees
|
||||
.Include(e => e!.Organization)
|
||||
.Include(e => e!.JobRole)
|
||||
.Where(e => e.OrganizationId == organizationId && e.Organization != null && e.JobRole != null && e.IsActive);
|
||||
|
||||
// Apply filters based on optional parameters
|
||||
if (!includeInactive)
|
||||
|
||||
var lstAttendanceQuery = _context.Attendes.Where(c => c.AttendanceDate.Date == forDate && c.TenantId == tenantId);
|
||||
|
||||
if (projectId.HasValue)
|
||||
{
|
||||
query = query.Where(pa => pa.IsActive);
|
||||
}
|
||||
if (organizationId.HasValue)
|
||||
{
|
||||
query = query.Where(pa => pa.Employee != null && pa.Employee.OrganizationId == organizationId);
|
||||
lstAttendanceQuery = lstAttendanceQuery.Where(a => a.ProjectID == projectId);
|
||||
}
|
||||
|
||||
List<Attendance> lstAttendance = await _context.Attendes
|
||||
.Include(a => a.RequestedBy)
|
||||
.ThenInclude(e => e!.JobRole)
|
||||
.Include(a => a.Approver)
|
||||
.ThenInclude(e => e!.JobRole)
|
||||
.Where(c => c.ProjectID == projectId && c.AttendanceDate.Date == forDate && c.TenantId == tenantId).ToListAsync();
|
||||
List<Attendance> lstAttendance = await lstAttendanceQuery.ToListAsync();
|
||||
|
||||
var teamAttendance = await query
|
||||
var employees = await query
|
||||
.AsNoTracking()
|
||||
.ToListAsync();
|
||||
|
||||
var response = teamAttendance
|
||||
.Select(teamMember =>
|
||||
var projectIds = lstAttendance.Select(a => a.ProjectID).ToList();
|
||||
var projects = await _context.Projects.Where(p => projectIds.Contains(p.Id) && p.TenantId == tenantId).ToListAsync();
|
||||
|
||||
var response = employees
|
||||
.Select(employee =>
|
||||
{
|
||||
var result1 = new EmployeeAttendanceVM()
|
||||
{
|
||||
EmployeeAvatar = null,
|
||||
EmployeeId = teamMember.EmployeeId,
|
||||
FirstName = teamMember.Employee?.FirstName,
|
||||
LastName = teamMember.Employee?.LastName,
|
||||
OrganizationName = teamMember.Employee?.Organization?.Name,
|
||||
JobRoleName = teamMember.Employee?.JobRole?.Name,
|
||||
ProjectId = projectId,
|
||||
ProjectName = teamMember.Project?.Name
|
||||
EmployeeId = employee.Id,
|
||||
FirstName = employee.FirstName,
|
||||
LastName = employee.LastName,
|
||||
OrganizationName = employee.Organization!.Name,
|
||||
JobRoleName = employee.JobRole!.Name,
|
||||
};
|
||||
|
||||
var attendance = lstAttendance.Find(x => x.EmployeeId == teamMember.EmployeeId) ?? new Attendance();
|
||||
var attendance = lstAttendance.Find(x => x.EmployeeId == employee.Id) ?? new Attendance();
|
||||
if (attendance != null)
|
||||
{
|
||||
result1.Id = attendance.Id;
|
||||
result1.ProjectId = attendance.ProjectID;
|
||||
result1.CheckInTime = attendance.InTime;
|
||||
result1.CheckOutTime = attendance.OutTime;
|
||||
result1.Activity = attendance.Activity;
|
||||
result1.ApprovedAt = attendance.ApprovedAt;
|
||||
result1.Approver = _mapper.Map<BasicEmployeeVM>(attendance.Approver);
|
||||
result1.RequestedAt = attendance.RequestedAt;
|
||||
result1.RequestedBy = _mapper.Map<BasicEmployeeVM>(attendance.RequestedBy);
|
||||
result1.ProjectName = projects.Where(p => p.Id == attendance.ProjectID).Select(p => p.Name).FirstOrDefault();
|
||||
}
|
||||
return result1;
|
||||
})
|
||||
@ -954,57 +896,50 @@ namespace MarcoBMS.Services.Controllers
|
||||
/// <summary>
|
||||
/// Fetches a single attendance record for the logged-in employee.
|
||||
/// </summary>
|
||||
private async Task<List<EmployeeAttendanceVM>> GetSelfAttendanceAsync(Guid tenantId, Guid projectId, Guid employeeId, Guid? organizationId, DateTime forDate)
|
||||
private async Task<List<EmployeeAttendanceVM>> GetSelfAttendanceAsync(Guid tenantId, Guid? projectId, Guid employeeId, DateTime forDate)
|
||||
{
|
||||
List<EmployeeAttendanceVM> result = new List<EmployeeAttendanceVM>();
|
||||
|
||||
// This query fetches the employee's project allocation and their attendance in a single trip.
|
||||
Attendance lstAttendance = await _context.Attendes
|
||||
.Include(a => a.RequestedBy)
|
||||
.ThenInclude(e => e!.JobRole)
|
||||
.Include(a => a.Approver)
|
||||
.ThenInclude(e => e!.JobRole)
|
||||
.FirstOrDefaultAsync(c => c.ProjectID == projectId && c.EmployeeId == employeeId && c.AttendanceDate.Date == forDate && c.TenantId == tenantId) ?? new Attendance();
|
||||
var lstAttendanceQuery = _context.Attendes
|
||||
.Where(c => c.EmployeeId == employeeId && c.AttendanceDate.Date == forDate && c.TenantId == tenantId);
|
||||
|
||||
var projectAllocationQuery = _context.ProjectAllocations
|
||||
.Include(pa => pa.Project)
|
||||
.Include(pa => pa.Employee)
|
||||
.ThenInclude(e => e!.Organization)
|
||||
.Include(pa => pa.Employee)
|
||||
.ThenInclude(e => e!.JobRole)
|
||||
.Where(pa => pa.EmployeeId == employeeId && pa.TenantId == tenantId && pa.IsActive &&
|
||||
pa.ProjectId == projectId && pa.Project != null &&
|
||||
pa.Employee != null && pa.Employee.Organization != null && pa.Employee.JobRole != null);
|
||||
|
||||
if (organizationId.HasValue)
|
||||
if (projectId.HasValue)
|
||||
{
|
||||
projectAllocationQuery = projectAllocationQuery.Where(pa => pa.Employee != null && pa.Employee.OrganizationId == organizationId);
|
||||
lstAttendanceQuery = lstAttendanceQuery.Where(a => a.ProjectID == projectId);
|
||||
}
|
||||
|
||||
var projectAllocation = await projectAllocationQuery.FirstOrDefaultAsync();
|
||||
List<Attendance> lstAttendances = await lstAttendanceQuery.ToListAsync() ?? new List<Attendance>();
|
||||
|
||||
if (projectAllocation != null)
|
||||
var projectIds = lstAttendances.Select(a => a.ProjectID).ToList();
|
||||
var projects = await _context.Projects.Where(p => projectIds.Contains(p.Id) && p.TenantId == tenantId).ToListAsync();
|
||||
|
||||
var employee = await _context.Employees
|
||||
.Include(e => e.Organization)
|
||||
.Include(e => e.JobRole)
|
||||
.FirstOrDefaultAsync(e => e.Id == employeeId && e.IsActive);
|
||||
|
||||
if (employee != null && employee.JobRole != null && employee.Organization != null)
|
||||
{
|
||||
EmployeeAttendanceVM result1 = new EmployeeAttendanceVM
|
||||
foreach (var lstAttendance in lstAttendances)
|
||||
{
|
||||
Id = lstAttendance.Id,
|
||||
EmployeeAvatar = null,
|
||||
EmployeeId = projectAllocation.EmployeeId,
|
||||
FirstName = projectAllocation.Employee?.FirstName,
|
||||
OrganizationName = projectAllocation.Employee?.Organization?.Name,
|
||||
LastName = projectAllocation.Employee?.LastName,
|
||||
JobRoleName = projectAllocation.Employee?.JobRole?.Name,
|
||||
ProjectId = projectId,
|
||||
ProjectName = projectAllocation.Project?.Name,
|
||||
CheckInTime = lstAttendance.InTime,
|
||||
CheckOutTime = lstAttendance.OutTime,
|
||||
Activity = lstAttendance.Activity,
|
||||
ApprovedAt = lstAttendance.ApprovedAt,
|
||||
Approver = _mapper.Map<BasicEmployeeVM>(lstAttendance.Approver),
|
||||
RequestedAt = lstAttendance.RequestedAt,
|
||||
RequestedBy = _mapper.Map<BasicEmployeeVM>(lstAttendance.RequestedBy)
|
||||
};
|
||||
result.Add(result1);
|
||||
EmployeeAttendanceVM result1 = new EmployeeAttendanceVM
|
||||
{
|
||||
Id = lstAttendance.Id,
|
||||
EmployeeAvatar = null,
|
||||
ProjectId = lstAttendance.ProjectID,
|
||||
EmployeeId = employee.Id,
|
||||
FirstName = employee.FirstName,
|
||||
OrganizationName = employee.Organization.Name,
|
||||
ProjectName = projects.Where(p => p.Id == lstAttendance.ProjectID).Select(p => p.Name).FirstOrDefault(),
|
||||
LastName = employee.LastName,
|
||||
JobRoleName = employee.JobRole.Name,
|
||||
CheckInTime = lstAttendance.InTime,
|
||||
CheckOutTime = lstAttendance.OutTime,
|
||||
Activity = lstAttendance.Activity
|
||||
};
|
||||
result.Add(result1);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
|
@ -7,7 +7,6 @@ using Marco.Pms.Model.Employees;
|
||||
using Marco.Pms.Model.Entitlements;
|
||||
using Marco.Pms.Model.Utilities;
|
||||
using Marco.Pms.Model.ViewModels.Tenant;
|
||||
using Marco.Pms.Services.Helpers;
|
||||
using MarcoBMS.Services.Helpers;
|
||||
using MarcoBMS.Services.Service;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
@ -60,7 +59,7 @@ namespace MarcoBMS.Services.Controllers
|
||||
{
|
||||
|
||||
var user = await _context.ApplicationUsers
|
||||
.FirstOrDefaultAsync(u => u.Email == loginDto.Username || u.PhoneNumber == loginDto.Username);
|
||||
.FirstOrDefaultAsync(u => u.Email == loginDto.Username || u.UserName == loginDto.Username);
|
||||
|
||||
if (user == null)
|
||||
{
|
||||
@ -104,9 +103,13 @@ namespace MarcoBMS.Services.Controllers
|
||||
return NotFound(ApiResponse<object>.ErrorResponse("Username not found", "Username not found", 404));
|
||||
}
|
||||
|
||||
var tenants = await _context.Tenants.Where(t => t.OrganizationId == emp.OrganizationId).ToListAsync();
|
||||
|
||||
var tenant = tenants.OrderBy(t => t.OnBoardingDate).FirstOrDefault();
|
||||
|
||||
// Generate tokens
|
||||
var token = _refreshTokenService.GenerateJwtToken(user.UserName, emp.TenantId ?? Guid.Empty, emp.OrganizationId, _jwtSettings);
|
||||
var refreshToken = await _refreshTokenService.CreateRefreshToken(user.Id, emp.TenantId.ToString(), emp.OrganizationId, _jwtSettings);
|
||||
var token = _refreshTokenService.GenerateJwtToken(user.UserName, tenant?.Id ?? Guid.Empty, emp.OrganizationId, _jwtSettings);
|
||||
var refreshToken = await _refreshTokenService.CreateRefreshToken(user.Id, tenant?.Id.ToString(), emp.OrganizationId, _jwtSettings);
|
||||
|
||||
_logger.LogInfo("User login successful - UserId: {UserId}", user.Id);
|
||||
return Ok(ApiResponse<object>.SuccessResponse(new
|
||||
@ -202,12 +205,17 @@ namespace MarcoBMS.Services.Controllers
|
||||
}
|
||||
_logger.LogInfo("Successfully found employee details for tenant ID: {TenantId}", emp.TenantId ?? Guid.Empty);
|
||||
|
||||
|
||||
var tenants = await _context.Tenants.Where(t => t.OrganizationId == emp.OrganizationId).ToListAsync();
|
||||
|
||||
var tenant = tenants.OrderBy(t => t.OnBoardingDate).FirstOrDefault();
|
||||
|
||||
// Generate JWT token
|
||||
var token = _refreshTokenService.GenerateJwtToken(user.UserName, emp.TenantId ?? Guid.Empty, emp.OrganizationId, _jwtSettings);
|
||||
var token = _refreshTokenService.GenerateJwtToken(user.UserName, tenant?.Id ?? Guid.Empty, emp.OrganizationId, _jwtSettings);
|
||||
|
||||
// Generate a new refresh token and store it in the database.
|
||||
_logger.LogInfo("Generating and storing Refresh Token for user: {Username}", user.UserName);
|
||||
var refreshToken = await _refreshTokenService.CreateRefreshToken(user.Id, emp.TenantId.ToString(), emp.OrganizationId, _jwtSettings);
|
||||
var refreshToken = await _refreshTokenService.CreateRefreshToken(user.Id, tenant?.Id.ToString(), emp.OrganizationId, _jwtSettings);
|
||||
|
||||
// Fetch MPIN Token
|
||||
var mpinToken = await _context.MPINDetails.FirstOrDefaultAsync(p => p.UserId == Guid.Parse(user.Id));
|
||||
@ -265,31 +273,32 @@ namespace MarcoBMS.Services.Controllers
|
||||
}
|
||||
|
||||
string? tokenType = claimsPrincipal.FindFirst("token_type")?.Value;
|
||||
string? tokenTenantId = claimsPrincipal.FindFirst("TenantId")?.Value;
|
||||
string? tokenUserId = claimsPrincipal.FindFirst(ClaimTypes.NameIdentifier)?.Value;
|
||||
|
||||
// Validate essential claims
|
||||
if (string.IsNullOrWhiteSpace(tokenType) || string.IsNullOrWhiteSpace(tokenTenantId) || string.IsNullOrWhiteSpace(tokenUserId))
|
||||
if (string.IsNullOrWhiteSpace(tokenType) || string.IsNullOrWhiteSpace(tokenUserId))
|
||||
{
|
||||
_logger.LogWarning("MPIN token claims are incomplete");
|
||||
return Unauthorized(ApiResponse<object>.ErrorResponse("Invalid token claims", "MPIN token does not match your identity", 401));
|
||||
}
|
||||
|
||||
Guid tenantId = Guid.Parse(tokenTenantId);
|
||||
|
||||
// Fetch employee by ID and tenant
|
||||
var requestEmployee = await _context.Employees
|
||||
.Include(e => e.ApplicationUser)
|
||||
.FirstOrDefaultAsync(e => e.Id == verifyMPIN.EmployeeId && e.TenantId == tenantId && e.ApplicationUserId == tokenUserId && e.IsActive);
|
||||
.FirstOrDefaultAsync(e => e.Id == verifyMPIN.EmployeeId && e.HasApplicationAccess && e.ApplicationUserId == tokenUserId && e.IsActive);
|
||||
|
||||
if (requestEmployee == null || string.IsNullOrWhiteSpace(requestEmployee.ApplicationUserId))
|
||||
{
|
||||
_logger.LogWarning("Employee not found or invalid for verification - EmployeeId: {EmployeeId}", verifyMPIN.EmployeeId);
|
||||
return BadRequest(ApiResponse<object>.ErrorResponse("Invalid request", "Provided invalid employee information", 400));
|
||||
}
|
||||
var tenants = await _context.Tenants.Where(t => t.OrganizationId == requestEmployee.OrganizationId).ToListAsync();
|
||||
|
||||
var tenant = tenants.OrderBy(t => t.OnBoardingDate).FirstOrDefault();
|
||||
Guid tenantId = tenant?.Id ?? Guid.Empty;
|
||||
|
||||
// Validate that the token belongs to the same employee making the request
|
||||
if (requestEmployee.ApplicationUserId != tokenUserId || tokenType != "mpin")
|
||||
if (requestEmployee.ApplicationUserId != tokenUserId || tokenType != "mpin" || tenantId == Guid.Empty)
|
||||
{
|
||||
_logger.LogWarning("Token identity does not match employee info - EmployeeId: {EmployeeId}", requestEmployee.Id);
|
||||
return Unauthorized(ApiResponse<object>.ErrorResponse("Unauthorized", "MPIN token does not match your identity", 401));
|
||||
@ -402,7 +411,9 @@ namespace MarcoBMS.Services.Controllers
|
||||
//var accessToken = _refreshTokenService.GenerateJwtTokenWithOrganization(requestEmployee.ApplicationUser?.UserName, requestEmployee.OrganizationId, _jwtSettings);
|
||||
//var refreshToken = await _refreshTokenService.CreateRefreshTokenWithOrganization(requestEmployee.ApplicationUserId, requestEmployee.OrganizationId, _jwtSettings);
|
||||
|
||||
var tenant = await _context.Tenants.FirstOrDefaultAsync(t => t.OrganizationId == requestEmployee.OrganizationId);
|
||||
var tenants = await _context.Tenants.Where(t => t.OrganizationId == requestEmployee.OrganizationId).ToListAsync();
|
||||
|
||||
var tenant = tenants.OrderBy(t => t.OnBoardingDate).FirstOrDefault();
|
||||
|
||||
var accessToken = _refreshTokenService.GenerateJwtToken(requestEmployee.ApplicationUser?.UserName,
|
||||
tenant?.Id ?? Guid.Empty, requestEmployee.OrganizationId, _jwtSettings);
|
||||
@ -1470,9 +1481,6 @@ namespace MarcoBMS.Services.Controllers
|
||||
// Generate and store refresh token
|
||||
var refreshToken = await _refreshTokenService.CreateRefreshToken(loggedInEmployee.ApplicationUserId, tenantId.ToString(), loggedInEmployee.OrganizationId, _jwtSettings);
|
||||
|
||||
var _cache = scope.ServiceProvider.GetRequiredService<CacheUpdateHelper>();
|
||||
await _cache.ClearAllEmployeesFromCacheByOnlyEmployeeId(loggedInEmployee.Id);
|
||||
|
||||
_logger.LogInfo("Tenant selected and tokens generated for TenantId: {TenantId} and Employee: {EmployeeEmail}", tenantId, loggedInEmployee.Email ?? string.Empty);
|
||||
|
||||
// Return success response including tokens
|
||||
|
@ -2,6 +2,8 @@
|
||||
using Marco.Pms.Model.Activities;
|
||||
using Marco.Pms.Model.Dtos.Attendance;
|
||||
using Marco.Pms.Model.Employees;
|
||||
using Marco.Pms.Model.Entitlements;
|
||||
using Marco.Pms.Model.Expenses;
|
||||
using Marco.Pms.Model.Projects;
|
||||
using Marco.Pms.Model.Utilities;
|
||||
using Marco.Pms.Model.ViewModels.DashBoard;
|
||||
@ -12,6 +14,7 @@ using MarcoBMS.Services.Service;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using System.Globalization;
|
||||
|
||||
namespace Marco.Pms.Services.Controllers
|
||||
{
|
||||
@ -25,19 +28,35 @@ namespace Marco.Pms.Services.Controllers
|
||||
private readonly IProjectServices _projectServices;
|
||||
private readonly ILoggingService _logger;
|
||||
private readonly PermissionServices _permissionServices;
|
||||
private readonly IServiceScopeFactory _serviceScopeFactory;
|
||||
public static readonly Guid ActiveId = Guid.Parse("b74da4c2-d07e-46f2-9919-e75e49b12731");
|
||||
public DashboardController(ApplicationDbContext context, UserHelper userHelper, IProjectServices projectServices, ILoggingService logger, PermissionServices permissionServices)
|
||||
private static readonly Guid Draft = Guid.Parse("297e0d8f-f668-41b5-bfea-e03b354251c8");
|
||||
private static readonly Guid Review = Guid.Parse("6537018f-f4e9-4cb3-a210-6c3b2da999d7");
|
||||
private static readonly Guid Approve = Guid.Parse("4068007f-c92f-4f37-a907-bc15fe57d4d8");
|
||||
private static readonly Guid ProcessPending = Guid.Parse("f18c5cfd-7815-4341-8da2-2c2d65778e27");
|
||||
private static readonly Guid Processed = Guid.Parse("61578360-3a49-4c34-8604-7b35a3787b95");
|
||||
private static readonly Guid RejectedByReviewer = Guid.Parse("965eda62-7907-4963-b4a1-657fb0b2724b");
|
||||
private static readonly Guid RejectedByApprover = Guid.Parse("d1ee5eec-24b6-4364-8673-a8f859c60729");
|
||||
private readonly Guid tenantId;
|
||||
|
||||
public DashboardController(ApplicationDbContext context,
|
||||
UserHelper userHelper,
|
||||
IProjectServices projectServices,
|
||||
IServiceScopeFactory serviceScopeFactory,
|
||||
ILoggingService logger,
|
||||
PermissionServices permissionServices)
|
||||
{
|
||||
_context = context;
|
||||
_userHelper = userHelper;
|
||||
_projectServices = projectServices;
|
||||
_logger = logger;
|
||||
_serviceScopeFactory = serviceScopeFactory;
|
||||
_permissionServices = permissionServices;
|
||||
tenantId = userHelper.GetTenantId();
|
||||
}
|
||||
[HttpGet("progression")]
|
||||
public async Task<IActionResult> GetGraph([FromQuery] double days, [FromQuery] string FromDate, [FromQuery] Guid? projectId)
|
||||
{
|
||||
var tenantId = _userHelper.GetTenantId();
|
||||
var LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
|
||||
|
||||
DateTime fromDate = new DateTime();
|
||||
@ -149,7 +168,6 @@ namespace Marco.Pms.Services.Controllers
|
||||
[HttpGet("projects")]
|
||||
public async Task<IActionResult> GetProjectCount()
|
||||
{
|
||||
var tenantId = _userHelper.GetTenantId();
|
||||
var LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
|
||||
|
||||
var projects = await _context.Projects.Where(p => p.TenantId == tenantId).ToListAsync();
|
||||
@ -176,7 +194,6 @@ namespace Marco.Pms.Services.Controllers
|
||||
{
|
||||
try
|
||||
{
|
||||
var tenantId = _userHelper.GetTenantId();
|
||||
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
|
||||
|
||||
_logger.LogInfo("GetTotalEmployees called by user {UserId} for ProjectId: {ProjectId}", loggedInEmployee.Id, projectId ?? Guid.Empty);
|
||||
@ -269,7 +286,6 @@ namespace Marco.Pms.Services.Controllers
|
||||
{
|
||||
try
|
||||
{
|
||||
var tenantId = _userHelper.GetTenantId();
|
||||
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
|
||||
|
||||
_logger.LogInfo("GetTotalTasks called by user {UserId} for ProjectId: {ProjectId}", loggedInEmployee.Id, projectId ?? Guid.Empty);
|
||||
@ -348,10 +364,10 @@ namespace Marco.Pms.Services.Controllers
|
||||
return StatusCode(500, ApiResponse<object>.ErrorResponse("An internal server error occurred.", null, 500));
|
||||
}
|
||||
}
|
||||
|
||||
[HttpGet("pending-attendance")]
|
||||
public async Task<IActionResult> GetPendingAttendance()
|
||||
{
|
||||
Guid tenantId = _userHelper.GetTenantId();
|
||||
var LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
|
||||
|
||||
var attendance = await _context.Attendes.Where(a => a.EmployeeId == LoggedInEmployee.Id && a.TenantId == tenantId).ToListAsync();
|
||||
@ -374,7 +390,6 @@ namespace Marco.Pms.Services.Controllers
|
||||
[HttpGet("project-attendance/{projectId}")]
|
||||
public async Task<IActionResult> GetProjectAttendance(Guid projectId, [FromQuery] string? date)
|
||||
{
|
||||
Guid tenantId = _userHelper.GetTenantId();
|
||||
var LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
|
||||
|
||||
DateTime currentDate = DateTime.UtcNow;
|
||||
@ -428,7 +443,6 @@ namespace Marco.Pms.Services.Controllers
|
||||
[HttpGet("activities/{projectId}")]
|
||||
public async Task<IActionResult> GetActivities(Guid projectId, [FromQuery] string? date)
|
||||
{
|
||||
Guid tenantId = _userHelper.GetTenantId();
|
||||
var LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
|
||||
|
||||
DateTime currentDate = DateTime.UtcNow;
|
||||
@ -600,5 +614,317 @@ namespace Marco.Pms.Services.Controllers
|
||||
|
||||
return Ok(ApiResponse<object>.SuccessResponse(sortedResult, $"{sortedResult.Count} records fetched for attendance overview", 200));
|
||||
}
|
||||
|
||||
[HttpGet("expense/monthly")]
|
||||
public async Task<IActionResult> GetExpenseReportByProjectsAsync([FromQuery] Guid? projectId, [FromQuery] Guid? categoryId, [FromQuery] int months)
|
||||
{
|
||||
|
||||
try
|
||||
{
|
||||
// Read-only base filter with tenant scope and non-draft
|
||||
var baseQuery = _context.Expenses
|
||||
.AsNoTracking()
|
||||
.Where(e =>
|
||||
e.TenantId == tenantId
|
||||
&& e.IsActive
|
||||
&& e.StatusId != Draft); // [Server Filters]
|
||||
|
||||
if (months != 0)
|
||||
{
|
||||
months = 0 - months;
|
||||
var end = DateTime.UtcNow.Date;
|
||||
var start = end.AddMonths(months); // inclusive EOD
|
||||
baseQuery = baseQuery.Where(e => e.TransactionDate >= start
|
||||
&& e.TransactionDate <= end);
|
||||
}
|
||||
|
||||
if (projectId.HasValue)
|
||||
baseQuery = baseQuery.Where(e => e.ProjectId == projectId);
|
||||
|
||||
if (categoryId.HasValue)
|
||||
baseQuery = baseQuery.Where(e => e.ExpensesTypeId == categoryId);
|
||||
|
||||
// Single server-side group/aggregate by project
|
||||
var report = await baseQuery
|
||||
.AsNoTracking()
|
||||
.GroupBy(e => new { e.TransactionDate.Year, e.TransactionDate.Month })
|
||||
.Select(g => new
|
||||
{
|
||||
Year = g.Key.Year,
|
||||
Month = g.Key.Month,
|
||||
Total = g.Sum(x => x.Amount),
|
||||
Count = g.Count()
|
||||
})
|
||||
.OrderBy(x => x.Year).ThenBy(x => x.Month)
|
||||
.ToListAsync();
|
||||
|
||||
var culture = CultureInfo.GetCultureInfo("en-IN"); // pick desired locale
|
||||
|
||||
var response = report
|
||||
.Select(x => new
|
||||
{
|
||||
MonthName = culture.DateTimeFormat.GetMonthName(x.Month), // e.g., "January"
|
||||
Year = x.Year,
|
||||
Total = x.Total,
|
||||
Count = x.Count
|
||||
}).ToList();
|
||||
|
||||
_logger.LogInfo(
|
||||
"GetExpenseReportByProjects completed. TenantId={TenantId}, Rows={Rows}",
|
||||
tenantId, report.Count); // [Completion Log]
|
||||
|
||||
return Ok(ApiResponse<object>.SuccessResponse(response, "Expense report by project fetched successfully", 200)); // [Success Response]
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
_logger.LogWarning("GetExpenseReportByProjects canceled by client. TenantId={TenantId}", tenantId); // [Cancel Log]
|
||||
return StatusCode(499, ApiResponse<object>.ErrorResponse("Client has canceled the opration", "Client has canceled the opration", 499)); // [Cancel Response]
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex,
|
||||
"GetExpenseReportByProjects failed. TenantId={TenantId}",
|
||||
tenantId); // [Error Log]
|
||||
return StatusCode(500,
|
||||
ApiResponse<object>.ErrorResponse("An error occurred while fetching the expense report.", 500)); // [Error Response]
|
||||
}
|
||||
}
|
||||
|
||||
[HttpGet("expense/type")]
|
||||
public async Task<IActionResult> GetExpenseReportByExpenseTypeAsync([FromQuery] Guid? projectId, [FromQuery] DateTime startDate, [FromQuery] DateTime endDate)
|
||||
{
|
||||
// Structured log: entering action with filters
|
||||
_logger.LogDebug(
|
||||
"GetExpenseReportByExpenseType started. TenantId={TenantId}, ProjectId={ProjectId}, StartDate={StartDate}, EndDate={EndDate}",
|
||||
tenantId, projectId ?? Guid.Empty, startDate, endDate); // [Start Log] [memory:4][memory:1]
|
||||
|
||||
|
||||
try
|
||||
{
|
||||
// Compose base query: push filters to DB, avoid client evaluation
|
||||
IQueryable<Expenses> baseQuery = _context.Expenses
|
||||
.AsNoTracking() // Reduce tracking overhead for read-only endpoint
|
||||
.Where(e => e.TenantId == tenantId
|
||||
&& e.IsActive
|
||||
&& e.StatusId != Draft
|
||||
&& e.TransactionDate >= startDate
|
||||
&& e.TransactionDate <= endDate.AddDays(1).AddTicks(-1));
|
||||
|
||||
if (projectId.HasValue)
|
||||
baseQuery = baseQuery.Where(e => e.ProjectId == projectId.Value); // [Filter] [memory:7]
|
||||
|
||||
// Project to a minimal shape before grouping to avoid loading navigation graphs
|
||||
// Group by expense type name; adjust to the correct key if ExpensesCategory is an enum or navigation
|
||||
var query = baseQuery
|
||||
.Where(e => e.ExpensesType != null)
|
||||
.Select(e => new
|
||||
{
|
||||
ExpenseTypeName = e.ExpensesType!.Name, // If enum, use e.ExpensesCategory.ToString()
|
||||
Amount = e.Amount,
|
||||
StatusId = e.StatusId
|
||||
})
|
||||
.GroupBy(x => x.ExpenseTypeName)
|
||||
.Select(g => new
|
||||
{
|
||||
ProjectName = g.Key, // Original code used g.Key!.Name; here the grouping key is already a string
|
||||
TotalApprovedAmount = g.Where(x => x.StatusId == Processed
|
||||
|| x.StatusId == ProcessPending).Sum(x => x.Amount),
|
||||
TotalPendingAmount = g.Where(x => x.StatusId != Processed
|
||||
&& x.StatusId != RejectedByReviewer
|
||||
&& x.StatusId != RejectedByApprover)
|
||||
.Sum(x => x.Amount),
|
||||
TotalRejectedAmount = g.Where(x => x.StatusId == RejectedByReviewer
|
||||
|| x.StatusId == RejectedByApprover)
|
||||
.Sum(x => x.Amount),
|
||||
TotalProcessedAmount = g.Where(x => x.StatusId == Processed)
|
||||
.Sum(x => x.Amount)
|
||||
})
|
||||
.OrderBy(r => r.ProjectName); // Server-side order [memory:7]
|
||||
|
||||
var report = await query.ToListAsync(); // Single round-trip [memory:7]
|
||||
|
||||
var response = new
|
||||
{
|
||||
Report = report,
|
||||
TotalAmount = report.Sum(r => r.TotalApprovedAmount)
|
||||
};
|
||||
|
||||
_logger.LogInfo(
|
||||
"GetExpenseReportByExpenseType completed. TenantId={TenantId}, Filters: ProjectId={ProjectId}, StartDate={StartDate}, EndDate={EndDate}, Rows={RowCount}, TotalAmount={TotalAmount}",
|
||||
tenantId, projectId ?? Guid.Empty, startDate, endDate, report.Count, response.TotalAmount); // [Completion Log] [memory:4]
|
||||
|
||||
return Ok(ApiResponse<object>.SuccessResponse(response, "Expense report by expense type fetched successfully", 200)); // [Success Response] [memory:1]
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
_logger.LogWarning("GetExpenseReportByExpenseType canceled by client. TenantId={TenantId}", tenantId); // [Cancel Log] [memory:4]
|
||||
return StatusCode(499, ApiResponse<object>.ErrorResponse("Client has canceled the opration", "Client has canceled the opration", 499)); // [Cancel Response] [memory:1]
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex,
|
||||
"GetExpenseReportByExpenseType failed. TenantId={TenantId}, ProjectId={ProjectId}, StartDate={StartDate}, EndDate={EndDate}",
|
||||
tenantId, projectId ?? Guid.Empty, startDate, endDate); // [Error Log] [memory:4]
|
||||
return StatusCode(StatusCodes.Status500InternalServerError,
|
||||
ApiResponse<object>.ErrorResponse("An error occurred while fetching the expense report.", 500)); // [Error Response] [memory:1]
|
||||
}
|
||||
}
|
||||
|
||||
[HttpGet("expense/pendings")]
|
||||
public async Task<IActionResult> GetPendingExpenseListAsync([FromQuery] Guid? projectId)
|
||||
{
|
||||
// Start log with correlation fields
|
||||
_logger.LogDebug(
|
||||
"GetPendingExpenseListAsync started. Project={ProjectId} TenantId={TenantId}", projectId ?? Guid.Empty, tenantId); // [Start Log]
|
||||
|
||||
try
|
||||
{
|
||||
// Resolve current employee once; avoid using scoped services inside Task.Run
|
||||
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); // [User Context]
|
||||
|
||||
// Resolve permission service from current scope once
|
||||
using var scope = _serviceScopeFactory.CreateScope();
|
||||
|
||||
// Fire permission checks concurrently without Task.Run; these are async I/O methods
|
||||
|
||||
var hasReviewPermissionTask = Task.Run(async () =>
|
||||
{
|
||||
var _permission = scope.ServiceProvider.GetRequiredService<PermissionServices>();
|
||||
return await _permission.HasPermission(PermissionsMaster.ExpenseReview, loggedInEmployee.Id);
|
||||
});
|
||||
|
||||
var hasApprovePermissionTask = Task.Run(async () =>
|
||||
{
|
||||
var _permission = scope.ServiceProvider.GetRequiredService<PermissionServices>();
|
||||
return await _permission.HasPermission(PermissionsMaster.ExpenseApprove, loggedInEmployee.Id);
|
||||
});
|
||||
|
||||
var hasProcessPermissionTask = Task.Run(async () =>
|
||||
{
|
||||
var _permission = scope.ServiceProvider.GetRequiredService<PermissionServices>();
|
||||
return await _permission.HasPermission(PermissionsMaster.ExpenseProcess, loggedInEmployee.Id);
|
||||
});
|
||||
|
||||
var hasManagePermissionTask = Task.Run(async () =>
|
||||
{
|
||||
var _permission = scope.ServiceProvider.GetRequiredService<PermissionServices>();
|
||||
return await _permission.HasPermission(PermissionsMaster.ExpenseManage, loggedInEmployee.Id);
|
||||
});
|
||||
|
||||
await Task.WhenAll(hasReviewPermissionTask, hasApprovePermissionTask, hasProcessPermissionTask, hasManagePermissionTask); // [Parallel Await]
|
||||
|
||||
var hasReviewPermission = hasReviewPermissionTask.Result;
|
||||
var hasApprovePermission = hasApprovePermissionTask.Result;
|
||||
var hasProcessPermission = hasProcessPermissionTask.Result;
|
||||
var hasManagePermission = hasManagePermissionTask.Result;
|
||||
|
||||
_logger.LogInfo(
|
||||
"Permissions resolved: Review={Review}, Approve={Approve}, Process={Process}",
|
||||
hasReviewPermission, hasApprovePermission, hasProcessPermission); // [Permissions Log]
|
||||
|
||||
// Build base query: read-only, tenant-scoped
|
||||
var baseQuery = _context.Expenses
|
||||
.Include(e => e.Status)
|
||||
.AsNoTracking() // Reduce tracking overhead for read-only list
|
||||
.Where(e => e.IsActive && e.TenantId == tenantId && e.StatusId != Processed && e.Status != null); // [Base Filter]
|
||||
|
||||
// Project to DTO in SQL to avoid heavy Include graph.
|
||||
if (projectId.HasValue)
|
||||
baseQuery = baseQuery.Where(e => e.ProjectId == projectId);
|
||||
|
||||
// Prefer ProjectTo when profiles exist; otherwise project minimal fields
|
||||
var expenses = await baseQuery
|
||||
.ToListAsync(); // Single round-trip; no Include needed for this shape
|
||||
|
||||
var draftExpenses = expenses.Where(e => e.StatusId == Draft && e.CreatedById == loggedInEmployee.Id).ToList();
|
||||
var reviewExpenses = expenses.Where(e => (hasReviewPermission || e.CreatedById == loggedInEmployee.Id) && e.StatusId == Review).ToList();
|
||||
var approveExpenses = expenses.Where(e => (hasApprovePermission || e.CreatedById == loggedInEmployee.Id) && e.StatusId == Approve).ToList();
|
||||
var processPendingExpenses = expenses.Where(e => (hasProcessPermission || e.CreatedById == loggedInEmployee.Id) && e.StatusId == ProcessPending).ToList();
|
||||
var submitedExpenses = expenses.Where(e => e.StatusId != Draft && e.CreatedById == loggedInEmployee.Id).ToList();
|
||||
var totalAmount = expenses.Where(e => e.StatusId != Draft).Sum(e => e.Amount);
|
||||
|
||||
if (hasManagePermission)
|
||||
{
|
||||
var response = new
|
||||
{
|
||||
Draft = new
|
||||
{
|
||||
Count = draftExpenses.Count,
|
||||
TotalAmount = draftExpenses.Sum(e => e.Amount)
|
||||
},
|
||||
ReviewPending = new
|
||||
{
|
||||
Count = reviewExpenses.Count,
|
||||
TotalAmount = reviewExpenses.Sum(e => e.Amount)
|
||||
},
|
||||
ApprovePending = new
|
||||
{
|
||||
Count = approveExpenses.Count,
|
||||
TotalAmount = approveExpenses.Sum(e => e.Amount)
|
||||
},
|
||||
ProcessPending = new
|
||||
{
|
||||
Count = processPendingExpenses.Count,
|
||||
TotalAmount = processPendingExpenses.Sum(e => e.Amount)
|
||||
},
|
||||
Submited = new
|
||||
{
|
||||
Count = submitedExpenses.Count,
|
||||
TotalAmount = submitedExpenses.Sum(e => e.Amount)
|
||||
},
|
||||
TotalAmount = totalAmount
|
||||
};
|
||||
_logger.LogInfo(
|
||||
"GetPendingExpenseListAsync completed. TenantId={TenantId}",
|
||||
tenantId); // [Completion Log]
|
||||
|
||||
return Ok(ApiResponse<object>.SuccessResponse(response, "Pending Expenses fetched successfully", 200)); // [Success Response]
|
||||
}
|
||||
else
|
||||
{
|
||||
var response = new
|
||||
{
|
||||
Draft = new
|
||||
{
|
||||
Count = draftExpenses.Count
|
||||
},
|
||||
ReviewPending = new
|
||||
{
|
||||
Count = reviewExpenses.Count
|
||||
},
|
||||
ApprovePending = new
|
||||
{
|
||||
Count = approveExpenses.Count
|
||||
},
|
||||
ProcessPending = new
|
||||
{
|
||||
Count = processPendingExpenses.Count
|
||||
},
|
||||
Submited = new
|
||||
{
|
||||
Count = submitedExpenses.Count
|
||||
},
|
||||
TotalAmount = totalAmount
|
||||
};
|
||||
_logger.LogInfo(
|
||||
"GetPendingExpenseListAsync completed. TenantId={TenantId}",
|
||||
tenantId); // [Completion Log]
|
||||
|
||||
return Ok(ApiResponse<object>.SuccessResponse(response, "Pending Expenses fetched successfully", 200)); // [Success Response]
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
_logger.LogWarning("GetPendingExpenseListAsync canceled by client. TenantId={TenantId}", tenantId); // [Cancel Log]
|
||||
return StatusCode(499, ApiResponse<object>.ErrorResponse("Client has canceled the opration", "Client has canceled the opration", 499)); // [Cancel Response]
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "GetPendingExpenseListAsync failed. TenantId={TenantId}", tenantId); // [Error Log]
|
||||
return StatusCode(500,
|
||||
ApiResponse<object>.ErrorResponse("An error occurred while fetching pending expenses.", "An error occurred while fetching pending expenses.", 500)); // [Error Response]
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -44,14 +44,14 @@ namespace Marco.Pms.Services.Controllers
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> GetContactList([FromQuery] string? search, [FromQuery] List<Guid>? bucketIds, [FromQuery] List<Guid>? categoryIds, [FromQuery] Guid? projectId, [FromQuery] bool active = true)
|
||||
public async Task<IActionResult> GetContactList([FromQuery] string? searchString, [FromQuery] List<Guid>? bucketIds, [FromQuery] List<Guid>? categoryIds, [FromQuery] Guid? projectId, [FromQuery] bool active = true)
|
||||
{
|
||||
ContactFilterDto filterDto = new ContactFilterDto
|
||||
{
|
||||
BucketIds = bucketIds,
|
||||
CategoryIds = categoryIds
|
||||
};
|
||||
var response = await _directoryService.GetListOfContactsOld(search, active, filterDto, projectId);
|
||||
var response = await _directoryService.GetListOfContactsOld(searchString, active, filterDto, projectId);
|
||||
|
||||
|
||||
return StatusCode(response.StatusCode, response);
|
||||
|
@ -35,6 +35,7 @@ namespace Marco.Pms.Services.Controllers
|
||||
private readonly ILoggingService _logger;
|
||||
private readonly IMapper _mapper;
|
||||
private readonly Guid tenantId;
|
||||
private readonly Guid organizationId;
|
||||
|
||||
private static readonly Guid ProjectEntity = Guid.Parse("c8fe7115-aa27-43bc-99f4-7b05fabe436e");
|
||||
private static readonly Guid EmployeeEntity = Guid.Parse("dbb9555a-7a0c-40f2-a9ed-f0463f1ceed7");
|
||||
@ -52,6 +53,7 @@ namespace Marco.Pms.Services.Controllers
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
_mapper = mapper ?? throw new ArgumentNullException(nameof(mapper));
|
||||
tenantId = userHelper.GetTenantId();
|
||||
organizationId = _userHelper.GetCurrentOrganizationId();
|
||||
}
|
||||
|
||||
[HttpGet("list/{entityTypeId}/entity/{entityId}")]
|
||||
@ -93,21 +95,21 @@ namespace Marco.Pms.Services.Controllers
|
||||
return NotFound(ApiResponse<object>.ErrorResponse("Entity type not found", "Entity Type not found in database", 404));
|
||||
}
|
||||
|
||||
// Project permission check
|
||||
if (ProjectEntity == entityTypeId)
|
||||
{
|
||||
var hasProjectPermission = await _permission.HasProjectPermission(loggedInEmployee, entityId);
|
||||
if (!hasProjectPermission)
|
||||
{
|
||||
_logger.LogWarning("Employee {EmployeeId} does not have project access for ProjectId {ProjectId}", loggedInEmployee.Id, entityId);
|
||||
return StatusCode(403, ApiResponse<object>.ErrorResponse("Access Denied.", "You do not have permission to access project documents", 403));
|
||||
}
|
||||
}
|
||||
//// Project permission check
|
||||
//if (ProjectEntity == entityTypeId)
|
||||
//{
|
||||
// var hasProjectPermission = await _permission.HasProjectPermission(loggedInEmployee, entityId);
|
||||
// if (!hasProjectPermission)
|
||||
// {
|
||||
// _logger.LogWarning("Employee {EmployeeId} does not have project access for ProjectId {ProjectId}", loggedInEmployee.Id, entityId);
|
||||
// return StatusCode(403, ApiResponse<object>.ErrorResponse("Access Denied.", "You do not have permission to access project documents", 403));
|
||||
// }
|
||||
//}
|
||||
// Employee validation
|
||||
else if (EmployeeEntity == entityTypeId)
|
||||
{
|
||||
var isEmployeeExists = await _context.Employees
|
||||
.AnyAsync(e => e.Id == entityId && e.TenantId == tenantId);
|
||||
.AnyAsync(e => e.Id == entityId && e.OrganizationId == organizationId);
|
||||
|
||||
if (!isEmployeeExists)
|
||||
{
|
||||
@ -691,7 +693,7 @@ namespace Marco.Pms.Services.Controllers
|
||||
bool entityExists = false;
|
||||
if (entityType.Equals(EmployeeEntity))
|
||||
{
|
||||
entityExists = await _context.Employees.AnyAsync(e => e.Id == model.EntityId && e.TenantId == tenantId);
|
||||
entityExists = await _context.Employees.AnyAsync(e => e.Id == model.EntityId && e.OrganizationId == organizationId);
|
||||
}
|
||||
else if (entityType.Equals(ProjectEntity))
|
||||
{
|
||||
@ -1078,15 +1080,15 @@ namespace Marco.Pms.Services.Controllers
|
||||
bool entityExists;
|
||||
if (entityType.Equals(EmployeeEntity))
|
||||
{
|
||||
entityExists = await _context.Employees.AnyAsync(e => e.Id == oldAttachment.EntityId && e.TenantId == tenantId);
|
||||
entityExists = await _context.Employees.AnyAsync(e => e.Id == oldAttachment.EntityId && e.OrganizationId == organizationId);
|
||||
}
|
||||
else if (entityType.Equals(ProjectEntity))
|
||||
{
|
||||
entityExists = await _context.Projects.AnyAsync(p => p.Id == oldAttachment.EntityId && p.TenantId == tenantId);
|
||||
if (entityExists)
|
||||
{
|
||||
entityExists = await _permission.HasProjectPermission(loggedInEmployee, oldAttachment.EntityId);
|
||||
}
|
||||
//if (entityExists)
|
||||
//{
|
||||
// entityExists = await _permission.HasProjectPermission(loggedInEmployee, oldAttachment.EntityId);
|
||||
//}
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -233,83 +233,11 @@ namespace MarcoBMS.Services.Controllers
|
||||
_logger.LogInfo("GetEmployeesByProject called. EmployeeId: {EmployeeId}, ProjectId: {ProjectId}, showInactive: {ShowInactive}",
|
||||
loggedInEmployee.Id, projectId ?? Guid.Empty, showInactive);
|
||||
|
||||
// Step 3: Fetch permissions concurrently
|
||||
var viewAllTask = Task.Run(async () =>
|
||||
{
|
||||
var _permission = scope.ServiceProvider.GetRequiredService<PermissionServices>();
|
||||
return await _permission.HasPermission(PermissionsMaster.ViewAllEmployees, loggedInEmployee.Id);
|
||||
});
|
||||
var viewTeamTask = Task.Run(async () =>
|
||||
{
|
||||
var _permission = scope.ServiceProvider.GetRequiredService<PermissionServices>();
|
||||
return await _permission.HasPermission(PermissionsMaster.ViewTeamMembers, loggedInEmployee.Id);
|
||||
});
|
||||
|
||||
await Task.WhenAll(viewAllTask, viewTeamTask);
|
||||
|
||||
var hasViewAllEmployeesPermission = viewAllTask.Result;
|
||||
var hasViewTeamMembersPermission = viewTeamTask.Result;
|
||||
|
||||
List<Employee> employees = new List<Employee>();
|
||||
|
||||
// Step 4: Query based on permission
|
||||
if (hasViewAllEmployeesPermission && !projectId.HasValue)
|
||||
{
|
||||
// OrganizationId needs to be retrieved from loggedInEmployee or context based on your app's structure
|
||||
var employeeQuery = _context.Employees
|
||||
.AsNoTracking() // Optimize EF query for read-only operation[web:1][web:13][web:18]
|
||||
.Include(e => e.JobRole)
|
||||
.Where(e => e.OrganizationId == organizationId);
|
||||
|
||||
employeeQuery = showInactive
|
||||
? employeeQuery.Where(e => !e.IsActive)
|
||||
: employeeQuery.Where(e => e.IsActive);
|
||||
|
||||
employees = await employeeQuery.ToListAsync();
|
||||
_logger.LogInfo("Employee list fetched with full access. Count: {Count}", employees.Count);
|
||||
}
|
||||
else if (hasViewTeamMembersPermission && !showInactive && !projectId.HasValue)
|
||||
{
|
||||
// Only active team members with limited permission
|
||||
var projectIds = await _projectServices.GetMyProjectIdsAsync(tenantId, loggedInEmployee);
|
||||
|
||||
employees = await _context.ProjectAllocations
|
||||
.AsNoTracking()
|
||||
.Include(pa => pa.Employee)
|
||||
.ThenInclude(e => e!.JobRole)
|
||||
.Where(pa =>
|
||||
projectIds.Contains(pa.ProjectId)
|
||||
&& pa.IsActive
|
||||
&& pa.Employee != null
|
||||
&& pa.Employee.IsActive
|
||||
&& pa.TenantId == tenantId)
|
||||
.Select(pa => pa.Employee!)
|
||||
.Distinct()
|
||||
.ToListAsync();
|
||||
|
||||
_logger.LogInfo("Employee list fetched with limited access (active only). Count: {Count}", employees.Count);
|
||||
}
|
||||
|
||||
// If a specific projectId is provided, override employee fetching to ensure strict project context
|
||||
if (projectId.HasValue)
|
||||
{
|
||||
employees = await _context.ProjectAllocations
|
||||
.AsNoTracking()
|
||||
.Include(pa => pa.Employee)
|
||||
.ThenInclude(e => e!.JobRole)
|
||||
.Where(pa =>
|
||||
pa.ProjectId == projectId
|
||||
&& pa.IsActive
|
||||
&& pa.Employee != null
|
||||
&& pa.Employee.IsActive
|
||||
&& pa.TenantId == tenantId)
|
||||
.Select(pa => pa.Employee!)
|
||||
.Distinct()
|
||||
.ToListAsync();
|
||||
|
||||
_logger.LogInfo("Employee list fetched for specific project. ProjectId: {ProjectId}. Count: {Count}",
|
||||
projectId, employees.Count);
|
||||
}
|
||||
var employees = await _context.Employees
|
||||
.Include(e => e.JobRole)
|
||||
.Include(e => e.Organization)
|
||||
.Where(e => e.OrganizationId == loggedInEmployee.OrganizationId && e.IsActive != showInactive)
|
||||
.ToListAsync();
|
||||
|
||||
// Step 5: Map to view model
|
||||
result = employees.Select(e => _mapper.Map<EmployeeVM>(e)).Distinct().ToList();
|
||||
@ -329,7 +257,7 @@ namespace MarcoBMS.Services.Controllers
|
||||
|
||||
|
||||
[HttpGet("basic")]
|
||||
public async Task<IActionResult> GetEmployeesByProjectBasic(Guid? projectId, [FromQuery] string? searchString, [FromQuery] bool allEmployee)
|
||||
public async Task<IActionResult> GetEmployeesByProjectBasic(Guid? projectId, [FromQuery] string? searchString, [FromQuery] bool sendAll = false)
|
||||
{
|
||||
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
|
||||
var employeeQuery = _context.Employees.Where(e => e.IsActive);
|
||||
@ -353,17 +281,11 @@ namespace MarcoBMS.Services.Controllers
|
||||
var searchStringLower = searchString.ToLower();
|
||||
employeeQuery = employeeQuery.Where(e => (e.FirstName + " " + e.LastName).ToLower().Contains(searchStringLower));
|
||||
}
|
||||
|
||||
var query = employeeQuery.OrderBy(e => e.FirstName);
|
||||
|
||||
if (!allEmployee)
|
||||
if (!sendAll)
|
||||
{
|
||||
query = (IOrderedQueryable<Employee>)query.Take(10);
|
||||
employeeQuery = employeeQuery.Take(10);
|
||||
}
|
||||
|
||||
var response = await query
|
||||
.Select(e => _mapper.Map<BasicEmployeeVM>(e))
|
||||
.ToListAsync();
|
||||
var response = await employeeQuery.Select(e => _mapper.Map<BasicEmployeeVM>(e)).ToListAsync();
|
||||
return Ok(ApiResponse<object>.SuccessResponse(response, $"{response.Count} records of employees fetched successfully", 200));
|
||||
}
|
||||
|
||||
@ -479,6 +401,9 @@ namespace MarcoBMS.Services.Controllers
|
||||
Guid tenantId = _userHelper.GetTenantId();
|
||||
var LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
|
||||
Guid employeeId = Guid.Empty;
|
||||
|
||||
|
||||
|
||||
if (model == null)
|
||||
return BadRequest(ApiResponse<object>.ErrorResponse("Invalid data", "Invaild Data", 400));
|
||||
|
||||
@ -611,6 +536,7 @@ namespace MarcoBMS.Services.Controllers
|
||||
{
|
||||
// Correlation and context capture for logs
|
||||
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
|
||||
Guid organizationId = model.OrganizationId ?? loggedInEmployee.OrganizationId;
|
||||
|
||||
{
|
||||
if (model == null)
|
||||
@ -634,10 +560,10 @@ namespace MarcoBMS.Services.Controllers
|
||||
if (model.Id.HasValue && model.Id.Value != Guid.Empty)
|
||||
{
|
||||
existingEmployee = await _context.Employees
|
||||
.FirstOrDefaultAsync(e => e.Id == model.Id);
|
||||
.FirstOrDefaultAsync(e => e.Id == model.Id && e.OrganizationId == organizationId);
|
||||
if (existingEmployee == null)
|
||||
{
|
||||
_logger.LogInfo("Employee not found for update. Id={EmployeeId}", model.Id);
|
||||
_logger.LogInfo("Employee not found for update. Id={EmployeeId}, Org={OrgId}", model.Id, organizationId);
|
||||
return NotFound(ApiResponse<object>.ErrorResponse("Employee not found", "Employee not found in database", 404));
|
||||
}
|
||||
}
|
||||
@ -732,7 +658,7 @@ namespace MarcoBMS.Services.Controllers
|
||||
existingEmployee.ApplicationUserId = createdIdentityUser.Id;
|
||||
await SendResetIfApplicableAsync(createdIdentityUser, existingEmployee.FirstName ?? "User");
|
||||
}
|
||||
|
||||
existingEmployee.OrganizationId = organizationId;
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
employeeId = existingEmployee.Id;
|
||||
@ -754,7 +680,7 @@ namespace MarcoBMS.Services.Controllers
|
||||
newEmployee.ApplicationUserId = createdIdentityUser.Id;
|
||||
await SendResetIfApplicableAsync(createdIdentityUser, newEmployee.FirstName ?? "User");
|
||||
}
|
||||
|
||||
newEmployee.OrganizationId = organizationId;
|
||||
await _context.Employees.AddAsync(newEmployee);
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
@ -886,6 +812,7 @@ namespace MarcoBMS.Services.Controllers
|
||||
public async Task<IActionResult> CreateUserMobileAsync([FromBody] MobileUserManageDto model)
|
||||
{
|
||||
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
|
||||
Guid organizationId = model.OrganizationId ?? loggedInEmployee.OrganizationId;
|
||||
if (tenantId == Guid.Empty)
|
||||
{
|
||||
_logger.LogWarning("Tenant resolution failed in CreateUserMobile"); // structured warning
|
||||
@ -921,11 +848,11 @@ namespace MarcoBMS.Services.Controllers
|
||||
if (model.Id == null || model.Id == Guid.Empty)
|
||||
{
|
||||
var emailExists = await _context.Employees
|
||||
.AnyAsync(e => e.Email == model.Email && e.OrganizationId == model.OrganizationId);
|
||||
.AnyAsync(e => e.Email == model.Email);
|
||||
|
||||
if (emailExists && !string.IsNullOrWhiteSpace(model.Email))
|
||||
if (emailExists)
|
||||
{
|
||||
_logger.LogInfo("Employee email already exists in org. Email={Email}, Org={OrgId}", model.Email, model.OrganizationId);
|
||||
_logger.LogInfo("Employee email already exists in org. Email={Email}, Org={OrgId}", model.Email ?? string.Empty, organizationId);
|
||||
return StatusCode(409, ApiResponse<object>.ErrorResponse("Employee with email already exists", "Employee with this email already exists", 409));
|
||||
}
|
||||
|
||||
@ -942,7 +869,7 @@ namespace MarcoBMS.Services.Controllers
|
||||
JoiningDate = model.JoiningDate,
|
||||
JobRoleId = model.JobRoleId,
|
||||
Photo = imageBytes,
|
||||
OrganizationId = model.OrganizationId,
|
||||
OrganizationId = organizationId,
|
||||
HasApplicationAccess = model.HasApplicationAccess,
|
||||
};
|
||||
|
||||
@ -995,7 +922,7 @@ namespace MarcoBMS.Services.Controllers
|
||||
// Update path: fetch scoped to tenant
|
||||
var employeeId = model.Id.Value;
|
||||
var existingEmployee = await _context.Employees
|
||||
.FirstOrDefaultAsync(e => e.Id == employeeId); // tenant-safe lookup
|
||||
.FirstOrDefaultAsync(e => e.Id == employeeId && e.TenantId == tenantId); // tenant-safe lookup
|
||||
|
||||
if (existingEmployee is null)
|
||||
{
|
||||
@ -1010,17 +937,17 @@ namespace MarcoBMS.Services.Controllers
|
||||
existingEmployee.PhoneNumber = model.PhoneNumber;
|
||||
existingEmployee.JoiningDate = model.JoiningDate;
|
||||
existingEmployee.JobRoleId = model.JobRoleId;
|
||||
existingEmployee.OrganizationId = model.OrganizationId;
|
||||
existingEmployee.OrganizationId = organizationId;
|
||||
existingEmployee.HasApplicationAccess = model.HasApplicationAccess;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(existingEmployee.Email) && !string.IsNullOrWhiteSpace(model.Email))
|
||||
{
|
||||
var emailExists = await _context.Employees
|
||||
.AnyAsync(e => e.Email == model.Email);
|
||||
.AnyAsync(e => e.Email == model.Email && e.OrganizationId == model.OrganizationId);
|
||||
|
||||
if (emailExists)
|
||||
{
|
||||
_logger.LogInfo("Employee email already exists in org. Email={Email}, Org={OrgId}", model.Email, model.OrganizationId);
|
||||
_logger.LogInfo("Employee email already exists in org. Email={Email}, Org={OrgId}", model.Email, organizationId);
|
||||
return StatusCode(409, ApiResponse<object>.ErrorResponse("Employee with email already exists", "Employee with this email already exists", 409));
|
||||
}
|
||||
existingEmployee.Email = model.Email;
|
||||
@ -1084,7 +1011,7 @@ namespace MarcoBMS.Services.Controllers
|
||||
|
||||
Guid tenantId = _userHelper.GetTenantId();
|
||||
var LoggedEmployee = await _userHelper.GetCurrentEmployeeAsync();
|
||||
Employee? employee = await _context.Employees.FirstOrDefaultAsync(e => e.Id == id && e.TenantId == tenantId);
|
||||
Employee? employee = await _context.Employees.FirstOrDefaultAsync(e => e.Id == id && e.OrganizationId == organizationId);
|
||||
if (employee == null)
|
||||
{
|
||||
_logger.LogWarning("Employee with ID {EmploueeId} not found in database", id);
|
||||
@ -1254,13 +1181,17 @@ namespace MarcoBMS.Services.Controllers
|
||||
|
||||
}
|
||||
|
||||
|
||||
// Prepare reset link sender helper
|
||||
private async Task SendResetIfApplicableAsync(ApplicationUser u, string firstName)
|
||||
{
|
||||
var token = await _userManager.GeneratePasswordResetTokenAsync(u);
|
||||
var resetLink = $"{_configuration["AppSettings:WebFrontendUrl"]}/reset-password?token={WebUtility.UrlEncode(token)}";
|
||||
await _emailSender.SendResetPasswordEmailOnRegister(u.Email ?? "", firstName, resetLink);
|
||||
_logger.LogInfo("Reset password email queued. Email={Email}", u.Email ?? "");
|
||||
if (!string.IsNullOrWhiteSpace(u.Email))
|
||||
{
|
||||
var token = await _userManager.GeneratePasswordResetTokenAsync(u);
|
||||
var resetLink = $"{_configuration["AppSettings:WebFrontendUrl"]}/reset-password?token={WebUtility.UrlEncode(token)}";
|
||||
await _emailSender.SendResetPasswordEmailOnRegister(u.Email, firstName, resetLink);
|
||||
_logger.LogInfo("Reset password email queued. Email={Email}", u.Email);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -106,7 +106,7 @@ namespace Marco.Pms.Services.Controllers
|
||||
t.WorkItem.WorkArea != null &&
|
||||
t.WorkItem.WorkArea.Floor != null &&
|
||||
t.WorkItem.WorkArea.Floor.Building != null &&
|
||||
t.WorkItem.WorkArea.Floor.Building.ProjectId == projectId &&
|
||||
t.WorkItem.WorkArea.Floor.Building.ProjectId != projectId &&
|
||||
t.TenantId == tenantId);
|
||||
|
||||
// Step 4: Extract filter values
|
||||
|
@ -9,7 +9,6 @@ using Marco.Pms.Model.ViewModels.Activities;
|
||||
using Marco.Pms.Model.ViewModels.Master;
|
||||
using Marco.Pms.Model.ViewModels.Organization;
|
||||
using Marco.Pms.Model.ViewModels.Projects;
|
||||
using Marco.Pms.Services.Helpers;
|
||||
using Marco.Pms.Services.Service;
|
||||
using MarcoBMS.Services.Helpers;
|
||||
using MarcoBMS.Services.Service;
|
||||
@ -31,7 +30,6 @@ namespace Marco.Pms.Services.Controllers
|
||||
private readonly UserHelper _userHelper;
|
||||
private readonly Guid tenantId;
|
||||
private readonly IMapper _mapper;
|
||||
private readonly Guid loggedOrganizationId;
|
||||
private readonly ILoggingService _logger;
|
||||
|
||||
private static readonly Guid PMCProvider = Guid.Parse("b1877a3b-8832-47b1-bbe3-dc7e98672f49");
|
||||
@ -49,7 +47,6 @@ namespace Marco.Pms.Services.Controllers
|
||||
_userHelper = userHelper ?? throw new ArgumentNullException(nameof(userHelper));
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
_mapper = mapper ?? throw new ArgumentNullException(nameof(mapper));
|
||||
loggedOrganizationId = _userHelper.GetCurrentOrganizationId();
|
||||
tenantId = userHelper.GetTenantId();
|
||||
}
|
||||
#region =================================================================== Get Functions ===================================================================
|
||||
@ -672,8 +669,6 @@ namespace Marco.Pms.Services.Controllers
|
||||
Service = _mapper.Map<ServiceMasterVM>(s)
|
||||
}).ToList();
|
||||
|
||||
await AssignApplicationRoleToOrganization(organization.Id, project.TenantId);
|
||||
|
||||
return Ok(ApiResponse<object>.SuccessResponse(response, "Organization successfully assigned to the project", 200));
|
||||
}
|
||||
catch (DbUpdateException dbEx)
|
||||
@ -722,36 +717,34 @@ namespace Marco.Pms.Services.Controllers
|
||||
return NotFound(ApiResponse<object>.ErrorResponse("Organization not found", "Organization not found in database", 404));
|
||||
}
|
||||
|
||||
if (organizationTenantMapping != null)
|
||||
if (organizationTenantMapping == null)
|
||||
{
|
||||
// Create new tenant-organization mapping if none exists
|
||||
var newMapping = new TenantOrgMapping
|
||||
{
|
||||
OrganizationId = organization.Id,
|
||||
SPRID = organization.SPRID,
|
||||
AssignedDate = DateTime.UtcNow,
|
||||
IsActive = true,
|
||||
AssignedById = loggedInEmployee.Id,
|
||||
TenantId = tenantId
|
||||
};
|
||||
_context.TenantOrgMappings.Add(newMapping);
|
||||
await _context.SaveChangesAsync();
|
||||
await transaction.CommitAsync();
|
||||
|
||||
_logger.LogInfo("Assigned organization {OrganizationId} to tenant {TenantId} successfully.", organizationId, tenantId);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogInfo("Organization {OrganizationId} is already assigned to tenant {TenantId}. No action taken.", organizationId, tenantId);
|
||||
// Commit transaction anyway to complete scope cleanly (optional)
|
||||
await transaction.RollbackAsync();
|
||||
return StatusCode(409, ApiResponse<object>.ErrorResponse("Organization is already assigned to tenant", "Organization is already assigned to tenant", 409));
|
||||
await transaction.CommitAsync();
|
||||
}
|
||||
|
||||
// Create new tenant-organization mapping if none exists
|
||||
var newMapping = new TenantOrgMapping
|
||||
{
|
||||
OrganizationId = organization.Id,
|
||||
SPRID = organization.SPRID,
|
||||
AssignedDate = DateTime.UtcNow,
|
||||
IsActive = true,
|
||||
AssignedById = loggedInEmployee.Id,
|
||||
TenantId = tenantId
|
||||
};
|
||||
_context.TenantOrgMappings.Add(newMapping);
|
||||
await _context.SaveChangesAsync();
|
||||
await transaction.CommitAsync();
|
||||
|
||||
_logger.LogInfo("Assigned organization {OrganizationId} to tenant {TenantId} successfully.", organizationId, tenantId);
|
||||
|
||||
|
||||
// Prepare response view model
|
||||
var response = _mapper.Map<BasicOrganizationVm>(organization);
|
||||
|
||||
await AssignApplicationRoleToOrganization(organization.Id, tenantId);
|
||||
|
||||
return Ok(ApiResponse<object>.SuccessResponse(response, "Organization has been assigned to tenant", 200));
|
||||
}
|
||||
catch (DbUpdateException dbEx)
|
||||
@ -945,98 +938,45 @@ namespace Marco.Pms.Services.Controllers
|
||||
#endregion
|
||||
|
||||
#region =================================================================== Helper Functions ===================================================================
|
||||
//private ServicesProviderFilter? TryDeserializeServicesProviderFilter(string? filter)
|
||||
//{
|
||||
// if (string.IsNullOrWhiteSpace(filter))
|
||||
// {
|
||||
// return null;
|
||||
// }
|
||||
|
||||
private async Task AssignApplicationRoleToOrganization(Guid organizationId, Guid tenantId)
|
||||
{
|
||||
if (loggedOrganizationId == organizationId)
|
||||
{
|
||||
return;
|
||||
}
|
||||
await using var _context = await _dbContextFactory.CreateDbContextAsync();
|
||||
using var scope = _serviceScope.CreateScope();
|
||||
// var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true };
|
||||
// ServicesProviderFilter? documentFilter = null;
|
||||
|
||||
var rootEmployee = await _context.Employees
|
||||
.Include(e => e.ApplicationUser)
|
||||
.FirstOrDefaultAsync(e => e.ApplicationUser != null && e.ApplicationUser.IsRootUser.HasValue && e.ApplicationUser.IsRootUser.Value && e.OrganizationId == organizationId && e.IsPrimary);
|
||||
if (rootEmployee == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
string serviceProviderRoleName = "Service Provider Role";
|
||||
// try
|
||||
// {
|
||||
// // First, try to deserialize directly. This is the expected case (e.g., from a web client).
|
||||
// documentFilter = JsonSerializer.Deserialize<ServicesProviderFilter>(filter, options);
|
||||
// }
|
||||
// catch (JsonException ex)
|
||||
// {
|
||||
// _logger.LogError(ex, "[{MethodName}] Failed to directly deserialize filter. Attempting to unescape and re-parse. Filter: {Filter}", nameof(TryDeserializeServicesProviderFilter), filter);
|
||||
|
||||
var serviceProviderRole = await _context.ApplicationRoles.FirstOrDefaultAsync(ar => ar.Role == serviceProviderRoleName && ar.TenantId == tenantId);
|
||||
if (serviceProviderRole == null)
|
||||
{
|
||||
serviceProviderRole = new Model.Roles.ApplicationRole
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Role = serviceProviderRoleName,
|
||||
Description = serviceProviderRoleName,
|
||||
IsSystem = true,
|
||||
TenantId = tenantId
|
||||
};
|
||||
_context.ApplicationRoles.Add(serviceProviderRole);
|
||||
// // If direct deserialization fails, it might be an escaped string (common with tools like Postman or some mobile clients).
|
||||
// try
|
||||
// {
|
||||
// // Unescape the string first, then deserialize the result.
|
||||
// string unescapedJsonString = JsonSerializer.Deserialize<string>(filter, options) ?? "";
|
||||
// if (!string.IsNullOrWhiteSpace(unescapedJsonString))
|
||||
// {
|
||||
// documentFilter = JsonSerializer.Deserialize<ServicesProviderFilter>(unescapedJsonString, options);
|
||||
// }
|
||||
// }
|
||||
// catch (JsonException ex1)
|
||||
// {
|
||||
// // If both attempts fail, log the final error and return null.
|
||||
// _logger.LogError(ex1, "[{MethodName}] All attempts to deserialize the filter failed. Filter will be ignored. Filter: {Filter}", nameof(TryDeserializeServicesProviderFilter), filter);
|
||||
// return null;
|
||||
// }
|
||||
// }
|
||||
// return documentFilter;
|
||||
//}
|
||||
|
||||
var rolePermissionMappigs = new List<RolePermissionMappings> {
|
||||
new RolePermissionMappings
|
||||
{
|
||||
ApplicationRoleId = serviceProviderRole.Id,
|
||||
FeaturePermissionId = PermissionsMaster.ViewProject
|
||||
},
|
||||
new RolePermissionMappings
|
||||
{
|
||||
ApplicationRoleId = serviceProviderRole.Id,
|
||||
FeaturePermissionId = PermissionsMaster.ViewProjectInfra
|
||||
},
|
||||
new RolePermissionMappings
|
||||
{
|
||||
ApplicationRoleId = serviceProviderRole.Id,
|
||||
FeaturePermissionId = PermissionsMaster.ViewTask
|
||||
},
|
||||
new RolePermissionMappings
|
||||
{
|
||||
ApplicationRoleId = serviceProviderRole.Id,
|
||||
FeaturePermissionId = PermissionsMaster.ViewAllEmployees
|
||||
},
|
||||
new RolePermissionMappings
|
||||
{
|
||||
ApplicationRoleId = serviceProviderRole.Id,
|
||||
FeaturePermissionId = PermissionsMaster.TeamAttendance
|
||||
},
|
||||
new RolePermissionMappings
|
||||
{
|
||||
ApplicationRoleId = serviceProviderRole.Id,
|
||||
FeaturePermissionId = PermissionsMaster.AssignRoles
|
||||
},
|
||||
new RolePermissionMappings
|
||||
{
|
||||
ApplicationRoleId = serviceProviderRole.Id,
|
||||
FeaturePermissionId = PermissionsMaster.ManageProjectInfra
|
||||
},
|
||||
new RolePermissionMappings
|
||||
{
|
||||
ApplicationRoleId = serviceProviderRole.Id,
|
||||
FeaturePermissionId = PermissionsMaster.AssignAndReportProgress
|
||||
},
|
||||
new RolePermissionMappings
|
||||
{
|
||||
ApplicationRoleId = serviceProviderRole.Id,
|
||||
FeaturePermissionId = PermissionsMaster.AddAndEditTask
|
||||
}
|
||||
};
|
||||
_context.RolePermissionMappings.AddRange(rolePermissionMappigs);
|
||||
}
|
||||
_context.EmployeeRoleMappings.Add(new EmployeeRoleMapping
|
||||
{
|
||||
EmployeeId = rootEmployee.Id,
|
||||
RoleId = serviceProviderRole.Id,
|
||||
IsEnabled = true,
|
||||
TenantId = tenantId
|
||||
});
|
||||
|
||||
var _cache = scope.ServiceProvider.GetRequiredService<CacheUpdateHelper>();
|
||||
await _cache.ClearAllPermissionIdsByEmployeeID(rootEmployee.Id, tenantId);
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
@ -560,13 +560,6 @@ namespace MarcoBMS.Services.Controllers
|
||||
var response = await _projectServices.GetAssignedOrganizationsToProjectAsync(projectId, tenantId, loggedInEmployee);
|
||||
return StatusCode(response.StatusCode, response);
|
||||
}
|
||||
[HttpGet("get/assigned/organization/dropdown/{projectId}")]
|
||||
public async Task<IActionResult> GetAssignedOrganizationsToProjectForDropdownAsync(Guid projectId)
|
||||
{
|
||||
Employee loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
|
||||
var response = await _projectServices.GetAssignedOrganizationsToProjectForDropdownAsync(projectId, tenantId, loggedInEmployee);
|
||||
return StatusCode(response.StatusCode, response);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
@ -449,7 +449,7 @@ namespace Marco.Pms.Services.Controllers
|
||||
}
|
||||
|
||||
[HttpGet("report-attendance")]
|
||||
public async Task<IActionResult> GetAttendanceReportAsync([FromQuery] bool isCurrentMonth = false)
|
||||
public async Task<IActionResult> GetAttendanceReportAsync()
|
||||
{
|
||||
Guid tenantId = _userHelper.GetTenantId();
|
||||
using var scope = _serviceScopeFactory.CreateScope();
|
||||
@ -458,75 +458,48 @@ namespace Marco.Pms.Services.Controllers
|
||||
DateTime firstDayOfMonth = new DateTime(today.Year, today.Month, 1);
|
||||
DateTime firstDayOfNextMonth = firstDayOfMonth.AddMonths(1);
|
||||
|
||||
if (!isCurrentMonth)
|
||||
{
|
||||
firstDayOfNextMonth = firstDayOfMonth;
|
||||
firstDayOfMonth = firstDayOfMonth.AddMonths(-1);
|
||||
}
|
||||
|
||||
// Generate list of all dates in the month
|
||||
var allDates = Enumerable.Range(0, (firstDayOfNextMonth - firstDayOfMonth).Days)
|
||||
.Select(offset => firstDayOfMonth.AddDays(offset))
|
||||
.ToList();
|
||||
|
||||
var attendancesTask = Task.Run(async () =>
|
||||
{
|
||||
await using var context = await _dbContextFactory.CreateDbContextAsync();
|
||||
return await context.Attendes
|
||||
.Where(a => a.AttendanceDate >= firstDayOfMonth && a.AttendanceDate < firstDayOfNextMonth && a.Employee != null && a.TenantId == tenantId)
|
||||
.GroupBy(a => a.ProjectID)
|
||||
.ToListAsync();
|
||||
});
|
||||
|
||||
var projectAllocationTask = Task.Run(async () =>
|
||||
{
|
||||
await using var context = await _dbContextFactory.CreateDbContextAsync();
|
||||
return await context.ProjectAllocations
|
||||
.Include(pa => pa.Employee)
|
||||
.Where(pa => pa.TenantId == tenantId && pa.IsActive)
|
||||
.ToListAsync();
|
||||
});
|
||||
|
||||
await Task.WhenAll(attendancesTask, projectAllocationTask);
|
||||
|
||||
var attendances = attendancesTask.Result;
|
||||
var projectAllocations = projectAllocationTask.Result;
|
||||
var attendances = await _context.Attendes
|
||||
.Include(a => a.Employee)
|
||||
.Where(a => a.AttendanceDate >= firstDayOfMonth && a.AttendanceDate < firstDayOfNextMonth && a.Employee != null && a.TenantId == tenantId)
|
||||
.GroupBy(a => a.ProjectID)
|
||||
.ToListAsync();
|
||||
|
||||
var result = attendances.Select(g =>
|
||||
{
|
||||
var projectAllocation = projectAllocations.Where(pa => pa.ProjectId == g.Key && pa.Employee != null).ToList();
|
||||
var projectAttendance = projectAllocation.Select(pa =>
|
||||
{
|
||||
var attendances = g.Where(a => a.EmployeeId == pa.EmployeeId).ToList();
|
||||
var attendanceDate = attendances.Select(a => a.AttendanceDate.Date).ToList();
|
||||
return new
|
||||
{
|
||||
FirstName = pa.Employee!.FirstName,
|
||||
LastName = pa.Employee.LastName,
|
||||
Attendances = allDates.Select(d =>
|
||||
{
|
||||
var attendance = attendances.FirstOrDefault(a => a.AttendanceDate.Date == d);
|
||||
return new
|
||||
{
|
||||
AttendanceDate = d,
|
||||
CheckIn = attendance?.InTime,
|
||||
CheckOut = attendance?.OutTime,
|
||||
Activity = attendance?.Activity,
|
||||
IsApproved = attendance?.ApprovedById.HasValue,
|
||||
};
|
||||
}).ToList(),
|
||||
CheckInCheckOutDone = attendances.Where(a => a.InTime.HasValue && a.OutTime.HasValue && a.Activity == ATTENDANCE_MARK_TYPE.REGULARIZE).Count(),
|
||||
CheckInDone = attendances.Where(a => a.InTime.HasValue).Count(),
|
||||
CheckOutPending = attendances.Where(a => a.InTime.HasValue && !a.OutTime.HasValue).Count(),
|
||||
RejectedRegularize = attendances.Where(a => a.Activity == ATTENDANCE_MARK_TYPE.REGULARIZE_REJECT).Count(),
|
||||
AbsentAttendance = allDates.Where(d => !attendanceDate.Contains(d) && d.DayOfWeek != DayOfWeek.Sunday).Count()
|
||||
};
|
||||
}).OrderBy(ar => ar.FirstName).ThenBy(ar => ar.LastName).ToList();
|
||||
|
||||
return new
|
||||
{
|
||||
ProjectName = _context.Projects.Where(p => p.Id == g.Key && p.TenantId == tenantId).Select(p => p.Name).FirstOrDefault(),
|
||||
ProjectAttendance = projectAttendance
|
||||
ProjectAttendance = g.GroupBy(a => a.Employee).Select(gp =>
|
||||
{
|
||||
var attendanceDate = gp.Select(a => a.AttendanceDate.Date).ToList();
|
||||
return new
|
||||
{
|
||||
FirstName = gp.Key!.FirstName,
|
||||
LastName = gp.Key.LastName,
|
||||
Attendances = allDates.Select(d =>
|
||||
{
|
||||
var attendance = gp.FirstOrDefault(a => a.AttendanceDate.Date == d);
|
||||
return new
|
||||
{
|
||||
AttendanceDate = d,
|
||||
CheckIn = attendance?.InTime,
|
||||
CheckOut = attendance?.OutTime,
|
||||
Activity = attendance?.Activity,
|
||||
IsApproved = attendance?.ApprovedById.HasValue,
|
||||
};
|
||||
}).ToList(),
|
||||
CheckInCheckOutDone = gp.Where(a => a.InTime.HasValue && a.OutTime.HasValue && a.Activity == ATTENDANCE_MARK_TYPE.REGULARIZE).Count(),
|
||||
CheckInDone = gp.Where(a => a.InTime.HasValue).Count(),
|
||||
CheckOutPending = gp.Where(a => a.InTime.HasValue && !a.OutTime.HasValue).Count(),
|
||||
RejectedRegularize = gp.Where(a => a.Activity == ATTENDANCE_MARK_TYPE.REGULARIZE_REJECT).Count(),
|
||||
AbsentAttendance = allDates.Where(d => !attendanceDate.Contains(d) && d.DayOfWeek != DayOfWeek.Sunday).Count()
|
||||
};
|
||||
}).OrderBy(ar => ar.FirstName).ThenBy(ar => ar.LastName).ToList()
|
||||
};
|
||||
}).ToList();
|
||||
|
||||
|
@ -273,12 +273,7 @@ namespace Marco.Pms.Services.Controllers
|
||||
return StatusCode(403,
|
||||
ApiResponse<object>.ErrorResponse("Access denied", "User does not have the required permissions for this action.", 403));
|
||||
}
|
||||
if (!hasManagePermission && (hasModifyPermission || hasViewPermission) && id != loggedInEmployee.TenantId)
|
||||
{
|
||||
_logger.LogWarning("Permission denied: User {EmployeeId} attempted to access tenant details of other tenant.", loggedInEmployee.Id);
|
||||
return StatusCode(403,
|
||||
ApiResponse<object>.ErrorResponse("Access denied", "User does not have the required permissions for this action.", 403));
|
||||
}
|
||||
|
||||
|
||||
// Create a single DbContext for main tenant fetch and related data requests
|
||||
await using var _context = await _dbContextFactory.CreateDbContextAsync();
|
||||
@ -297,6 +292,13 @@ namespace Marco.Pms.Services.Controllers
|
||||
}
|
||||
_logger.LogInfo("Tenant {TenantId} found.", tenant.Id);
|
||||
|
||||
if (!hasManagePermission && (hasModifyPermission || hasViewPermission) && tenant.OrganizationId != loggedInEmployee.OrganizationId)
|
||||
{
|
||||
_logger.LogWarning("Permission denied: User {EmployeeId} attempted to access tenant details of other tenant.", loggedInEmployee.Id);
|
||||
return StatusCode(403,
|
||||
ApiResponse<object>.ErrorResponse("Access denied", "User does not have the required permissions for this action.", 403));
|
||||
}
|
||||
|
||||
// Fetch dependent data in parallel to improve performance
|
||||
var employeesTask = Task.Run(async () =>
|
||||
{
|
||||
@ -550,7 +552,7 @@ namespace Marco.Pms.Services.Controllers
|
||||
JobRole = adminJobRole, // Link to the newly created role
|
||||
CurrentAddress = model.BillingAddress,
|
||||
IsActive = true,
|
||||
IsSystem = false,
|
||||
IsSystem = true,
|
||||
IsPrimary = true,
|
||||
OrganizationId = organization.Id,
|
||||
HasApplicationAccess = true
|
||||
@ -566,43 +568,36 @@ namespace Marco.Pms.Services.Controllers
|
||||
};
|
||||
_context.ApplicationRoles.Add(applicationRole);
|
||||
|
||||
var rolePermissionMappigs = new List<RolePermissionMappings> {
|
||||
new RolePermissionMappings
|
||||
{
|
||||
ApplicationRoleId = applicationRole.Id,
|
||||
FeaturePermissionId = PermissionsMaster.ModifyTenant
|
||||
},
|
||||
new RolePermissionMappings
|
||||
{
|
||||
ApplicationRoleId = applicationRole.Id,
|
||||
FeaturePermissionId = PermissionsMaster.ViewTenant
|
||||
},
|
||||
new RolePermissionMappings
|
||||
{
|
||||
ApplicationRoleId = applicationRole.Id,
|
||||
FeaturePermissionId = PermissionsMaster.ManageMasters
|
||||
},
|
||||
new RolePermissionMappings
|
||||
{
|
||||
ApplicationRoleId = applicationRole.Id,
|
||||
FeaturePermissionId = PermissionsMaster.ViewMasters
|
||||
},
|
||||
new RolePermissionMappings
|
||||
{
|
||||
ApplicationRoleId = applicationRole.Id,
|
||||
FeaturePermissionId = PermissionsMaster.ViewOrganization
|
||||
},
|
||||
new RolePermissionMappings
|
||||
{
|
||||
ApplicationRoleId = applicationRole.Id,
|
||||
FeaturePermissionId = PermissionsMaster.AddOrganization
|
||||
},
|
||||
new RolePermissionMappings
|
||||
{
|
||||
ApplicationRoleId = applicationRole.Id,
|
||||
FeaturePermissionId = PermissionsMaster.EditOrganization
|
||||
}
|
||||
var featureIds = new List<Guid>
|
||||
{
|
||||
new Guid("a4e25142-449b-4334-a6e5-22f70e4732d7"), // Expense Management feature
|
||||
new Guid("81ab8a87-8ccd-4015-a917-0627cee6a100"), // Employee Management feature
|
||||
new Guid("52c9cf54-1eb2-44d2-81bb-524cf29c0a94"), // Attendance Management feature
|
||||
new Guid("a8cf4331-8f04-4961-8360-a3f7c3cc7462"), // Document Management feature
|
||||
new Guid("be3b3afc-6ccf-4566-b9b6-aafcb65546be"), // Masters Management feature
|
||||
new Guid("39e66f81-efc6-446c-95bd-46bff6cfb606"), // Directory Management feature
|
||||
new Guid("6d4c82d6-dbce-48ab-b8b8-f785f4d8c914") // Organization Management feature
|
||||
};
|
||||
|
||||
var permissionIds = await _context.FeaturePermissions.Where(fp => featureIds.Contains(fp.FeatureId)).Select(fp => fp.Id).ToListAsync();
|
||||
|
||||
var rolePermissionMappigs = permissionIds.Select(p => new RolePermissionMappings
|
||||
{
|
||||
ApplicationRoleId = applicationRole.Id,
|
||||
FeaturePermissionId = p
|
||||
}).ToList();
|
||||
|
||||
rolePermissionMappigs.Add(new RolePermissionMappings
|
||||
{
|
||||
ApplicationRoleId = applicationRole.Id,
|
||||
FeaturePermissionId = PermissionsMaster.ModifyTenant
|
||||
});
|
||||
rolePermissionMappigs.Add(new RolePermissionMappings
|
||||
{
|
||||
ApplicationRoleId = applicationRole.Id,
|
||||
FeaturePermissionId = PermissionsMaster.ViewTenant
|
||||
});
|
||||
|
||||
_context.RolePermissionMappings.AddRange(rolePermissionMappigs);
|
||||
|
||||
_context.EmployeeRoleMappings.Add(new EmployeeRoleMapping
|
||||
@ -651,6 +646,22 @@ namespace Marco.Pms.Services.Controllers
|
||||
_context.OrgServiceMappings.AddRange(serviceOrgMappings);
|
||||
}
|
||||
|
||||
var _masteData = scope.ServiceProvider.GetRequiredService<MasterDataService>();
|
||||
|
||||
var expensesTypeMaster = _masteData.GetExpensesTypeesData(tenant.Id);
|
||||
var paymentModeMatser = _masteData.GetPaymentModesData(tenant.Id);
|
||||
var documentCategoryMaster = _masteData.GetDocumentCategoryData(tenant.Id);
|
||||
|
||||
var employeeDocumentId = documentCategoryMaster.Where(dc => dc.Name == "Employee Documents").Select(dc => dc.Id).FirstOrDefault();
|
||||
var projectDocumentId = documentCategoryMaster.Where(dc => dc.Name == "Project Documents").Select(dc => dc.Id).FirstOrDefault();
|
||||
|
||||
var documentTypeMaster = _masteData.GetDocumentTypeData(tenant.Id, employeeDocumentId, projectDocumentId);
|
||||
|
||||
_context.ExpensesTypeMaster.AddRange(expensesTypeMaster);
|
||||
_context.PaymentModeMatser.AddRange(paymentModeMatser);
|
||||
_context.DocumentCategoryMasters.AddRange(documentCategoryMaster);
|
||||
_context.DocumentTypeMasters.AddRange(documentTypeMaster);
|
||||
|
||||
// All entities are now added to the context. Save them all in a single database operation.
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
@ -1013,6 +1024,10 @@ namespace Marco.Pms.Services.Controllers
|
||||
|
||||
try
|
||||
{
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
await ClearPermissionForTenant();
|
||||
});
|
||||
var features = await _featureDetailsHelper.GetFeatureDetails(subscriptionPlan.FeaturesId);
|
||||
if (features == null)
|
||||
{
|
||||
@ -1065,7 +1080,7 @@ namespace Marco.Pms.Services.Controllers
|
||||
// Get root employee and role for this tenant
|
||||
var rootEmployee = await _context.Employees
|
||||
.Include(e => e.ApplicationUser)
|
||||
.FirstOrDefaultAsync(e => e.ApplicationUser != null && (e.ApplicationUser.IsRootUser ?? false) && e.OrganizationId == tenant.OrganizationId);
|
||||
.FirstOrDefaultAsync(e => e.ApplicationUser != null && (e.ApplicationUser.IsRootUser ?? false) && e.TenantId == model.TenantId);
|
||||
|
||||
if (rootEmployee == null)
|
||||
{
|
||||
@ -1123,9 +1138,6 @@ namespace Marco.Pms.Services.Controllers
|
||||
_logger.LogInfo("Removed {Count} role permission mappings for role {RoleId}", deleteMappings.Count, roleId);
|
||||
}
|
||||
|
||||
var _cache = scope.ServiceProvider.GetRequiredService<CacheUpdateHelper>();
|
||||
await _cache.ClearAllEmployeesFromCacheByTenantId(tenant.Id);
|
||||
|
||||
var _masteData = scope.ServiceProvider.GetRequiredService<MasterDataService>();
|
||||
|
||||
if (features.Modules?.ProjectManagement?.Enabled ?? false)
|
||||
@ -1324,6 +1336,10 @@ namespace Marco.Pms.Services.Controllers
|
||||
_logger.LogInfo("Subscription plan changed: Tenant={TenantId}, NewPlan={PlanId}",
|
||||
model.TenantId, model.PlanId);
|
||||
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
await ClearPermissionForTenant();
|
||||
});
|
||||
|
||||
// 8. Update tenant permissions based on subscription features.
|
||||
var features = await _featureDetailsHelper.GetFeatureDetails(subscriptionPlan.FeaturesId);
|
||||
@ -1358,7 +1374,7 @@ namespace Marco.Pms.Services.Controllers
|
||||
// 8c. Find root employee & role for this tenant.
|
||||
var rootEmployee = await context.Employees
|
||||
.Include(e => e.ApplicationUser)
|
||||
.FirstOrDefaultAsync(e => e.ApplicationUser != null && (e.ApplicationUser.IsRootUser ?? false) && e.OrganizationId == tenant.OrganizationId);
|
||||
.FirstOrDefaultAsync(e => e.ApplicationUser != null && (e.ApplicationUser.IsRootUser ?? false) && e.TenantId == model.TenantId);
|
||||
|
||||
if (rootEmployee == null)
|
||||
{
|
||||
@ -1369,8 +1385,7 @@ namespace Marco.Pms.Services.Controllers
|
||||
|
||||
var rootRoleId = await context.EmployeeRoleMappings
|
||||
.AsNoTracking()
|
||||
.Include(er => er.Role)
|
||||
.Where(er => er.EmployeeId == rootEmployee.Id && er.TenantId == model.TenantId && er.Role != null && er.Role.Role == "Super User")
|
||||
.Where(er => er.EmployeeId == rootEmployee.Id && er.TenantId == model.TenantId)
|
||||
.Select(er => er.RoleId)
|
||||
.FirstOrDefaultAsync();
|
||||
|
||||
@ -1435,9 +1450,6 @@ namespace Marco.Pms.Services.Controllers
|
||||
_logger.LogInfo("Permissions revoked: {Count} for Role={RoleId}", mappingsToRemove.Count, rootRoleId);
|
||||
}
|
||||
|
||||
var _cache = scope.ServiceProvider.GetRequiredService<CacheUpdateHelper>();
|
||||
await _cache.ClearAllEmployeesFromCacheByTenantId(tenant.Id);
|
||||
|
||||
var _masteData = scope.ServiceProvider.GetRequiredService<MasterDataService>();
|
||||
|
||||
if (features.Modules?.ProjectManagement?.Enabled ?? false)
|
||||
@ -1822,6 +1834,19 @@ namespace Marco.Pms.Services.Controllers
|
||||
return ApiResponse<SubscriptionPlanVM>.SuccessResponse(VM, "Success", 200);
|
||||
}
|
||||
|
||||
private async Task ClearPermissionForTenant()
|
||||
{
|
||||
await using var _context = await _dbContextFactory.CreateDbContextAsync();
|
||||
using var scope = _serviceScopeFactory.CreateScope();
|
||||
|
||||
var _cache = scope.ServiceProvider.GetRequiredService<CacheUpdateHelper>();
|
||||
var _cacheLogger = scope.ServiceProvider.GetRequiredService<ILoggingService>();
|
||||
|
||||
var employeeIds = await _context.Employees.Where(e => e.TenantId == tenantId).Select(e => e.Id).ToListAsync();
|
||||
await _cache.ClearAllEmployeesFromCacheByEmployeeIds(employeeIds, tenantId);
|
||||
_cacheLogger.LogInfo("{EmployeeCount} number of employee deleted", employeeIds.Count);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
@ -956,28 +956,6 @@ namespace Marco.Pms.Services.Helpers
|
||||
_logger.LogError(ex, "Error occured while deleting all employees from Cache");
|
||||
}
|
||||
}
|
||||
public async Task ClearAllEmployeesFromCacheByOnlyEmployeeId(Guid employeeId)
|
||||
{
|
||||
try
|
||||
{
|
||||
var response = await _employeeCache.ClearAllEmployeesFromCacheByOnlyEmployeeId(employeeId);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error occured while deleting all employees from Cache");
|
||||
}
|
||||
}
|
||||
public async Task ClearAllEmployeesFromCacheByTenantId(Guid tenantId)
|
||||
{
|
||||
try
|
||||
{
|
||||
var response = await _employeeCache.ClearAllEmployeesFromCacheByTenantId(tenantId);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error occured while deleting all employees from Cache");
|
||||
}
|
||||
}
|
||||
public async Task ClearAllEmployees()
|
||||
{
|
||||
try
|
||||
|
@ -253,16 +253,29 @@ namespace Marco.Pms.Services.Helpers
|
||||
!ts.IsCancelled &&
|
||||
ts.EndDate.Date >= DateTime.UtcNow.Date); // FIX: Subscription should not be expired
|
||||
|
||||
var featureIds = new List<Guid>
|
||||
{
|
||||
new Guid("a4e25142-449b-4334-a6e5-22f70e4732d7"), // Expense Management feature
|
||||
new Guid("81ab8a87-8ccd-4015-a917-0627cee6a100"), // Employee Management feature
|
||||
new Guid("52c9cf54-1eb2-44d2-81bb-524cf29c0a94"), // Attendance Management feature
|
||||
new Guid("a8cf4331-8f04-4961-8360-a3f7c3cc7462"), // Document Management feature
|
||||
new Guid("be3b3afc-6ccf-4566-b9b6-aafcb65546be"), // Masters Management feature
|
||||
new Guid("39e66f81-efc6-446c-95bd-46bff6cfb606"), // Directory Management feature
|
||||
new Guid("6d4c82d6-dbce-48ab-b8b8-f785f4d8c914") // Organization Management feature
|
||||
//new Guid("2f3509b7-160d-410a-b9b6-daadd96c986d") // Tenant Management feature
|
||||
};
|
||||
|
||||
if (tenantSubscription == null)
|
||||
{
|
||||
_logger.LogWarning("No active subscription found for tenant: {TenantId}", tenantId);
|
||||
return new List<Guid>();
|
||||
return featureIds;
|
||||
}
|
||||
|
||||
_logger.LogDebug("Active subscription found for tenant: {TenantId}, PlanId: {PlanId}",
|
||||
tenantId, tenantSubscription.Plan!.Id);
|
||||
|
||||
var featureIds = new List<Guid> { new Guid("2f3509b7-160d-410a-b9b6-daadd96c986d"), new Guid("be3b3afc-6ccf-4566-b9b6-aafcb65546be") };
|
||||
//var featureIds = new List<Guid> { new Guid("be3b3afc-6ccf-4566-b9b6-aafcb65546be") };
|
||||
|
||||
|
||||
// Step 2: Get feature details from Plan
|
||||
var featureDetails = await _featureDetailsHelper.GetFeatureDetails(tenantSubscription.Plan!.FeaturesId);
|
||||
|
@ -190,7 +190,6 @@ namespace Marco.Pms.Services.Helpers
|
||||
double totalCompletedWork = workItems.Sum(w => w.CompletedWork);
|
||||
|
||||
var todayAssignedTasks = tasks.Where(t => t.AssignmentDate.Date == reportDate).ToList();
|
||||
var todaysCompletedTasks = tasks.Where(t => t.AssignmentDate.Date == reportDate && t.ReportedById != null).ToList();
|
||||
var reportPending = tasks.Where(t => t.ReportedDate == null).ToList();
|
||||
|
||||
double totalPlannedTask = todayAssignedTasks.Sum(t => t.PlannedTask);
|
||||
@ -264,17 +263,14 @@ namespace Marco.Pms.Services.Helpers
|
||||
// Fill report
|
||||
statisticReport.TodaysAttendances = checkedInEmployeeIds.Count;
|
||||
statisticReport.TotalEmployees = assignedEmployeeIds.Count;
|
||||
statisticReport.AttendancePercentage = assignedEmployeeIds.Count > 0 ? (checkedInEmployeeIds.Count / assignedEmployeeIds.Count) * 100 : 0;
|
||||
statisticReport.RegularizationPending = regularizationIds.Count;
|
||||
statisticReport.CheckoutPending = checkoutPendingIds.Count;
|
||||
statisticReport.TotalPlannedWork = totalPlannedWork;
|
||||
statisticReport.TotalCompletedWork = totalCompletedWork;
|
||||
statisticReport.CompletionStatus = totalPlannedWork > 0 ? (totalCompletedWork / totalPlannedWork) * 100 : 0;
|
||||
statisticReport.TotalPlannedTask = totalPlannedTask;
|
||||
statisticReport.TotalCompletedTask = totalCompletedTask;
|
||||
statisticReport.AttendancePercentage = totalCompletedTask > 0 ? (totalCompletedTask / totalPlannedTask) * 100 : 0;
|
||||
statisticReport.CompletionStatus = totalPlannedWork > 0 ? totalCompletedWork / totalPlannedWork : 0;
|
||||
statisticReport.TodaysAssignTasks = todayAssignedTasks.Count;
|
||||
statisticReport.TodaysCompletedTasks = todaysCompletedTasks.Count;
|
||||
statisticReport.ReportPending = reportPending.Count;
|
||||
statisticReport.TeamOnSite = teamOnSite;
|
||||
statisticReport.PerformedTasks = performedTasks;
|
||||
|
@ -3,6 +3,7 @@ using Microsoft.AspNetCore.SignalR;
|
||||
|
||||
namespace Marco.Pms.Services.Hubs
|
||||
{
|
||||
//[Authorize]
|
||||
public class MarcoHub : Hub
|
||||
{
|
||||
private readonly ILoggingService _logger;
|
||||
|
@ -248,6 +248,7 @@ namespace Marco.Pms.Services.MappingProfiles
|
||||
dest => dest.Id,
|
||||
opt => opt.MapFrom(src => Guid.Parse(src.Id)));
|
||||
|
||||
CreateMap<Expenses, ExpenseDetailsVM>();
|
||||
CreateMap<ExpenseDetailsMongoDB, ExpenseDetailsVM>()
|
||||
.ForMember(
|
||||
dest => dest.Id,
|
||||
@ -379,27 +380,6 @@ namespace Marco.Pms.Services.MappingProfiles
|
||||
|
||||
#endregion
|
||||
|
||||
#region ======================================================= Contact Category Master =======================================================
|
||||
CreateMap<CreateContactCategoryDto, ContactCategoryMaster>();
|
||||
CreateMap<UpdateContactCategoryDto, ContactCategoryMaster>();
|
||||
CreateMap<ContactCategoryMaster, ContactCategoryVM>();
|
||||
#endregion
|
||||
#region ======================================================= Contact Tag Master =======================================================
|
||||
CreateMap<CreateContactTagDto, ContactTagMaster>();
|
||||
CreateMap<UpdateContactTagDto, ContactTagMaster>();
|
||||
CreateMap<ContactTagMaster, ContactTagVM>();
|
||||
#endregion
|
||||
#region ======================================================= Expenses Status Master =======================================================
|
||||
#endregion
|
||||
#region ======================================================= Expenses Status Master =======================================================
|
||||
#endregion
|
||||
#region ======================================================= Expenses Status Master =======================================================
|
||||
#endregion
|
||||
#region ======================================================= Expenses Status Master =======================================================
|
||||
#endregion
|
||||
#region ======================================================= Expenses Status Master =======================================================
|
||||
#endregion
|
||||
|
||||
#endregion
|
||||
|
||||
#region ======================================================= Document =======================================================
|
||||
|
@ -1533,9 +1533,11 @@ namespace Marco.Pms.Services.Service
|
||||
return ApiResponse<object>.ErrorResponse("Contact ID is empty", "Contact ID is empty", 400);
|
||||
}
|
||||
|
||||
var (hasAdminPermission, hasManagerPermission, hasUserPermission) = await CheckPermissionsAsync(loggedInEmployee.Id);
|
||||
|
||||
var bucketIds = await _context.ContactBucketMappings.Where(cb => cb.ContactId == id).Select(cb => cb.BucketId).ToListAsync();
|
||||
var hasContactAccess = await _context.EmployeeBucketMappings.AnyAsync(eb => bucketIds.Contains(eb.BucketId) && eb.EmployeeId == loggedInEmployee.Id);
|
||||
if (hasContactAccess)
|
||||
if (!hasAdminPermission && !hasContactAccess)
|
||||
{
|
||||
_logger.LogWarning("Employee {EmployeeId} does not have permission to delete contact {ContactId}",
|
||||
loggedInEmployee.Id, id);
|
||||
@ -2131,7 +2133,7 @@ namespace Marco.Pms.Services.Service
|
||||
|
||||
var bucketIds = await _context.ContactBucketMappings.Where(cb => cb.ContactId == noteDto.ContactId).Select(cb => cb.BucketId).ToListAsync();
|
||||
var hasContactAccess = await _context.EmployeeBucketMappings.AnyAsync(eb => bucketIds.Contains(eb.BucketId) && eb.EmployeeId == loggedInEmployee.Id);
|
||||
if (!hasAdminPermission && hasContactAccess)
|
||||
if (!hasAdminPermission && !hasContactAccess)
|
||||
{
|
||||
_logger.LogWarning("Employee {EmployeeId} does not have permission to delete contact {ContactId}",
|
||||
loggedInEmployee.Id, noteDto.ContactId);
|
||||
@ -2270,11 +2272,10 @@ namespace Marco.Pms.Services.Service
|
||||
|
||||
return ApiResponse<object>.ErrorResponse("Note not found", "Note not found", 404);
|
||||
}
|
||||
|
||||
var (hasAdminPermission, hasManagerPermission, hasUserPermission) = await CheckPermissionsAsync(loggedInEmployee.Id);
|
||||
|
||||
var bucketIds = await _context.ContactBucketMappings.AsNoTracking().Where(cb => cb.ContactId == note.ContactId).Select(cb => cb.BucketId).ToListAsync();
|
||||
var hasContactAccess = await _context.EmployeeBucketMappings.AsNoTracking().AnyAsync(eb => bucketIds.Contains(eb.BucketId) && eb.EmployeeId == loggedInEmployee.Id);
|
||||
var bucketIds = await _context.ContactBucketMappings.Where(cb => cb.ContactId == note.ContactId).Select(cb => cb.BucketId).ToListAsync();
|
||||
var hasContactAccess = await _context.EmployeeBucketMappings.AnyAsync(eb => bucketIds.Contains(eb.BucketId) && eb.EmployeeId == loggedInEmployee.Id);
|
||||
if (!hasAdminPermission && !hasContactAccess)
|
||||
{
|
||||
_logger.LogWarning("Employee {EmployeeId} does not have permission to delete contact {ContactId}",
|
||||
|
@ -130,6 +130,16 @@ namespace Marco.Pms.Services.Service
|
||||
// 3. --- Build Base Query and Apply Permissions ---
|
||||
// Start with a base IQueryable. Filters will be chained onto this.
|
||||
var expensesQuery = _context.Expenses
|
||||
.Include(e => e.PaidBy)
|
||||
.Include(e => e.CreatedBy)
|
||||
.Include(e => e.ProcessedBy)
|
||||
.Include(e => e.ApprovedBy)
|
||||
.Include(e => e.ReviewedBy)
|
||||
.Include(e => e.PaymentMode)
|
||||
.Include(e => e.Project)
|
||||
.Include(e => e.PaymentMode)
|
||||
.Include(e => e.ExpensesType)
|
||||
.Include(e => e.Status)
|
||||
.Where(e => e.TenantId == tenantId); // Always filter by TenantId first.
|
||||
|
||||
if (cacheList == null)
|
||||
@ -177,6 +187,10 @@ namespace Marco.Pms.Services.Service
|
||||
{
|
||||
expensesQuery = expensesQuery.Where(e => expenseFilter.PaidById.Contains(e.PaidById));
|
||||
}
|
||||
if (expenseFilter.ExpenseTypeIds?.Any() == true)
|
||||
{
|
||||
expensesQuery = expensesQuery.Where(e => expenseFilter.ExpenseTypeIds.Contains(e.ExpensesTypeId));
|
||||
}
|
||||
|
||||
// Only allow filtering by 'CreatedBy' if the user has 'View All' permission.
|
||||
if (expenseFilter.CreatedByIds?.Any() == true && hasViewAllPermissionTask.Result)
|
||||
@ -214,7 +228,8 @@ namespace Marco.Pms.Services.Service
|
||||
return ApiResponse<object>.SuccessResponse(new List<ExpenseList>(), "No expenses found for the given criteria.", 200);
|
||||
}
|
||||
|
||||
expenseVM = await GetAllExpnesRelatedTables(expensesList, tenantId);
|
||||
//expenseVM = await GetAllExpnesRelatedTables(expensesList, tenantId);
|
||||
expenseVM = _mapper.Map<List<ExpenseList>>(expensesList);
|
||||
totalPages = (int)Math.Ceiling((double)totalEntites / pageSize);
|
||||
|
||||
}
|
||||
@ -277,7 +292,18 @@ namespace Marco.Pms.Services.Service
|
||||
ExpenseDetailsMongoDB? expenseDetails = null;
|
||||
if (expenseDetails == null)
|
||||
{
|
||||
var expense = await _context.Expenses.AsNoTracking().FirstOrDefaultAsync(e => e.Id == id && e.TenantId == tenantId);
|
||||
var expense = await _context.Expenses
|
||||
.Include(e => e.PaidBy)
|
||||
.Include(e => e.CreatedBy)
|
||||
.Include(e => e.ProcessedBy)
|
||||
.Include(e => e.ApprovedBy)
|
||||
.Include(e => e.ReviewedBy)
|
||||
.Include(e => e.PaymentMode)
|
||||
.Include(e => e.Project)
|
||||
.Include(e => e.PaymentMode)
|
||||
.Include(e => e.ExpensesType)
|
||||
.Include(e => e.Status)
|
||||
.AsNoTracking().FirstOrDefaultAsync(e => e.Id == id && e.TenantId == tenantId);
|
||||
|
||||
if (expense == null)
|
||||
{
|
||||
@ -369,66 +395,25 @@ namespace Marco.Pms.Services.Service
|
||||
{
|
||||
try
|
||||
{
|
||||
// Task 1: Get all distinct projects associated with the tenant's expenses.
|
||||
var projectsTask = Task.Run(async () =>
|
||||
{
|
||||
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
|
||||
return await dbContext.Expenses
|
||||
.Where(e => e.TenantId == tenantId && e.Project != null)
|
||||
.Select(e => e.Project!)
|
||||
.Distinct()
|
||||
.Select(p => new { p.Id, Name = $"{p.Name}" })
|
||||
.ToListAsync();
|
||||
});
|
||||
|
||||
// Task 2: Get all distinct users who paid for the tenant's expenses.
|
||||
var paidByTask = Task.Run(async () =>
|
||||
{
|
||||
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
|
||||
return await dbContext.Expenses
|
||||
.Where(e => e.TenantId == tenantId && e.PaidBy != null)
|
||||
.Select(e => e.PaidBy!)
|
||||
.Distinct()
|
||||
.Select(u => new { u.Id, Name = $"{u.FirstName} {u.LastName}" })
|
||||
.ToListAsync();
|
||||
});
|
||||
|
||||
// Task 3: Get all distinct users who created the tenant's expenses.
|
||||
var createdByTask = Task.Run(async () =>
|
||||
{
|
||||
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
|
||||
return await dbContext.Expenses
|
||||
.Where(e => e.TenantId == tenantId && e.CreatedBy != null)
|
||||
.Select(e => e.CreatedBy!)
|
||||
.Distinct()
|
||||
.Select(u => new { u.Id, Name = $"{u.FirstName} {u.LastName}" })
|
||||
.ToListAsync();
|
||||
});
|
||||
|
||||
// Task 4: Get all distinct statuses associated with the tenant's expenses.
|
||||
var statusTask = Task.Run(async () =>
|
||||
{
|
||||
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
|
||||
return await dbContext.Expenses
|
||||
.Where(e => e.TenantId == tenantId && e.Status != null)
|
||||
.Select(e => e.Status!)
|
||||
.Distinct()
|
||||
.Select(s => new { s.Id, s.Name })
|
||||
.ToListAsync();
|
||||
});
|
||||
|
||||
// Execute all four queries concurrently. The total wait time will be determined
|
||||
// by the longest-running query, not the sum of all four.
|
||||
await Task.WhenAll(projectsTask, paidByTask, createdByTask, statusTask);
|
||||
var expenses = await _context.Expenses
|
||||
.Include(e => e.PaidBy)
|
||||
.Include(e => e.Project)
|
||||
.Include(e => e.CreatedBy)
|
||||
.Include(e => e.Status)
|
||||
.Include(e => e.ExpensesType)
|
||||
.Where(e => e.TenantId == tenantId)
|
||||
.ToListAsync();
|
||||
|
||||
// Construct the final object from the results of the completed tasks.
|
||||
return ApiResponse<object>.SuccessResponse(new
|
||||
var response = new
|
||||
{
|
||||
Projects = await projectsTask,
|
||||
PaidBy = await paidByTask,
|
||||
CreatedBy = await createdByTask,
|
||||
Status = await statusTask
|
||||
}, "Successfully fetched the filter list", 200);
|
||||
Projects = expenses.Where(e => e.Project != null).Select(e => new { Id = e.Project!.Id, Name = e.Project.Name }).Distinct().ToList(),
|
||||
PaidBy = expenses.Where(e => e.PaidBy != null).Select(e => new { Id = e.PaidBy!.Id, Name = $"{e.PaidBy.FirstName} {e.PaidBy.LastName}" }).Distinct().ToList(),
|
||||
CreatedBy = expenses.Where(e => e.CreatedBy != null).Select(e => new { Id = e.CreatedBy!.Id, Name = $"{e.CreatedBy.FirstName} {e.CreatedBy.LastName}" }).Distinct().ToList(),
|
||||
Status = expenses.Where(e => e.Status != null).Select(e => new { Id = e.Status!.Id, Name = e.Status.Name }).Distinct().ToList(),
|
||||
ExpensesType = expenses.Where(e => e.ExpensesType != null).Select(e => new { Id = e.ExpensesType!.Id, Name = e.ExpensesType.Name }).Distinct().ToList()
|
||||
};
|
||||
return ApiResponse<object>.SuccessResponse(response, "Successfully fetched the filter list", 200);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@ -466,13 +451,6 @@ namespace Marco.Pms.Services.Service
|
||||
return await permissionService.HasPermission(PermissionsMaster.ExpenseUpload, loggedInEmployee.Id);
|
||||
});
|
||||
|
||||
var hasProjectPermissionTask = Task.Run(async () =>
|
||||
{
|
||||
using var scope = _serviceScopeFactory.CreateScope();
|
||||
var permissionService = scope.ServiceProvider.GetRequiredService<PermissionServices>();
|
||||
return await permissionService.HasProjectPermission(loggedInEmployee, dto.ProjectId);
|
||||
});
|
||||
|
||||
// VALIDATION CHECKS: Use IDbContextFactory for thread-safe, parallel database queries.
|
||||
// Each task gets its own DbContext instance.
|
||||
var projectTask = Task.Run(async () =>
|
||||
@ -523,21 +501,21 @@ namespace Marco.Pms.Services.Service
|
||||
|
||||
|
||||
// Await all prerequisite checks at once.
|
||||
await Task.WhenAll(hasUploadPermissionTask, hasProjectPermissionTask, projectTask, expenseTypeTask, paymentModeTask, statusMappingTask, paidByTask, expenseUIdTask);
|
||||
await Task.WhenAll(hasUploadPermissionTask, projectTask, expenseTypeTask, paymentModeTask, statusMappingTask, paidByTask, expenseUIdTask);
|
||||
|
||||
// 2. Aggregate and Check Results
|
||||
if (!await hasUploadPermissionTask || !await hasProjectPermissionTask)
|
||||
if (!await hasUploadPermissionTask)
|
||||
{
|
||||
_logger.LogWarning("Access DENIED for employee {EmployeeId} on project {ProjectId}.", loggedInEmployee.Id, dto.ProjectId);
|
||||
return ApiResponse<object>.ErrorResponse("Access Denied.", "You do not have permission to upload expenses for this project.", 403);
|
||||
}
|
||||
|
||||
var validationErrors = new List<string>();
|
||||
var project = projectTask.Result;
|
||||
var expenseType = expenseTypeTask.Result;
|
||||
var paymentMode = paymentModeTask.Result;
|
||||
var statusMapping = statusMappingTask.Result;
|
||||
var paidBy = paidByTask.Result;
|
||||
var project = await projectTask;
|
||||
var expenseType = await expenseTypeTask;
|
||||
var paymentMode = await paymentModeTask;
|
||||
var statusMapping = await statusMappingTask;
|
||||
var paidBy = await paidByTask;
|
||||
var lastExpenseUId = expenseUIdTask.Result;
|
||||
|
||||
if (project == null) validationErrors.Add("Project not found.");
|
||||
@ -554,7 +532,9 @@ namespace Marco.Pms.Services.Service
|
||||
_logger.LogWarning("Expense creation failed due to validation errors: {ValidationErrors}", errorMessage);
|
||||
return ApiResponse<object>.ErrorResponse("Invalid input data.", errorMessage, 400);
|
||||
}
|
||||
|
||||
var currentexpenseUId = (lastExpenseUId + 1).ToString("D5");
|
||||
|
||||
// 3. Entity Creation
|
||||
var expense = _mapper.Map<Expenses>(dto);
|
||||
expense.ExpenseUId = $"EX-{currentexpenseUId}";
|
||||
@ -1107,6 +1087,7 @@ namespace Marco.Pms.Services.Service
|
||||
var m = Regex.Match(id ?? string.Empty, @"(\d+)$");
|
||||
return m.Success ? int.Parse(m.Value) : int.MinValue; // put invalid IDs at the bottom
|
||||
}
|
||||
|
||||
private static object ExceptionMapper(Exception ex)
|
||||
{
|
||||
return new
|
||||
@ -1240,46 +1221,6 @@ namespace Marco.Pms.Services.Service
|
||||
|
||||
private async Task<ExpenseDetailsMongoDB> GetAllExpnesRelatedTablesForSingle(Expenses model, Guid tenantId)
|
||||
{
|
||||
var projectTask = Task.Run(async () =>
|
||||
{
|
||||
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
|
||||
return await dbContext.Projects.AsNoTracking().FirstOrDefaultAsync(p => p.Id == model.ProjectId && p.TenantId == tenantId);
|
||||
});
|
||||
var paidByTask = Task.Run(async () =>
|
||||
{
|
||||
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
|
||||
return await dbContext.Employees.Include(e => e.JobRole).AsNoTracking().FirstOrDefaultAsync(e => e.Id == model.PaidById && e.TenantId == tenantId);
|
||||
});
|
||||
var createdByTask = Task.Run(async () =>
|
||||
{
|
||||
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
|
||||
return await dbContext.Employees.Include(e => e.JobRole).AsNoTracking().FirstOrDefaultAsync(e => e.Id == model.CreatedById && e.TenantId == tenantId);
|
||||
});
|
||||
var reviewedByTask = Task.Run(async () =>
|
||||
{
|
||||
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
|
||||
return await dbContext.Employees.Include(e => e.JobRole).AsNoTracking().FirstOrDefaultAsync(e => e.Id == model.ReviewedById && e.TenantId == tenantId);
|
||||
});
|
||||
var approvedByTask = Task.Run(async () =>
|
||||
{
|
||||
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
|
||||
return await dbContext.Employees.Include(e => e.JobRole).AsNoTracking().FirstOrDefaultAsync(e => e.Id == model.ApprovedById && e.TenantId == tenantId);
|
||||
});
|
||||
var processedByTask = Task.Run(async () =>
|
||||
{
|
||||
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
|
||||
return await dbContext.Employees.Include(e => e.JobRole).AsNoTracking().FirstOrDefaultAsync(e => e.Id == model.ProcessedById && e.TenantId == tenantId);
|
||||
});
|
||||
var expenseTypeTask = Task.Run(async () =>
|
||||
{
|
||||
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
|
||||
return await dbContext.ExpensesTypeMaster.AsNoTracking().FirstOrDefaultAsync(et => et.Id == model.ExpensesTypeId && et.TenantId == tenantId);
|
||||
});
|
||||
var paymentModeTask = Task.Run(async () =>
|
||||
{
|
||||
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
|
||||
return await dbContext.PaymentModeMatser.AsNoTracking().FirstOrDefaultAsync(pm => pm.Id == model.PaymentModeId && pm.TenantId == tenantId);
|
||||
});
|
||||
var statusMappingTask = Task.Run(async () =>
|
||||
{
|
||||
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
|
||||
@ -1327,29 +1268,20 @@ namespace Marco.Pms.Services.Service
|
||||
});
|
||||
|
||||
// Await all prerequisite checks at once.
|
||||
await Task.WhenAll(projectTask, expenseTypeTask, paymentModeTask, statusMappingTask, paidByTask, createdByTask, reviewedByTask, approvedByTask,
|
||||
processedByTask, statusTask, billAttachmentsTask);
|
||||
await Task.WhenAll(statusTask, billAttachmentsTask);
|
||||
|
||||
var project = projectTask.Result;
|
||||
var expenseType = expenseTypeTask.Result;
|
||||
var paymentMode = paymentModeTask.Result;
|
||||
var statusMapping = statusMappingTask.Result;
|
||||
var paidBy = paidByTask.Result;
|
||||
var createdBy = createdByTask.Result;
|
||||
var reviewedBy = reviewedByTask.Result;
|
||||
var approvedBy = approvedByTask.Result;
|
||||
var processedBy = processedByTask.Result;
|
||||
var billAttachment = billAttachmentsTask.Result;
|
||||
|
||||
|
||||
var response = _mapper.Map<ExpenseDetailsMongoDB>(model);
|
||||
|
||||
response.Project = _mapper.Map<ProjectBasicMongoDB>(project);
|
||||
response.PaidBy = _mapper.Map<BasicEmployeeMongoDB>(paidBy);
|
||||
response.CreatedBy = _mapper.Map<BasicEmployeeMongoDB>(createdBy);
|
||||
response.ReviewedBy = _mapper.Map<BasicEmployeeMongoDB>(reviewedBy);
|
||||
response.ApprovedBy = _mapper.Map<BasicEmployeeMongoDB>(approvedBy);
|
||||
response.ProcessedBy = _mapper.Map<BasicEmployeeMongoDB>(processedBy);
|
||||
response.Project = _mapper.Map<ProjectBasicMongoDB>(model.Project);
|
||||
response.PaidBy = _mapper.Map<BasicEmployeeMongoDB>(model.PaidBy);
|
||||
response.CreatedBy = _mapper.Map<BasicEmployeeMongoDB>(model.CreatedBy);
|
||||
response.ReviewedBy = _mapper.Map<BasicEmployeeMongoDB>(model.ReviewedBy);
|
||||
response.ApprovedBy = _mapper.Map<BasicEmployeeMongoDB>(model.ApprovedBy);
|
||||
response.ProcessedBy = _mapper.Map<BasicEmployeeMongoDB>(model.ProcessedBy);
|
||||
if (statusMapping != null)
|
||||
{
|
||||
response.Status = _mapper.Map<ExpensesStatusMasterMongoDB>(statusMapping.Status);
|
||||
@ -1360,8 +1292,8 @@ namespace Marco.Pms.Services.Service
|
||||
var status = statusTask.Result;
|
||||
response.Status = _mapper.Map<ExpensesStatusMasterMongoDB>(status);
|
||||
}
|
||||
response.PaymentMode = _mapper.Map<PaymentModeMatserMongoDB>(paymentMode);
|
||||
response.ExpensesType = _mapper.Map<ExpensesTypeMasterMongoDB>(expenseType);
|
||||
response.PaymentMode = _mapper.Map<PaymentModeMatserMongoDB>(model.PaymentMode);
|
||||
response.ExpensesType = _mapper.Map<ExpensesTypeMasterMongoDB>(model.ExpensesType);
|
||||
if (billAttachment != null) response.Documents = billAttachment.Documents;
|
||||
|
||||
return response;
|
||||
|
@ -1,4 +1,5 @@
|
||||
using Marco.Pms.Model.Forum;
|
||||
using Marco.Pms.Model.DocumentManager;
|
||||
using Marco.Pms.Model.Forum;
|
||||
using Marco.Pms.Model.Master;
|
||||
|
||||
namespace Marco.Pms.Services.Service
|
||||
@ -335,6 +336,194 @@ namespace Marco.Pms.Services.Service
|
||||
}
|
||||
};
|
||||
}
|
||||
public List<DocumentCategoryMaster> GetDocumentCategoryData(Guid tenantId)
|
||||
{
|
||||
return new List<DocumentCategoryMaster> {
|
||||
new DocumentCategoryMaster
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Name = "Project Documents",
|
||||
Description = "Project documents are formal records that outline the plans, progress, and details necessary to execute and manage a project effectively.",
|
||||
EntityTypeId = Guid.Parse("c8fe7115-aa27-43bc-99f4-7b05fabe436e"),
|
||||
CreatedAt = new DateTime(2025, 9, 15, 12, 42, 3, 202, DateTimeKind.Utc),
|
||||
TenantId = tenantId
|
||||
},
|
||||
new DocumentCategoryMaster
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Name = "Employee Documents",
|
||||
Description = "Employment details along with legal IDs like passports or driver’s licenses to verify identity and work authorization.",
|
||||
EntityTypeId = Guid.Parse("dbb9555a-7a0c-40f2-a9ed-f0463f1ceed7"),
|
||||
CreatedAt = new DateTime(2025, 9, 15, 12, 42, 3, 202, DateTimeKind.Utc),
|
||||
TenantId = tenantId
|
||||
}
|
||||
};
|
||||
}
|
||||
public List<DocumentTypeMaster> GetDocumentTypeData(Guid tenantId, Guid employeeDocumentId, Guid projectDocumentId)
|
||||
{
|
||||
return new List<DocumentTypeMaster> {
|
||||
new DocumentTypeMaster
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Name = "Aadhaar card",
|
||||
RegexExpression = "^[2-9][0-9]{11}$",
|
||||
AllowedContentType = "application/pdf,image/jpeg",
|
||||
MaxSizeAllowedInMB = 2,
|
||||
IsValidationRequired = true,
|
||||
IsMandatory = true,
|
||||
CreatedAt = new DateTime(2025, 9, 3, 10, 46, 49, 955, DateTimeKind.Utc),
|
||||
IsSystem = true,
|
||||
IsActive = true,
|
||||
DocumentCategoryId = employeeDocumentId,
|
||||
TenantId = tenantId
|
||||
},
|
||||
new DocumentTypeMaster
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Name = "Pan Card",
|
||||
RegexExpression = "^[A-Z]{5}[0-9]{4}[A-Z]{1}$",
|
||||
AllowedContentType = "application/pdf,image/jpeg",
|
||||
MaxSizeAllowedInMB = 2,
|
||||
IsValidationRequired = true,
|
||||
IsMandatory = true,
|
||||
CreatedAt = new DateTime(2025, 9, 3, 10, 46, 49, 955, DateTimeKind.Utc),
|
||||
IsSystem = true,
|
||||
IsActive = true,
|
||||
DocumentCategoryId = employeeDocumentId,
|
||||
TenantId = tenantId
|
||||
},
|
||||
new DocumentTypeMaster
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Name = "Voter Card",
|
||||
RegexExpression = "^[A-Z]{3}[0-9]{7}$",
|
||||
AllowedContentType = "application/pdf,image/jpeg",
|
||||
MaxSizeAllowedInMB = 2,
|
||||
IsValidationRequired = true,
|
||||
IsMandatory = true,
|
||||
CreatedAt = new DateTime(2025, 9, 3, 10, 46, 49, 955, DateTimeKind.Utc),
|
||||
IsSystem = true,
|
||||
IsActive = true,
|
||||
DocumentCategoryId = employeeDocumentId,
|
||||
TenantId = tenantId
|
||||
},
|
||||
new DocumentTypeMaster
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Name = "Passport",
|
||||
RegexExpression = "^[A-PR-WY][1-9]\\d\\s?\\d{4}[1-9]$",
|
||||
AllowedContentType = "application/pdf,image/jpeg",
|
||||
MaxSizeAllowedInMB = 2,
|
||||
IsValidationRequired = true,
|
||||
IsMandatory = true,
|
||||
CreatedAt = new DateTime(2025, 9, 3, 10, 46, 49, 955, DateTimeKind.Utc),
|
||||
IsSystem = true,
|
||||
IsActive = true,
|
||||
DocumentCategoryId = employeeDocumentId,
|
||||
TenantId = tenantId
|
||||
},
|
||||
new DocumentTypeMaster
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Name = "Bank Passbook",
|
||||
RegexExpression = "^\\d{9,18}$",
|
||||
AllowedContentType = "application/pdf,image/jpeg",
|
||||
MaxSizeAllowedInMB = 2,
|
||||
IsValidationRequired = true,
|
||||
IsMandatory = true,
|
||||
CreatedAt = new DateTime(2025, 9, 3, 10, 46, 49, 955, DateTimeKind.Utc),
|
||||
IsSystem = true,
|
||||
IsActive = true,
|
||||
DocumentCategoryId = employeeDocumentId,
|
||||
TenantId = tenantId
|
||||
},
|
||||
|
||||
new DocumentTypeMaster
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Name = "Bill of Quantities (BOQ)",
|
||||
AllowedContentType = "application/pdf,application/msword,application/vnd.ms-excel,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet,application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
||||
MaxSizeAllowedInMB = 1,
|
||||
IsValidationRequired = false,
|
||||
IsMandatory = false,
|
||||
CreatedAt = new DateTime(2025, 9, 3, 10, 46, 49, 955, DateTimeKind.Utc),
|
||||
IsSystem = true,
|
||||
IsActive = true,
|
||||
DocumentCategoryId = projectDocumentId,
|
||||
TenantId = tenantId
|
||||
},
|
||||
new DocumentTypeMaster
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Name = "Work Order",
|
||||
AllowedContentType = "application/pdf,application/msword,application/vnd.ms-excel,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet,application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
||||
MaxSizeAllowedInMB = 1,
|
||||
IsValidationRequired = false,
|
||||
IsMandatory = false,
|
||||
CreatedAt = new DateTime(2025, 9, 3, 10, 46, 49, 955, DateTimeKind.Utc),
|
||||
IsSystem = true,
|
||||
IsActive = true,
|
||||
DocumentCategoryId = projectDocumentId,
|
||||
TenantId = tenantId
|
||||
},
|
||||
new DocumentTypeMaster
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Name = "Letter of Agreement",
|
||||
AllowedContentType = "application/pdf,application/msword,application/vnd.ms-excel,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet,application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
||||
MaxSizeAllowedInMB = 1,
|
||||
IsValidationRequired = false,
|
||||
IsMandatory = false,
|
||||
CreatedAt = new DateTime(2025, 9, 3, 10, 46, 49, 955, DateTimeKind.Utc),
|
||||
IsSystem = true,
|
||||
IsActive = true,
|
||||
DocumentCategoryId = projectDocumentId,
|
||||
TenantId = tenantId
|
||||
},
|
||||
new DocumentTypeMaster
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Name = "Health and Safety Document",
|
||||
AllowedContentType = "application/pdf,application/msword,application/vnd.ms-excel,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet,application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
||||
MaxSizeAllowedInMB = 1,
|
||||
IsValidationRequired = false,
|
||||
IsMandatory = false,
|
||||
CreatedAt = new DateTime(2025, 9, 3, 10, 46, 49, 955, DateTimeKind.Utc),
|
||||
IsSystem = true,
|
||||
IsActive = true,
|
||||
DocumentCategoryId = projectDocumentId,
|
||||
TenantId = tenantId
|
||||
},
|
||||
new DocumentTypeMaster
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Name = "Standard Operating Procedure (SOP)",
|
||||
AllowedContentType = "application/pdf,application/msword,application/vnd.ms-excel,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet,application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
||||
MaxSizeAllowedInMB = 1,
|
||||
IsValidationRequired = false,
|
||||
IsMandatory = false,
|
||||
CreatedAt = new DateTime(2025, 9, 3, 10, 46, 49, 955, DateTimeKind.Utc),
|
||||
IsSystem = true,
|
||||
IsActive = true,
|
||||
DocumentCategoryId = projectDocumentId,
|
||||
TenantId = tenantId
|
||||
},
|
||||
new DocumentTypeMaster
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Name = "Drawings",
|
||||
AllowedContentType = "application/pdf,image/vnd.dwg,application/acad",
|
||||
MaxSizeAllowedInMB = 20,
|
||||
IsValidationRequired = false,
|
||||
IsMandatory = false,
|
||||
CreatedAt = new DateTime(2025, 9, 3, 10, 46, 49, 955, DateTimeKind.Utc),
|
||||
IsSystem = true,
|
||||
IsActive = true,
|
||||
DocumentCategoryId = projectDocumentId,
|
||||
TenantId = tenantId
|
||||
}
|
||||
};
|
||||
}
|
||||
public List<object> GetData(Guid tenantId)
|
||||
{
|
||||
return new List<object>
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -67,7 +67,7 @@ namespace Marco.Pms.Services.Service
|
||||
_logger.LogInfo("Basic project list requested by EmployeeId {EmployeeId}", loggedInEmployee.Id);
|
||||
|
||||
// Step 2: Get the list of project IDs the user has access to
|
||||
List<Guid> accessibleProjectIds = await GetMyProjects(tenantId, loggedInEmployee);
|
||||
List<Guid> accessibleProjectIds = await _context.Projects.Where(p => p.TenantId == tenantId).Select(p => p.Id).ToListAsync();
|
||||
|
||||
if (accessibleProjectIds == null || !accessibleProjectIds.Any())
|
||||
{
|
||||
@ -96,7 +96,7 @@ namespace Marco.Pms.Services.Service
|
||||
_logger.LogInfo("Starting GetAllProjects for TenantId: {TenantId}, User: {UserId}", tenantId, loggedInEmployee.Id);
|
||||
|
||||
// --- Step 1: Get a list of project IDs the user can access ---
|
||||
List<Guid> projectIds = await GetMyProjects(tenantId, loggedInEmployee);
|
||||
List<Guid> projectIds = await _context.Projects.Where(p => p.TenantId == tenantId).Select(p => p.Id).ToListAsync();
|
||||
if (!projectIds.Any())
|
||||
{
|
||||
_logger.LogInfo("User has no assigned projects. Returning empty list.");
|
||||
@ -201,21 +201,21 @@ namespace Marco.Pms.Services.Service
|
||||
using var scope = _serviceScopeFactory.CreateScope();
|
||||
var _permission = scope.ServiceProvider.GetRequiredService<PermissionServices>();
|
||||
|
||||
// Step 1: Check global view project permission
|
||||
var hasViewProjectPermission = await _permission.HasPermission(PermissionsMaster.ViewProject, loggedInEmployee.Id, id);
|
||||
if (!hasViewProjectPermission)
|
||||
{
|
||||
_logger.LogWarning("ViewProjects permission denied for EmployeeId: {EmployeeId}", loggedInEmployee.Id);
|
||||
return ApiResponse<object>.ErrorResponse("Access denied", "You don't have permission to view projects", 403);
|
||||
}
|
||||
//// Step 1: Check global view project permission
|
||||
//var hasViewProjectPermission = await _permission.HasPermission(PermissionsMaster.ViewProject, loggedInEmployee.Id, id);
|
||||
//if (!hasViewProjectPermission)
|
||||
//{
|
||||
// _logger.LogWarning("ViewProjects permission denied for EmployeeId: {EmployeeId}", loggedInEmployee.Id);
|
||||
// return ApiResponse<object>.ErrorResponse("Access denied", "You don't have permission to view projects", 403);
|
||||
//}
|
||||
|
||||
// Step 2: Check permission for this specific project
|
||||
var hasProjectPermission = await _permission.HasProjectPermission(loggedInEmployee, id);
|
||||
if (!hasProjectPermission)
|
||||
{
|
||||
_logger.LogWarning("Project-specific access denied. EmployeeId: {EmployeeId}, ProjectId: {ProjectId}", loggedInEmployee.Id, id);
|
||||
return ApiResponse<object>.ErrorResponse("Access denied", "You don't have access to this project", 403);
|
||||
}
|
||||
//// Step 2: Check permission for this specific project
|
||||
//var hasProjectPermission = await _permission.HasProjectPermission(loggedInEmployee, id);
|
||||
//if (!hasProjectPermission)
|
||||
//{
|
||||
// _logger.LogWarning("Project-specific access denied. EmployeeId: {EmployeeId}, ProjectId: {ProjectId}", loggedInEmployee.Id, id);
|
||||
// return ApiResponse<object>.ErrorResponse("Access denied", "You don't have access to this project", 403);
|
||||
//}
|
||||
|
||||
// Step 3: Fetch project with status
|
||||
var projectDetails = await _cache.GetProjectDetails(id);
|
||||
@ -368,17 +368,20 @@ namespace Marco.Pms.Services.Service
|
||||
return ApiResponse<object>.ErrorResponse("Access Denied", "You do not have permission to create a project for this tenant.", 403);
|
||||
}
|
||||
|
||||
var promoterId = model.PromoterId ?? loggedInEmployee.OrganizationId;
|
||||
var pmcId = model.PMCId ?? loggedInEmployee.OrganizationId;
|
||||
|
||||
// Step 2: Concurrent validation for Promoter and PMC organization existence.
|
||||
// Run database queries in parallel for better performance.
|
||||
var promoterTask = Task.Run(async () =>
|
||||
{
|
||||
await using var context = await _dbContextFactory.CreateDbContextAsync();
|
||||
return await context.Organizations.FirstOrDefaultAsync(o => o.Id == model.PromoterId);
|
||||
return await context.Organizations.FirstOrDefaultAsync(o => o.Id == promoterId);
|
||||
});
|
||||
var pmcTask = Task.Run(async () =>
|
||||
{
|
||||
await using var context = await _dbContextFactory.CreateDbContextAsync();
|
||||
return await context.Organizations.FirstOrDefaultAsync(o => o.Id == model.PMCId);
|
||||
return await context.Organizations.FirstOrDefaultAsync(o => o.Id == pmcId);
|
||||
});
|
||||
|
||||
await Task.WhenAll(promoterTask, pmcTask);
|
||||
@ -388,18 +391,20 @@ namespace Marco.Pms.Services.Service
|
||||
|
||||
if (promoter == null)
|
||||
{
|
||||
_logger.LogWarning("Promoter check failed. PromoterId={PromoterId} not found.", model.PromoterId);
|
||||
_logger.LogWarning("Promoter check failed. PromoterId={PromoterId} not found.", promoterId);
|
||||
return ApiResponse<object>.ErrorResponse("Promoter not found", "Promoter not found in database.", 404);
|
||||
}
|
||||
if (pmc == null)
|
||||
{
|
||||
_logger.LogWarning("PMC check failed. PMCId={PMCId} not found.", model.PMCId);
|
||||
_logger.LogWarning("PMC check failed. PMCId={PMCId} not found.", pmcId);
|
||||
return ApiResponse<object>.ErrorResponse("PMC not found", "PMC not found in database.", 404);
|
||||
}
|
||||
|
||||
// Step 3: Prepare the project entity.
|
||||
var loggedInUserId = loggedInEmployee.Id;
|
||||
var project = _mapper.Map<Project>(model);
|
||||
project.PromoterId = promoterId;
|
||||
project.PMCId = pmcId;
|
||||
project.TenantId = tenantId;
|
||||
|
||||
// Step 4: Save the new project to the database.
|
||||
@ -476,6 +481,7 @@ namespace Marco.Pms.Services.Service
|
||||
// --- Step 1: Fetch the Existing Entity from the Database ---
|
||||
// This is crucial to avoid the data loss bug. We only want to modify an existing record.
|
||||
var existingProject = await _context.Projects
|
||||
.AsNoTracking()
|
||||
.Where(p => p.Id == id && p.TenantId == tenantId)
|
||||
.SingleOrDefaultAsync();
|
||||
|
||||
@ -496,17 +502,20 @@ namespace Marco.Pms.Services.Service
|
||||
return ApiResponse<object>.ErrorResponse("Access Denied", "You do not have permission to update a project for this tenant.", 403);
|
||||
}
|
||||
|
||||
var promoterId = model.PromoterId ?? loggedInEmployee.OrganizationId;
|
||||
var pmcId = model.PMCId ?? loggedInEmployee.OrganizationId;
|
||||
|
||||
// 1bb. Concurrent validation for Promoter and PMC organization existence.
|
||||
// Run database queries in parallel for better performance.
|
||||
var promoterTask = Task.Run(async () =>
|
||||
{
|
||||
await using var context = await _dbContextFactory.CreateDbContextAsync();
|
||||
return await context.Organizations.FirstOrDefaultAsync(o => o.Id == model.PromoterId);
|
||||
return await context.Organizations.FirstOrDefaultAsync(o => o.Id == promoterId);
|
||||
});
|
||||
var pmcTask = Task.Run(async () =>
|
||||
{
|
||||
await using var context = await _dbContextFactory.CreateDbContextAsync();
|
||||
return await context.Organizations.FirstOrDefaultAsync(o => o.Id == model.PMCId);
|
||||
return await context.Organizations.FirstOrDefaultAsync(o => o.Id == pmcId);
|
||||
});
|
||||
|
||||
await Task.WhenAll(promoterTask, pmcTask);
|
||||
@ -516,12 +525,12 @@ namespace Marco.Pms.Services.Service
|
||||
|
||||
if (promoter == null)
|
||||
{
|
||||
_logger.LogWarning("Promoter check failed. PromoterId={PromoterId} not found.", model.PromoterId);
|
||||
_logger.LogWarning("Promoter check failed. PromoterId={PromoterId} not found.", promoterId);
|
||||
return ApiResponse<object>.ErrorResponse("Promoter not found", "Promoter not found in database.", 404);
|
||||
}
|
||||
if (pmc == null)
|
||||
{
|
||||
_logger.LogWarning("PMC check failed. PMCId={PMCId} not found.", model.PMCId);
|
||||
_logger.LogWarning("PMC check failed. PMCId={PMCId} not found.", pmcId);
|
||||
return ApiResponse<object>.ErrorResponse("PMC not found", "PMC not found in database.", 404);
|
||||
}
|
||||
|
||||
@ -538,8 +547,11 @@ namespace Marco.Pms.Services.Service
|
||||
// This only modifies the properties defined in the mapping, preventing data loss.
|
||||
_mapper.Map(model, existingProject);
|
||||
|
||||
existingProject.PromoterId = promoterId;
|
||||
existingProject.PMCId = pmcId;
|
||||
|
||||
// Mark the entity as modified (if your mapping doesn't do it automatically).
|
||||
_context.Entry(existingProject).State = EntityState.Modified;
|
||||
_context.Projects.Update(existingProject);
|
||||
|
||||
try
|
||||
{
|
||||
@ -716,7 +728,7 @@ namespace Marco.Pms.Services.Service
|
||||
.Include(pa => pa.Employee)
|
||||
.ThenInclude(e => e!.Organization)
|
||||
.Include(pa => pa.Service)
|
||||
.Where(pa => pa.ProjectId == projectId && pa.TenantId == tenantId);
|
||||
.Where(pa => pa.ProjectId == projectId && pa.TenantId == tenantId && pa.Service != null);
|
||||
|
||||
// Conditionally apply the filter for active allocations.
|
||||
if (!includeInactive)
|
||||
@ -738,7 +750,6 @@ namespace Marco.Pms.Services.Service
|
||||
}
|
||||
|
||||
var allocations = await projectAllocationQuery
|
||||
.Where(pa => pa.Service != null)
|
||||
.Select(pa => new
|
||||
{
|
||||
// Fields from ProjectAllocation
|
||||
@ -1013,12 +1024,6 @@ namespace Marco.Pms.Services.Service
|
||||
}
|
||||
}
|
||||
|
||||
var selectedEmployee = await _context.Employees.FirstOrDefaultAsync(e => e.Id == employeeId);
|
||||
if (selectedEmployee == null)
|
||||
{
|
||||
_logger.LogWarning("Employee not found while assigning the projects to employee");
|
||||
return ApiResponse<List<ProjectAllocationVM>>.ErrorResponse("Employee not found", "Employee not found", 404);
|
||||
}
|
||||
|
||||
// --- Step 2: Fetch all relevant existing data in ONE database call ---
|
||||
var projectIdsInDto = allocationsDto.Select(p => p.ProjectId).ToList();
|
||||
@ -1034,11 +1039,6 @@ namespace Marco.Pms.Services.Service
|
||||
|
||||
var processedAllocations = new List<ProjectAllocation>();
|
||||
|
||||
var serviceProjects = await _context.ProjectOrgMappings
|
||||
.Include(ps => ps.ProjectService)
|
||||
.Where(ps => ps.ProjectService != null && projectIdsInDto.Contains(ps.ProjectService.ProjectId) &&
|
||||
ps.OrganizationId == selectedEmployee.OrganizationId && ps.TenantId == tenantId).ToListAsync();
|
||||
|
||||
// --- Step 3: Process all logic IN MEMORY, tracking changes ---
|
||||
foreach (var dto in allocationsDto)
|
||||
{
|
||||
@ -1060,13 +1060,11 @@ namespace Marco.Pms.Services.Service
|
||||
{
|
||||
if (existingAllocation == null)
|
||||
{
|
||||
var serviceProject = serviceProjects.FirstOrDefault(ps => ps.ProjectService != null && ps.ProjectService.ProjectId == dto.ProjectId);
|
||||
// Create a new allocation because an active one doesn't exist.
|
||||
var newAllocation = _mapper.Map<ProjectAllocation>(dto);
|
||||
newAllocation.EmployeeId = employeeId;
|
||||
newAllocation.TenantId = tenantId;
|
||||
newAllocation.AllocationDate = DateTime.UtcNow;
|
||||
newAllocation.ServiceId = dto.ServiceId ?? serviceProject?.ProjectService?.ServiceId;
|
||||
newAllocation.IsActive = true;
|
||||
_context.ProjectAllocations.Add(newAllocation);
|
||||
processedAllocations.Add(newAllocation);
|
||||
@ -2693,8 +2691,6 @@ namespace Marco.Pms.Services.Service
|
||||
.AsNoTracking()
|
||||
.Include(po => po.ProjectService)
|
||||
.ThenInclude(ps => ps!.Service)
|
||||
.Include(po => po.AssignedBy)
|
||||
.Include(po => po.OrganizationType)
|
||||
.Include(po => po.Organization)
|
||||
.Where(po => po.ProjectService != null
|
||||
&& po.ProjectService.ProjectId == projectId
|
||||
@ -2711,7 +2707,7 @@ namespace Marco.Pms.Services.Service
|
||||
|
||||
// Filter and map the data to the desired view model
|
||||
var response = projectOrgMappings
|
||||
.Where(po => po.Organization != null && po.OrganizationType != null)
|
||||
.Where(po => po.Organization != null)
|
||||
.Select(po => new ProjectOrganizationVM
|
||||
{
|
||||
Id = po.Organization!.Id,
|
||||
@ -2720,7 +2716,6 @@ namespace Marco.Pms.Services.Service
|
||||
ContactPerson = po.Organization.ContactPerson,
|
||||
SPRID = po.Organization.SPRID,
|
||||
logoImage = po.Organization.logoImage,
|
||||
OrganizationType = po.OrganizationType!.Name,
|
||||
AssignedBy = _mapper.Map<BasicEmployeeVM>(po.AssignedBy),
|
||||
Service = _mapper.Map<ServiceMasterVM>(po.ProjectService!.Service),
|
||||
AssignedDate = po.AssignedDate,
|
||||
@ -2750,7 +2745,6 @@ namespace Marco.Pms.Services.Service
|
||||
ContactPerson = pmc.ContactPerson,
|
||||
SPRID = pmc.SPRID,
|
||||
logoImage = pmc.logoImage,
|
||||
OrganizationType = "PMC",
|
||||
AssignedBy = assignedBy,
|
||||
AssignedDate = assignedDate,
|
||||
CompletionDate = completionDate
|
||||
@ -2766,7 +2760,6 @@ namespace Marco.Pms.Services.Service
|
||||
ContactPerson = promoter.ContactPerson,
|
||||
SPRID = promoter.SPRID,
|
||||
logoImage = promoter.logoImage,
|
||||
OrganizationType = "Promotor",
|
||||
AssignedBy = assignedBy,
|
||||
AssignedDate = assignedDate,
|
||||
CompletionDate = completionDate
|
||||
@ -2782,7 +2775,6 @@ namespace Marco.Pms.Services.Service
|
||||
ContactPerson = organization.ContactPerson,
|
||||
SPRID = organization.SPRID,
|
||||
logoImage = organization.logoImage,
|
||||
OrganizationType = "Primary",
|
||||
AssignedBy = assignedBy,
|
||||
AssignedDate = assignedDate,
|
||||
CompletionDate = completionDate
|
||||
@ -2805,128 +2797,6 @@ namespace Marco.Pms.Services.Service
|
||||
return ApiResponse<object>.ErrorResponse("Internal error", "An internal exception occurred", 500);
|
||||
}
|
||||
}
|
||||
public async Task<ApiResponse<object>> GetAssignedOrganizationsToProjectForDropdownAsync(Guid projectId, Guid tenantId, Employee loggedInEmployee)
|
||||
{
|
||||
_logger.LogDebug("Started fetching assigned organizations for ProjectId: {ProjectId} and TenantId: {TenantId} by user {UserId}",
|
||||
projectId, tenantId, loggedInEmployee.Id);
|
||||
|
||||
try
|
||||
{
|
||||
// Create a scoped PermissionServices instance for permission checks
|
||||
using var scope = _serviceScopeFactory.CreateScope();
|
||||
var permissionService = scope.ServiceProvider.GetRequiredService<PermissionServices>();
|
||||
|
||||
// Retrieve the project by projectId and tenantId
|
||||
var projectTask = Task.Run(async () =>
|
||||
{
|
||||
await using var context = await _dbContextFactory.CreateDbContextAsync();
|
||||
return await context.Projects.AsNoTracking().Include(p => p.Promoter).Include(p => p.PMC).FirstOrDefaultAsync(p => p.Id == projectId && p.TenantId == tenantId);
|
||||
});
|
||||
|
||||
var tenantTask = Task.Run(async () =>
|
||||
{
|
||||
await using var context = await _dbContextFactory.CreateDbContextAsync();
|
||||
return await context.Tenants.AsNoTracking().Include(t => t.Organization).FirstOrDefaultAsync(t => t.Id == tenantId);
|
||||
});
|
||||
var projectServiceTask = Task.Run(async () =>
|
||||
{
|
||||
await using var context = await _dbContextFactory.CreateDbContextAsync();
|
||||
return await context.ProjectServiceMappings
|
||||
.AsNoTracking()
|
||||
.Include(ps => ps!.Service)
|
||||
.Where(ps => ps.ProjectId == projectId && ps.TenantId == tenantId).ToListAsync();
|
||||
});
|
||||
|
||||
await Task.WhenAll(projectTask, tenantTask, projectServiceTask);
|
||||
|
||||
var project = projectTask.Result;
|
||||
var tenant = tenantTask.Result;
|
||||
var projectService = projectServiceTask.Result;
|
||||
|
||||
if (project == null || tenant == null)
|
||||
{
|
||||
_logger.LogWarning("Project {ProjectId} not found in database for tenant {TenantId}", projectId, tenantId);
|
||||
return ApiResponse<object>.ErrorResponse("Project not found", "Project not found", 404);
|
||||
}
|
||||
|
||||
// Check if the logged in employee has permission to access the project
|
||||
var hasPermission = await permissionService.HasProjectPermission(loggedInEmployee, projectId);
|
||||
if (!hasPermission)
|
||||
{
|
||||
_logger.LogWarning("Access denied for user {UserId} on project {ProjectId}", loggedInEmployee.Id, projectId);
|
||||
return ApiResponse<object>.ErrorResponse("Access Denied", "You do not have permission to access this project.", 403);
|
||||
}
|
||||
|
||||
// Fetch all project-organization mappings with related service and organization data
|
||||
var projectOrgMappingsQuery = _context.ProjectOrgMappings
|
||||
.AsNoTracking()
|
||||
.Include(po => po.ProjectService)
|
||||
.ThenInclude(ps => ps!.Service)
|
||||
.Include(po => po.AssignedBy)
|
||||
.Include(po => po.OrganizationType)
|
||||
.Include(po => po.Organization)
|
||||
.Where(po => po.ProjectService != null
|
||||
&& po.ProjectService.ProjectId == projectId
|
||||
&& po.TenantId == tenantId);
|
||||
|
||||
if (loggedInEmployee.OrganizationId != project.PMCId && loggedInEmployee.OrganizationId != project.PromoterId && loggedInEmployee.OrganizationId != tenant.OrganizationId)
|
||||
{
|
||||
projectOrgMappingsQuery = projectOrgMappingsQuery.Where(po => po.ParentOrganizationId == loggedInEmployee.OrganizationId || po.OrganizationId == loggedInEmployee.OrganizationId);
|
||||
}
|
||||
|
||||
var projectOrgMappings = await projectOrgMappingsQuery
|
||||
.Distinct()
|
||||
.ToListAsync();
|
||||
var organizations = projectOrgMappings.Select(po => po.Organization!).ToList();
|
||||
|
||||
if (loggedInEmployee.OrganizationId == project.PMCId || loggedInEmployee.OrganizationId == project.PromoterId || loggedInEmployee.OrganizationId == tenant.OrganizationId)
|
||||
{
|
||||
var pmc = project.PMC;
|
||||
var promoter = project.Promoter;
|
||||
var organization = tenant.Organization;
|
||||
|
||||
if (!organizations.Any(r => r.Id == project.PMCId) && pmc != null)
|
||||
{
|
||||
organizations.Add(pmc);
|
||||
}
|
||||
if (!organizations.Any(r => r.Id == project.PromoterId) && promoter != null)
|
||||
{
|
||||
organizations.Add(promoter);
|
||||
}
|
||||
if (!organizations.Any(r => r.Id == tenant.OrganizationId) && organization != null)
|
||||
{
|
||||
organizations.Add(organization);
|
||||
}
|
||||
}
|
||||
|
||||
organizations = organizations.DistinctBy(o => o.Id).ToList();
|
||||
|
||||
// Filter and map the data to the desired view model
|
||||
var response = organizations
|
||||
.Select(o => new ProjectOrganizationVM
|
||||
{
|
||||
Id = o.Id,
|
||||
Name = o.Name,
|
||||
SPRID = 0
|
||||
})
|
||||
.ToList();
|
||||
|
||||
_logger.LogInfo("Fetched {Count} assigned organizations for ProjectId: {ProjectId}", response.Count, projectId);
|
||||
|
||||
return ApiResponse<object>.SuccessResponse(response, "Successfully fetched the list of organizations assigned to the project", 200);
|
||||
}
|
||||
catch (DbUpdateException dbEx)
|
||||
{
|
||||
_logger.LogError(dbEx, "Database exception while fetching assigned organizations for ProjectId: {ProjectId}", projectId);
|
||||
return ApiResponse<object>.ErrorResponse("Internal error", "A database exception occurred", 500);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Unhandled exception while fetching assigned organizations for ProjectId: {ProjectId}", projectId);
|
||||
return ApiResponse<object>.ErrorResponse("Internal error", "An internal exception occurred", 500);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#endregion
|
||||
|
||||
@ -2945,14 +2815,9 @@ namespace Marco.Pms.Services.Service
|
||||
public async Task<List<ProjectAllocation>> GetTeamByProject(Guid TenantId, Guid ProjectId, Guid? OrganizationId, bool IncludeInactive)
|
||||
{
|
||||
var projectAllocationQuery = _context.ProjectAllocations
|
||||
.Include(pa => pa.Project)
|
||||
.Include(pa => pa.Employee)
|
||||
.ThenInclude(e => e!.Organization)
|
||||
.Include(pa => pa.Employee)
|
||||
.ThenInclude(e => e!.JobRole)
|
||||
.Where(pa => pa.TenantId == TenantId &&
|
||||
pa.ProjectId == ProjectId && pa.Project != null &&
|
||||
pa.Employee != null && pa.Employee.Organization != null && pa.Employee.JobRole != null);
|
||||
.Include(pa => pa.Employee)
|
||||
.ThenInclude(e => e!.Organization)
|
||||
.Where(pa => pa.TenantId == TenantId && pa.ProjectId == ProjectId);
|
||||
if (!IncludeInactive)
|
||||
{
|
||||
projectAllocationQuery = projectAllocationQuery.Where(pa => pa.IsActive);
|
||||
|
@ -49,7 +49,6 @@ namespace Marco.Pms.Services.Service.ServiceInterfaces
|
||||
Task<ApiResponse<object>> DeassignServiceToProjectAsync(DeassignServiceDto model, Guid tenantId, Employee loggedInEmployee);
|
||||
|
||||
Task<ApiResponse<object>> GetAssignedOrganizationsToProjectAsync(Guid projectId, Guid tenantId, Employee loggedInEmployee);
|
||||
Task<ApiResponse<object>> GetAssignedOrganizationsToProjectForDropdownAsync(Guid projectId, Guid tenantId, Employee loggedInEmployee);
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -48,7 +48,7 @@
|
||||
},
|
||||
"MongoDB": {
|
||||
"SerilogDatabaseUrl": "mongodb://localhost:27017/DotNetLogs",
|
||||
"ConnectionString": "mongodb://localhost:27017/MarcoBMS_Caches?socketTimeoutMS=500&serverSelectionTimeoutMS=500&connectTimeoutMS=500",
|
||||
"ModificationConnectionString": "mongodb://devuser:DevPass123@147.93.98.152:27017/MarcoBMSLocalDev?authSource=admin&eplicaSet=rs01&directConnection=true"
|
||||
"ConnectionString": "mongodb://devuser:DevPass123@147.93.98.152:27017/MarcoBMSCacheLocalDev?authSource=admin&replicaSet=rs01",
|
||||
"ModificationConnectionString": "mongodb://devuser:DevPass123@147.93.98.152:27017/MarcoBMSLocalDev?authSource=admin&replicaSet=rs01&directConnection=true"
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user