Compare commits

..

43 Commits

Author SHA1 Message Date
663d94093c Enhanced the employe log API 2025-10-10 10:01:21 +05:30
dc83aa72a6 Added the API to get filter task allocation 2025-10-09 16:57:04 +05:30
ef597b2bb7 Added the project ID in attendance VM 2025-10-09 11:28:06 +05:30
4acc61f03a Added the Requested by in attendance table 2025-10-08 17:54:37 +05:30
c47b0da7b8 Showing the list of all attendance even if user have selfattendance permissons 2025-10-08 14:16:24 +05:30
d180d5d60e Changed the variable from totalCount to totalAmount 2025-10-08 12:32:09 +05:30
60eefa78c0 Corrected the total amount sent in expense pending dashboard 2025-10-08 12:30:34 +05:30
b1af96b923 Added the name of the project name in view model 2025-10-08 12:16:25 +05:30
629c4541d6 Removed the logic to save the FCM from verify FCM API 2025-10-08 11:35:43 +05:30
e93408c00d Removed the FCM token verify MPIN API 2025-10-08 11:30:47 +05:30
16e509ccbd Hided the tenant management feature permissions from tenant other than super tenant 2025-10-08 11:03:29 +05:30
8a4a056c2d Added the query parameter to get all employee in basic employee API 2025-10-08 10:56:05 +05:30
f6e8a0d5e2 Sloved missing veriable issue 2025-10-06 15:29:56 +05:30
0561e356d8 Added the condition to get all records in expense monthly report API 2025-10-06 15:29:00 +05:30
e2ee3f325c Added the Distinct in expense filter 2025-10-04 12:38:22 +05:30
6441103e30 Chnaged the search to searchString in get directory list API 2025-10-04 12:35:15 +05:30
d380dfebd2 Added the ExpensTypes in expense list API and get expenese filter API 2025-10-04 12:25:02 +05:30
658fa8cd23 Changed he expense UID format 2025-10-04 10:45:38 +05:30
3afdad29b2 Added total amout in expense pending dashboard 2025-10-03 17:13:23 +05:30
8f463ce90d Added ExpenseUId in expense table 2025-10-03 15:24:59 +05:30
590476a8aa Added the APi to get count of pending expenses 2025-10-03 14:32:59 +05:30
548e714ea9 Getting the monthly expenses report based on project and category 2025-10-03 12:53:19 +05:30
91f4305995 Added the projectId in get expense pending list API 2025-10-03 11:08:15 +05:30
7e15c517ac Optimized the dashboard APIs for expenses 2025-10-03 10:54:38 +05:30
9332d9cc0b Added the API to get list pending expenses according to permissions 2025-10-01 18:12:51 +05:30
541ed28bd2 Added the new deashboard report of expenses 2025-10-01 17:32:48 +05:30
0e1d20156f Check if email exist in total tenant 2025-10-01 17:16:27 +05:30
87c5de87a1 Checking in organization not in tenant 2025-10-01 12:19:03 +05:30
5eda1773b7 Chnaged the sequence of buttons in sequece 2025-10-01 11:33:56 +05:30
b3f54962ab Removed hasProjectAccess check from document controller 2025-10-01 10:55:00 +05:30
040e7df32b Removed the permission check from project details API 2025-10-01 10:32:04 +05:30
0066e20c43 Removed the tenant check from find employee query in document controller 2025-10-01 10:23:46 +05:30
7659eadd00 Added the check before sending mail when adding application access to employee 2025-10-01 10:10:01 +05:30
824381bb49 Added the project section in side menu 2025-10-01 09:53:40 +05:30
207a44acd7 Checking if have directory admin permission in delete contact API 2025-09-30 21:32:59 +05:30
7775f58d69 Added the check to check if has directory admin permission 2025-09-30 21:13:56 +05:30
91be729b41 Changed the logic of getting all employees in get employee list API 2025-09-30 20:53:54 +05:30
0bd57d29d8 Removed the Authorization of hub 2025-09-30 20:31:57 +05:30
b442bb4bbc Changed the logic to get tenantId in auth controller 2025-09-30 20:23:54 +05:30
ca3e47c1e6 Changed theexpenses Vms 2025-09-30 20:11:29 +05:30
061512d501 Removed the organization ID from manage employee DTO 2025-09-30 19:44:00 +05:30
71cc442054 getting the all project through get project list API 2025-09-30 19:28:09 +05:30
bab03a8e47 Getting all employees in attendence module regardless of project 2025-09-30 18:17:24 +05:30
45 changed files with 1320 additions and 1884 deletions

View File

@ -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 />

View File

@ -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 />

View File

@ -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();

View File

@ -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")]

View File

@ -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; }
}
}

View File

@ -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; }
}
}

View File

@ -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; }
}

View File

@ -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; }
}
}

View File

@ -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; }
}
}

View File

@ -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; }
}
}

View File

@ -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; }
}
}

View File

@ -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; }
}
}

View File

@ -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; }
}
}

View File

@ -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; }
}
}

View File

@ -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; }

View File

@ -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;

View File

@ -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; }

View File

@ -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; }

View File

@ -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>();

View File

@ -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))

View File

@ -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,98 +228,60 @@ 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);
}
}
}
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
{
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,
EmployeeId = attendance.EmployeeId,
FirstName = attendance.Employee?.FirstName,
LastName = attendance.Employee?.LastName,
JobRoleName = attendance.Employee?.JobRole?.Name,
ProjectId = attendance.ProjectID,
ProjectName = projectAllocation.Project?.Name,
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,
ApprovedAt = attendance.ApprovedAt,
Approver = _mapper.Map<BasicEmployeeVM>(attendance.Approver),
RequestedAt = attendance.RequestedAt,
RequestedBy = _mapper.Map<BasicEmployeeVM>(attendance.RequestedBy)
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);
return Ok(ApiResponse<object>.SuccessResponse(result, System.String.Format("{0} Attendance records fetched successfully", result.Count), 200));
}
[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,48 +330,49 @@ 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()
{
@ -431,19 +382,16 @@ namespace MarcoBMS.Services.Controllers
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),
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,58 +896,51 @@ 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)
{
foreach (var lstAttendance in lstAttendances)
{
EmployeeAttendanceVM result1 = new EmployeeAttendanceVM
{
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,
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,
ApprovedAt = lstAttendance.ApprovedAt,
Approver = _mapper.Map<BasicEmployeeVM>(lstAttendance.Approver),
RequestedAt = lstAttendance.RequestedAt,
RequestedBy = _mapper.Map<BasicEmployeeVM>(lstAttendance.RequestedBy)
Activity = lstAttendance.Activity
};
result.Add(result1);
}
}
return result;
}

View File

@ -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

View File

@ -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]
}
}
}
}

View File

@ -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);

View File

@ -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
{

View File

@ -233,84 +233,12 @@ 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]
var employees = await _context.Employees
.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()
.Include(e => e.Organization)
.Where(e => e.OrganizationId == loggedInEmployee.OrganizationId && e.IsActive != showInactive)
.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);
}
// 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)
{
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 ?? "");
await _emailSender.SendResetPasswordEmailOnRegister(u.Email, firstName, resetLink);
_logger.LogInfo("Reset password email queued. Email={Email}", u.Email);
}
}
}
}

View File

@ -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

View File

@ -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,14 +717,8 @@ 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)
{
_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));
}
// Create new tenant-organization mapping if none exists
var newMapping = new TenantOrgMapping
{
@ -745,13 +734,17 @@ namespace Marco.Pms.Services.Controllers
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.CommitAsync();
}
// 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
}
}

View File

@ -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
}

View File

@ -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,54 +458,32 @@ 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
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 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 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,
ProjectName = _context.Projects.Where(p => p.Id == g.Key && p.TenantId == tenantId).Select(p => p.Name).FirstOrDefault(),
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 = attendances.FirstOrDefault(a => a.AttendanceDate.Date == d);
var attendance = gp.FirstOrDefault(a => a.AttendanceDate.Date == d);
return new
{
AttendanceDate = d,
@ -515,18 +493,13 @@ namespace Marco.Pms.Services.Controllers
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(),
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();
return new
{
ProjectName = _context.Projects.Where(p => p.Id == g.Key && p.TenantId == tenantId).Select(p => p.Name).FirstOrDefault(),
ProjectAttendance = projectAttendance
}).OrderBy(ar => ar.FirstName).ThenBy(ar => ar.LastName).ToList()
};
}).ToList();

View File

@ -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
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
},
new RolePermissionMappings
});
rolePermissionMappigs.Add(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
}
};
});
_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
}
}

View File

@ -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

View File

@ -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);

View File

@ -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;

View File

@ -3,6 +3,7 @@ using Microsoft.AspNetCore.SignalR;
namespace Marco.Pms.Services.Hubs
{
//[Authorize]
public class MarcoHub : Hub
{
private readonly ILoggingService _logger;

View File

@ -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 =======================================================

View File

@ -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}",

View File

@ -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}" })
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();
});
// 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);
// 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;

View File

@ -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 drivers 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

View File

@ -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);
.Where(pa => pa.TenantId == TenantId && pa.ProjectId == ProjectId);
if (!IncludeInactive)
{
projectAllocationQuery = projectAllocationQuery.Where(pa => pa.IsActive);

View File

@ -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);
}
}

View File

@ -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"
}
}