Compare commits

...

38 Commits

Author SHA1 Message Date
7e20807325 corrected the distinct by error 2025-10-11 15:57:53 +05:30
186486d934 Added the new API to get organizations for dropdown 2025-10-11 15:36:52 +05:30
d07f0311ae Optmized the work status master APIs 2025-10-11 13:52:20 +05:30
2f6031e62c revert e02636b6b66287a22fcfb34c150aece1b136cc68
revert Optmized the work status master APIs
2025-10-11 08:19:59 +00:00
e02636b6b6 Optmized the work status master APIs 2025-10-11 13:48:36 +05:30
bad784e147 Optimized the contact related msater APIs 2025-10-11 12:59:31 +05:30
886d0bb3b1 Clearing the employee profile cache when selecting the tenant 2025-10-10 18:32:01 +05:30
74dd9eeb8d organization Tenant Mapping is already existed then return error message 2025-10-10 17:53:23 +05:30
1939a63d9a Assigning the appilcation role to root employee of the any organization 2025-10-10 17:05:43 +05:30
c07db9f94d removed employee project cache when adding the subscription 2025-10-10 15:59:42 +05:30
53da15416a Removing all employees profile from cache for certain tenant 2025-10-10 15:46:56 +05:30
a809bdd469 Merge pull request 'Sloved issues of permissions not been assigned when updating the subscription' (#144) from Ashutosh_Bug#1461 into main
Reviewed-on: #144
2025-10-10 08:49:19 +00:00
bb2c098cb9 Sloved issues of permissions not been assigned when updating the subscription 2025-10-10 14:17:32 +05:30
bd3b70f4ea Merge pull request 'Added the RequestedAt and RequestedBy in attendance' (#143) from Ashutosh_Enhancement#1452 into main
Reviewed-on: #143
2025-10-10 07:06:25 +00:00
000cef3bb7 enhanced the get team based on project 2025-10-10 12:36:11 +05:30
522deae8f7 Added the RequestedAt and RequestedBy in attendance 2025-10-10 12:08:53 +05:30
9a8aa4f5ce Changed the index of reject button in expense controller 2025-10-09 16:52:34 +05:30
af92ab977b Added the get filter API 2025-10-09 16:47:06 +05:30
c06dc8ebe7 Selecting the default services in project allocation 2025-10-09 16:00:26 +05:30
8609db64d2 Made the serviceId is nullable in project allocation 2025-10-09 15:23:37 +05:30
e831f50505 Removed the logic to save the FCM from verify FCM API 2025-10-08 11:33:34 +05:30
ba88fbced6 Removed the FCM token from MPIN verify API 2025-10-08 11:27:30 +05:30
13d2e1cd7d Corrected the mistake of showing all images except selected project 2025-10-07 12:34:48 +05:30
26acfec408 Added the todays completed tasks 2025-10-06 18:33:37 +05:30
3bd38f3c68 Changed the logic in API to get Attendance logs by employee 2025-10-06 16:10:58 +05:30
2e29dc9946 Chnaged the check for persentages 2025-10-06 14:42:29 +05:30
db752a4678 Merge branch 'main' of https://git.marcoaiot.com/admin/marco.pms.api 2025-10-06 13:16:31 +05:30
245182eb07 Added the attendance persentage and task persentage 2025-10-06 13:15:48 +05:30
9daf76c6eb return all employees when allEmployee=true 2025-10-05 00:24:26 +05:30
c33afa58c3 Solved spelling mistake 2025-10-04 17:51:58 +05:30
4ceb5c3cb2 Changed the authontication logic for delete contact notes 2025-10-04 17:50:18 +05:30
d0dabf776b Added the condition to not get services object in project allocation 2025-10-04 17:26:08 +05:30
04223578ad Added the ExpenseUId in expenses tables 2025-10-04 16:56:04 +05:30
f94a7de4ab change email in complete system 2025-10-01 17:46:11 +05:30
eb3a65428e Modyfied the attendance report API 2025-10-01 15:09:51 +05:30
8ddb414e91 Added the organization type in assignd organization list 2025-09-30 18:08:07 +05:30
d8329f1fab Removed the tenant check from master services 2025-09-30 17:10:30 +05:30
d0912cca56 Merge pull request 'Organization_Management' (#142) from Organization_Management into main
Reviewed-on: #142
2025-09-30 09:05:14 +00:00
44 changed files with 14275 additions and 573 deletions

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,29 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Marco.Pms.DataAccess.Migrations
{
/// <inheritdoc />
public partial class Added_ExpenceUID_In_Expense_Table : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "ExpenseUId",
table: "Expenses",
type: "longtext",
nullable: false)
.Annotation("MySql:CharSet", "utf8mb4");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "ExpenseUId",
table: "Expenses");
}
}
}

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,70 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Marco.Pms.DataAccess.Migrations
{
/// <inheritdoc />
public partial class Added_Requested_In_Attendance_Table : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<DateTime>(
name: "ApprovedAt",
table: "Attendes",
type: "datetime(6)",
nullable: true);
migrationBuilder.AddColumn<DateTime>(
name: "RequestedAt",
table: "Attendes",
type: "datetime(6)",
nullable: true);
migrationBuilder.AddColumn<Guid>(
name: "RequestedById",
table: "Attendes",
type: "char(36)",
nullable: true,
collation: "ascii_general_ci");
migrationBuilder.CreateIndex(
name: "IX_Attendes_RequestedById",
table: "Attendes",
column: "RequestedById");
migrationBuilder.AddForeignKey(
name: "FK_Attendes_Employees_RequestedById",
table: "Attendes",
column: "RequestedById",
principalTable: "Employees",
principalColumn: "Id");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_Attendes_Employees_RequestedById",
table: "Attendes");
migrationBuilder.DropIndex(
name: "IX_Attendes_RequestedById",
table: "Attendes");
migrationBuilder.DropColumn(
name: "ApprovedAt",
table: "Attendes");
migrationBuilder.DropColumn(
name: "RequestedAt",
table: "Attendes");
migrationBuilder.DropColumn(
name: "RequestedById",
table: "Attendes");
}
}
}

View File

@ -172,6 +172,9 @@ namespace Marco.Pms.DataAccess.Migrations
b.Property<int>("Activity")
.HasColumnType("int");
b.Property<DateTime?>("ApprovedAt")
.HasColumnType("datetime(6)");
b.Property<Guid?>("ApprovedById")
.HasColumnType("char(36)");
@ -200,6 +203,12 @@ namespace Marco.Pms.DataAccess.Migrations
b.Property<Guid>("ProjectID")
.HasColumnType("char(36)");
b.Property<DateTime?>("RequestedAt")
.HasColumnType("datetime(6)");
b.Property<Guid?>("RequestedById")
.HasColumnType("char(36)");
b.Property<Guid>("TenantId")
.HasColumnType("char(36)");
@ -209,6 +218,8 @@ namespace Marco.Pms.DataAccess.Migrations
b.HasIndex("EmployeeId");
b.HasIndex("RequestedById");
b.HasIndex("TenantId");
b.ToTable("Attendes");
@ -1832,6 +1843,10 @@ namespace Marco.Pms.DataAccess.Migrations
.IsRequired()
.HasColumnType("longtext");
b.Property<string>("ExpenseUId")
.IsRequired()
.HasColumnType("longtext");
b.Property<Guid>("ExpensesTypeId")
.HasColumnType("char(36)");
@ -4711,6 +4726,10 @@ namespace Marco.Pms.DataAccess.Migrations
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Marco.Pms.Model.Employees.Employee", "RequestedBy")
.WithMany()
.HasForeignKey("RequestedById");
b.HasOne("Marco.Pms.Model.TenantModels.Tenant", "Tenant")
.WithMany()
.HasForeignKey("TenantId")
@ -4721,6 +4740,8 @@ namespace Marco.Pms.DataAccess.Migrations
b.Navigation("Employee");
b.Navigation("RequestedBy");
b.Navigation("Tenant");
});

View File

@ -212,6 +212,48 @@ 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

@ -29,5 +29,12 @@ 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")]
[ValidateNever]
public Employee? RequestedBy { get; set; }
}
}

View File

@ -2,7 +2,7 @@
{
public class CreateWorkStatusMasterDto
{
public string? Name { get; set; }
public string? Description { get; set; }
public required string Name { get; set; }
public required string Description { get; set; }
}
}

View File

@ -3,7 +3,7 @@
public class UpdateWorkStatusMasterDto
{
public Guid Id { get; set; }
public string? Name { get; set; }
public string? Description { get; set; }
public required string Name { get; set; }
public required string Description { get; set; }
}
}

View File

@ -5,6 +5,5 @@
public required Guid EmployeeId { get; set; }
public required string MPIN { get; set; }
public required string MPINToken { get; set; }
public required string FcmToken { get; set; }
}
}

View File

@ -2,7 +2,7 @@
{
public class CreateContactCategoryDto
{
public string? Name { get; set; }
public string? Description { get; set; }
public required string Name { get; set; }
public required string Description { get; set; }
}
}

View File

@ -2,7 +2,7 @@
{
public class CreateContactTagDto
{
public string? Name { get; set; }
public string? Description { get; set; }
public required string Name { get; set; }
public required string Description { get; set; }
}
}

View File

@ -2,8 +2,8 @@
{
public class UpdateContactCategoryDto
{
public Guid Id { get; set; }
public string? Name { get; set; }
public string? Description { get; set; }
public required Guid Id { get; set; }
public required string Name { get; set; }
public required string Description { get; set; }
}
}

View File

@ -3,7 +3,7 @@
public class UpdateContactTagDto
{
public Guid Id { get; set; }
public string? Name { get; set; }
public string? Description { get; set; }
public required string Name { get; set; }
public required string Description { 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

@ -54,6 +54,7 @@ namespace Marco.Pms.Model.Expenses
public DateTime CreatedAt { get; set; }
public string? TransactionId { get; set; }
public string Description { get; set; } = string.Empty;
public string ExpenseUId { get; set; } = string.Empty;
public string? Location { get; set; }
public string? GSTNumber { get; set; }
public string SupplerName { get; set; } = string.Empty;

View File

@ -5,6 +5,7 @@
public List<Guid>? BuildingIds { get; set; }
public List<Guid>? FloorIds { get; set; }
public List<Guid>? ActivityIds { get; set; }
public List<Guid>? ServiceIds { get; set; }
public DateTime? dateFrom { get; set; }
public DateTime? dateTo { get; set; }
}

View File

@ -19,6 +19,7 @@ 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 ExpensesStatusMasterMongoDB Status { get; set; } = new ExpensesStatusMasterMongoDB();
public List<ExpensesStatusMasterMongoDB> NextStatus { get; set; } = new List<ExpensesStatusMasterMongoDB>();

View File

@ -1,4 +1,5 @@
using Marco.Pms.Model.Dtos.Attendance;
using Marco.Pms.Model.ViewModels.Activities;
namespace Marco.Pms.Model.ViewModels.AttendanceVM
{
@ -6,15 +7,20 @@ namespace Marco.Pms.Model.ViewModels.AttendanceVM
{
public Guid Id { get; set; }
public Guid EmployeeId { get; set; }
public Guid ProjectId { get; set; }
public string? FirstName { get; set; }
public string? LastName { get; set; }
public string? EmployeeAvatar { get; set; }
public string? OrganizationName { get; set; }
public string? ProjectName { get; set; }
public DateTime? CheckInTime { get; set; }
public DateTime? CheckOutTime { get; set; }
public DateTime? RequestedAt { get; set; }
public DateTime? ApprovedAt { get; set; }
public string? JobRoleName { get; set; }
public ATTENDANCE_MARK_TYPE Activity { get; set; }
public BasicEmployeeVM? Approver { get; set; }
public BasicEmployeeVM? RequestedBy { get; set; }
public Guid? DocumentId { get; set; }
public string? ThumbPreSignedUrl { get; set; }
public string? PreSignedUrl { get; set; }

View File

@ -19,6 +19,7 @@ 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; }

View File

@ -18,6 +18,7 @@ namespace Marco.Pms.Model.ViewModels.Expanses
public DateTime TransactionDate { get; set; }
public DateTime CreatedAt { get; set; }
public string SupplerName { get; set; } = string.Empty;
public string? ExpenseUId { get; set; }
public string Description { get; set; } = string.Empty;
public string TransactionId { get; set; } = string.Empty;
public double Amount { get; set; }

View File

@ -11,6 +11,7 @@ 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,15 +7,18 @@
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 CompletionStatus { get; set; }
public double TaskPercentage { 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

@ -1,4 +1,5 @@
using Marco.Pms.DataAccess.Data;
using AutoMapper;
using Marco.Pms.DataAccess.Data;
using Marco.Pms.Model.AttendanceModule;
using Marco.Pms.Model.Dtos.Attendance;
using Marco.Pms.Model.Employees;
@ -6,6 +7,7 @@ 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;
using Marco.Pms.Services.Hubs;
using Marco.Pms.Services.Service;
@ -28,48 +30,41 @@ namespace MarcoBMS.Services.Controllers
public class AttendanceController : ControllerBase
{
private readonly ApplicationDbContext _context;
private readonly EmployeeHelper _employeeHelper;
private readonly IProjectServices _projectServices;
private readonly IServiceScopeFactory _serviceScopeFactory;
private readonly UserHelper _userHelper;
private readonly S3UploadService _s3Service;
private readonly PermissionServices _permission;
private readonly ILoggingService _logger;
private readonly IHubContext<MarcoHub> _signalR;
private readonly IFirebaseService _firebase;
private readonly Guid tenantId;
private readonly IMapper _mapper;
public AttendanceController(
ApplicationDbContext context, EmployeeHelper employeeHelper, IProjectServices projectServices, UserHelper userHelper,
S3UploadService s3Service, ILoggingService logger, PermissionServices permission, IHubContext<MarcoHub> signalR, IFirebaseService firebase)
ApplicationDbContext context,
UserHelper userHelper,
IServiceScopeFactory serviceScopeFactory,
ILoggingService logger,
PermissionServices permission,
IMapper mapper)
{
_context = context;
_employeeHelper = employeeHelper;
_projectServices = projectServices;
_serviceScopeFactory = serviceScopeFactory;
_userHelper = userHelper;
_s3Service = s3Service;
_logger = logger;
_permission = permission;
_signalR = signalR;
_firebase = firebase;
}
private Guid GetTenantId()
{
return _userHelper.GetTenantId();
//var tenant = User.FindFirst("TenantId")?.Value;
//return (tenant != null ? Convert.ToInt32(tenant) : 1);
_mapper = mapper;
tenantId = userHelper.GetTenantId();
}
[HttpGet("log/attendance/{attendanceid}")]
public async Task<IActionResult> GetAttendanceLogById(Guid attendanceid)
{
Guid TenantId = GetTenantId();
using var scope = _serviceScopeFactory.CreateScope();
var _s3Service = scope.ServiceProvider.GetRequiredService<S3UploadService>();
List<AttendanceLog> lstAttendance = await _context.AttendanceLogs
.Include(a => a.Document)
.Include(a => a.Employee)
.Include(a => a.UpdatedByEmployee)
.Where(c => c.AttendanceId == attendanceid && c.TenantId == TenantId)
.Where(c => c.AttendanceId == attendanceid && c.TenantId == tenantId)
.ToListAsync();
List<AttendanceLogVM> attendanceLogVMs = new List<AttendanceLogVM>();
@ -85,30 +80,42 @@ namespace MarcoBMS.Services.Controllers
}
[HttpGet("log/employee/{employeeId}")]
public async Task<IActionResult> GetAttendanceLogByEmployeeId(Guid employeeId, [FromQuery] string? dateFrom = null, [FromQuery] string? dateTo = null)
public async Task<IActionResult> GetAttendanceLogByEmployeeId(Guid employeeId, [FromQuery] DateTime? dateFrom = null, [FromQuery] DateTime? dateTo = null)
{
Guid TenantId = GetTenantId();
DateTime fromDate = new DateTime();
DateTime toDate = new DateTime();
if (dateFrom != null && DateTime.TryParse(dateFrom, out fromDate) == false)
{
_logger.LogWarning("User sent Invalid from Date while featching attendance logs");
return BadRequest(ApiResponse<object>.ErrorResponse("Invalid Date", "Invalid Date", 400));
}
if (dateTo != null && DateTime.TryParse(dateTo, out toDate) == false)
{
_logger.LogWarning("User sent Invalid to Date while featching attendance logs");
return BadRequest(ApiResponse<object>.ErrorResponse("Invalid Date", "Invalid Date", 400));
}
if (employeeId == Guid.Empty)
{
_logger.LogWarning("The employee Id sent by user is empty");
return BadRequest(ApiResponse<object>.ErrorResponse("Employee ID is required and must not be Empty.", "Employee ID is required and must not be empty.", 400));
}
List<Attendance> attendances = await _context.Attendes.Where(c => c.EmployeeId == employeeId && c.TenantId == TenantId && c.AttendanceDate.Date >= fromDate && c.AttendanceDate.Date <= toDate).ToListAsync();
Employee? employee = await _context.Employees.Include(e => e.JobRole).FirstOrDefaultAsync(e => e.Id == employeeId && e.TenantId == TenantId && e.IsActive);
Employee? employee = await _context.Employees.Include(e => e.JobRole).FirstOrDefaultAsync(e => e.Id == employeeId && e.TenantId == tenantId);
if (employee == null)
{
_logger.LogWarning("Employee {EmployeeId} not found", employeeId);
return NotFound(ApiResponse<object>.ErrorResponse("Employee not found", "Employee not found in database", 404));
}
if (!dateFrom.HasValue)
{
dateFrom = DateTime.UtcNow;
}
if (!dateTo.HasValue)
{
var days = 0 - 7;
dateTo = dateFrom.Value.AddDays(days);
}
List<Attendance> attendances = await _context.Attendes
.Include(a => a.RequestedBy)
.ThenInclude(e => e!.JobRole)
.Include(a => a.RequestedBy)
.ThenInclude(e => e!.JobRole)
.Where(c => c.EmployeeId == employeeId && c.TenantId == tenantId && c.AttendanceDate.Date >= dateFrom && c.AttendanceDate.Date <= dateTo).ToListAsync();
var projectIds = attendances.Select(a => a.ProjectID).Distinct().ToList();
var projects = await _context.Projects.Where(p => projectIds.Contains(p.Id) && p.TenantId == tenantId).ToListAsync();
List<EmployeeAttendanceVM> results = new List<EmployeeAttendanceVM>();
if (employee != null)
@ -121,11 +128,17 @@ namespace MarcoBMS.Services.Controllers
EmployeeId = employee.Id,
FirstName = employee.FirstName,
LastName = employee.LastName,
ProjectId = attendance.ProjectID,
ProjectName = projects.Where(p => p.Id == attendance.ProjectID).Select(p => p.Name).FirstOrDefault(),
CheckInTime = attendance.InTime,
CheckOutTime = attendance.OutTime,
JobRoleName = employee.JobRole != null ? employee.JobRole.Name : "",
Activity = attendance.Activity,
EmployeeAvatar = null
EmployeeAvatar = null,
RequestedAt = attendance.RequestedAt,
RequestedBy = _mapper.Map<BasicEmployeeVM>(attendance.RequestedBy),
ApprovedAt = attendance.ApprovedAt,
Approver = _mapper.Map<BasicEmployeeVM>(attendance.Approver)
};
results.Add(result);
}
@ -145,11 +158,12 @@ 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)
{
Guid tenantId = GetTenantId();
var LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
using var scope = _serviceScopeFactory.CreateScope();
var _projectServices = scope.ServiceProvider.GetRequiredService<IProjectServices>();
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
var project = await _context.Projects.AsNoTracking().FirstOrDefaultAsync(p => p.Id == projectId && p.TenantId == tenantId);
if (project == null)
@ -158,13 +172,13 @@ namespace MarcoBMS.Services.Controllers
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);
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);
_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));
}
@ -197,7 +211,13 @@ namespace MarcoBMS.Services.Controllers
if (hasTeamAttendancePermission)
{
List<Attendance> lstAttendance = await _context.Attendes.Where(c => c.ProjectID == projectId && c.AttendanceDate.Date >= fromDate.Date && c.AttendanceDate.Date <= toDate.Date && c.TenantId == tenantId).ToListAsync();
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<ProjectAllocation> projectteam = await _projectServices.GetTeamByProject(tenantId, projectId, organizationId, true);
@ -209,7 +229,11 @@ namespace MarcoBMS.Services.Controllers
Id = attendance.Id,
CheckInTime = attendance.InTime,
CheckOutTime = attendance.OutTime,
Activity = attendance.Activity
Activity = attendance.Activity,
ApprovedAt = attendance.ApprovedAt,
Approver = _mapper.Map<BasicEmployeeVM>(attendance.Approver),
RequestedAt = attendance.RequestedAt,
RequestedBy = _mapper.Map<BasicEmployeeVM>(attendance.RequestedBy)
};
teamMember = projectteam.Find(x => x.EmployeeId == attendance.EmployeeId);
if (teamMember != null)
@ -222,6 +246,8 @@ namespace MarcoBMS.Services.Controllers
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
{
@ -239,13 +265,23 @@ namespace MarcoBMS.Services.Controllers
else if (hasSelfAttendancePermission)
{
List<Attendance> lstAttendances = await _context.Attendes
.Where(c => c.ProjectID == projectId && c.EmployeeId == LoggedInEmployee.Id && c.AttendanceDate.Date >= fromDate.Date && c.AttendanceDate.Date <= toDate.Date && c.TenantId == tenantId)
.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)
.Where(pa => pa.ProjectId == projectId && pa.EmployeeId == LoggedInEmployee.Id && pa.TenantId == tenantId && pa.IsActive);
.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);
if (organizationId.HasValue)
{
@ -267,9 +303,15 @@ namespace MarcoBMS.Services.Controllers
LastName = projectAllocation.Employee?.LastName,
JobRoleName = projectAllocation.Employee?.JobRole?.Name,
OrganizationName = projectAllocation.Employee?.Organization?.Name,
ProjectId = attendance.ProjectID,
ProjectName = projectAllocation.Project?.Name,
CheckInTime = attendance.InTime,
CheckOutTime = attendance.OutTime,
Activity = attendance.Activity
Activity = attendance.Activity,
ApprovedAt = attendance.ApprovedAt,
Approver = _mapper.Map<BasicEmployeeVM>(attendance.Approver),
RequestedAt = attendance.RequestedAt,
RequestedBy = _mapper.Map<BasicEmployeeVM>(attendance.RequestedBy)
};
result.Add(result1);
}
@ -291,7 +333,6 @@ namespace MarcoBMS.Services.Controllers
/// <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)
{
var tenantId = GetTenantId();
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
// --- 1. Initial Validation and Permission Checks ---
@ -353,7 +394,9 @@ namespace MarcoBMS.Services.Controllers
[HttpGet("regularize")]
public async Task<IActionResult> GetRequestRegularizeAttendance([FromQuery] Guid projectId, [FromQuery] Guid? organizationId, [FromQuery] bool IncludeInActive)
{
Guid TenantId = GetTenantId();
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);
@ -364,11 +407,16 @@ namespace MarcoBMS.Services.Controllers
return Unauthorized(ApiResponse<object>.ErrorResponse("Unauthorized access", "Unauthorized access", 404));
}
List<Attendance> lstAttendance = await _context.Attendes.Where(c => c.ProjectID == projectId && c.Activity == ATTENDANCE_MARK_TYPE.REQUEST_REGULARIZE && c.TenantId == TenantId).ToListAsync();
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.Activity == ATTENDANCE_MARK_TYPE.REQUEST_REGULARIZE && c.TenantId == tenantId)
.ToListAsync();
List<ProjectAllocation> projectteam = await _projectServices.GetTeamByProject(TenantId, projectId, organizationId, true);
List<ProjectAllocation> projectteam = await _projectServices.GetTeamByProject(tenantId, projectId, organizationId, true);
var idList = projectteam.Select(p => p.EmployeeId).ToList();
var jobRole = await _context.JobRoles.ToListAsync();
foreach (Attendance attende in lstAttendance)
{
@ -386,7 +434,13 @@ namespace MarcoBMS.Services.Controllers
FirstName = teamMember.Employee.FirstName,
LastName = teamMember.Employee.LastName,
JobRoleName = teamMember.Employee.JobRole.Name,
OrganizationName = teamMember.Employee.Organization?.Name
OrganizationName = teamMember.Employee.Organization?.Name,
ProjectId = projectId,
ProjectName = teamMember.Project?.Name,
ApprovedAt = attende.ApprovedAt,
Approver = _mapper.Map<BasicEmployeeVM>(attende.Approver),
RequestedAt = attende.RequestedAt,
RequestedBy = _mapper.Map<BasicEmployeeVM>(attende.RequestedBy)
};
result.Add(result1);
}
@ -416,13 +470,17 @@ namespace MarcoBMS.Services.Controllers
return BadRequest(ApiResponse<object>.ErrorResponse("Invalid data", errors, 400));
}
Guid TenantId = GetTenantId();
var currentEmployee = await _userHelper.GetCurrentEmployeeAsync();
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();
try
{
Attendance? attendance = await _context.Attendes.FirstOrDefaultAsync(a => a.Id == recordAttendanceDot.Id && a.TenantId == TenantId); ;
Attendance? attendance = await _context.Attendes.FirstOrDefaultAsync(a => a.Id == recordAttendanceDot.Id && a.TenantId == tenantId); ;
if (recordAttendanceDot.MarkTime == null)
{
@ -450,10 +508,6 @@ 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)
@ -464,6 +518,8 @@ namespace MarcoBMS.Services.Controllers
{
attendance.OutTime = finalDateTime;
attendance.Activity = ATTENDANCE_MARK_TYPE.REQUEST_REGULARIZE;
attendance.RequestedById = currentEmployee.Id;
attendance.RequestedAt = DateTime.UtcNow;
}
else
{
@ -477,12 +533,15 @@ namespace MarcoBMS.Services.Controllers
attendance.IsApproved = true;
attendance.Activity = ATTENDANCE_MARK_TYPE.REGULARIZE;
attendance.ApprovedById = currentEmployee.Id;
attendance.ApprovedAt = DateTime.UtcNow;
// do nothing
}
else if (recordAttendanceDot.Action == ATTENDANCE_MARK_TYPE.REGULARIZE_REJECT)
{
attendance.IsApproved = false;
attendance.Activity = ATTENDANCE_MARK_TYPE.REGULARIZE_REJECT;
attendance.ApprovedById = currentEmployee.Id;
attendance.ApprovedAt = DateTime.UtcNow;
// do nothing
}
attendance.Date = DateTime.UtcNow;
@ -493,7 +552,7 @@ namespace MarcoBMS.Services.Controllers
else
{
attendance = new Attendance();
attendance.TenantId = TenantId;
attendance.TenantId = tenantId;
attendance.AttendanceDate = recordAttendanceDot.Date;
// attendance.Activity = recordAttendanceDot.Action;
attendance.Comment = recordAttendanceDot.Comment;
@ -525,7 +584,7 @@ namespace MarcoBMS.Services.Controllers
Latitude = recordAttendanceDot.Latitude,
Longitude = recordAttendanceDot.Longitude,
TenantId = TenantId,
TenantId = tenantId,
UpdatedBy = currentEmployee.Id,
UpdatedOn = recordAttendanceDot.Date
};
@ -572,7 +631,7 @@ namespace MarcoBMS.Services.Controllers
var name = $"{vm.FirstName} {vm.LastName}";
await _firebase.SendAttendanceMessageAsync(attendance.ProjectID, name, recordAttendanceDot.Action, attendance.EmployeeId, TenantId);
await _firebase.SendAttendanceMessageAsync(attendance.ProjectID, name, recordAttendanceDot.Action, attendance.EmployeeId, tenantId);
});
@ -608,7 +667,12 @@ namespace MarcoBMS.Services.Controllers
return BadRequest(ApiResponse<object>.ErrorResponse("Invalid data", errors, 400));
}
Guid tenantId = GetTenantId();
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 loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
var batchId = Guid.NewGuid();
@ -672,6 +736,8 @@ namespace MarcoBMS.Services.Controllers
{
attendance.OutTime = finalDateTime;
attendance.Activity = ATTENDANCE_MARK_TYPE.REQUEST_REGULARIZE;
attendance.RequestedById = loggedInEmployee.Id;
attendance.RequestedAt = DateTime.UtcNow;
}
else
{
@ -682,10 +748,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;
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;
break;
}
@ -818,6 +888,7 @@ namespace MarcoBMS.Services.Controllers
// 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)
@ -834,7 +905,12 @@ namespace MarcoBMS.Services.Controllers
query = query.Where(pa => pa.Employee != null && pa.Employee.OrganizationId == organizationId);
}
List<Attendance> lstAttendance = await _context.Attendes.Where(c => c.ProjectID == projectId && c.AttendanceDate.Date == forDate && c.TenantId == tenantId).ToListAsync();
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();
var teamAttendance = await query
.AsNoTracking()
@ -851,11 +927,10 @@ namespace MarcoBMS.Services.Controllers
LastName = teamMember.Employee?.LastName,
OrganizationName = teamMember.Employee?.Organization?.Name,
JobRoleName = teamMember.Employee?.JobRole?.Name,
ProjectId = projectId,
ProjectName = teamMember.Project?.Name
};
//var member = emp.Where(e => e.Id == teamMember.EmployeeId);
var attendance = lstAttendance.Find(x => x.EmployeeId == teamMember.EmployeeId) ?? new Attendance();
if (attendance != null)
{
@ -863,6 +938,10 @@ namespace MarcoBMS.Services.Controllers
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);
}
return result1;
})
@ -881,12 +960,21 @@ namespace MarcoBMS.Services.Controllers
// 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 projectAllocationQuery = _context.ProjectAllocations
.Include(pa => pa.Project)
.Include(pa => pa.Employee)
.ThenInclude(e => e!.Organization)
.Where(pa => pa.ProjectId == projectId && pa.EmployeeId == employeeId && pa.TenantId == tenantId && pa.IsActive);
.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)
{
@ -906,9 +994,15 @@ namespace MarcoBMS.Services.Controllers
OrganizationName = projectAllocation.Employee?.Organization?.Name,
LastName = projectAllocation.Employee?.LastName,
JobRoleName = projectAllocation.Employee?.JobRole?.Name,
ProjectId = projectId,
ProjectName = projectAllocation.Project?.Name,
CheckInTime = lstAttendance.InTime,
CheckOutTime = lstAttendance.OutTime,
Activity = lstAttendance.Activity
Activity = lstAttendance.Activity,
ApprovedAt = lstAttendance.ApprovedAt,
Approver = _mapper.Map<BasicEmployeeVM>(lstAttendance.Approver),
RequestedAt = lstAttendance.RequestedAt,
RequestedBy = _mapper.Map<BasicEmployeeVM>(lstAttendance.RequestedBy)
};
result.Add(result1);
}

View File

@ -7,6 +7,7 @@ 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;
@ -319,36 +320,6 @@ namespace MarcoBMS.Services.Controllers
return Unauthorized(ApiResponse<object>.ErrorResponse("MPIN mismatch", "MPIN did not match", 401));
}
if (!string.IsNullOrWhiteSpace(verifyMPIN.FcmToken))
{
var existingFCMTokenMapping = await _context.FCMTokenMappings.Where(ft => ft.FcmToken == verifyMPIN.FcmToken).ToListAsync();
if (existingFCMTokenMapping.Any())
{
_context.FCMTokenMappings.RemoveRange(existingFCMTokenMapping);
}
var fcmTokenMapping = new FCMTokenMapping
{
EmployeeId = requestEmployee.Id,
FcmToken = verifyMPIN.FcmToken,
ExpiredAt = DateTime.UtcNow.AddDays(6),
TenantId = tenantId
};
_context.FCMTokenMappings.Add(fcmTokenMapping);
_logger.LogInfo("New FCM Token registering for employee {EmployeeId}", requestEmployee.Id);
try
{
await _context.SaveChangesAsync();
}
catch (Exception ex)
{
_logger.LogError(ex, "Exception occured while saving FCM Token for employee {EmployeeId}", requestEmployee.Id);
return StatusCode(500, ApiResponse<object>.ErrorResponse("Internal Error", ex.Message, 500));
}
}
// Generate new tokens
var jwtToken = _refreshTokenService.GenerateJwtToken(requestEmployee.Email, tenantId, requestEmployee.OrganizationId, _jwtSettings);
var refreshToken = await _refreshTokenService.CreateRefreshToken(requestEmployee.ApplicationUserId, tenantId.ToString(), requestEmployee.OrganizationId, _jwtSettings);
@ -368,6 +339,100 @@ namespace MarcoBMS.Services.Controllers
}
}
[HttpPost("login-otp/v1")]
public async Task<IActionResult> LoginWithOTP([FromBody] VerifyOTPDto verifyOTP)
{
await using var _context = await _dbContextFactory.CreateDbContextAsync();
using var scope = _serviceScopeFactory.CreateScope();
var _refreshTokenService = scope.ServiceProvider.GetRequiredService<RefreshTokenService>();
try
{
// Validate input
if (string.IsNullOrWhiteSpace(verifyOTP.Email) ||
string.IsNullOrWhiteSpace(verifyOTP.OTP) ||
verifyOTP.OTP.Length != 4 ||
!verifyOTP.OTP.All(char.IsDigit))
{
_logger.LogWarning("OTP login failed - invalid input provided");
return BadRequest(ApiResponse<object>.ErrorResponse("Invalid input", "Please provide a valid 4-digit OTP and Email", 400));
}
// Fetch employee by email
var requestEmployee = await _context.Employees
.Include(e => e.ApplicationUser)
.FirstOrDefaultAsync(e => e.Email == verifyOTP.Email && e.IsActive);
if (requestEmployee == null || string.IsNullOrWhiteSpace(requestEmployee.ApplicationUserId))
{
_logger.LogWarning("OTP login failed - user not found for email {Email}", verifyOTP.Email);
return NotFound(ApiResponse<object>.ErrorResponse("User not found", "User not found", 404));
}
Guid userId = Guid.Parse(requestEmployee.ApplicationUserId);
// Fetch most recent OTP
var otpDetails = await _context.OTPDetails
.Where(o => o.UserId == userId)
.OrderByDescending(o => o.TimeStamp)
.FirstOrDefaultAsync();
if (otpDetails == null)
{
_logger.LogWarning("OTP login failed - no OTP found for user {UserId}", userId);
return NotFound(ApiResponse<object>.ErrorResponse("OTP not found", "No OTP was generated for this user", 404));
}
// Validate OTP expiration
var validUntil = otpDetails.TimeStamp.AddSeconds(otpDetails.ExpriesInSec);
if (DateTime.UtcNow > validUntil || otpDetails.IsUsed)
{
_logger.LogWarning("OTP login failed - OTP expired for user {UserId}", userId);
return BadRequest(ApiResponse<object>.ErrorResponse("OTP expired", "The OTP has expired, please request a new one", 400));
}
// Match OTP
if (otpDetails.OTP != verifyOTP.OTP)
{
_logger.LogWarning("OTP login failed - incorrect OTP entered for user {UserId}", userId);
return Unauthorized(ApiResponse<object>.ErrorResponse("Invalid OTP", "OTP did not match", 401));
}
// Generate access and refresh tokens
//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 accessToken = _refreshTokenService.GenerateJwtToken(requestEmployee.ApplicationUser?.UserName,
tenant?.Id ?? Guid.Empty, requestEmployee.OrganizationId, _jwtSettings);
var refreshToken = await _refreshTokenService.CreateRefreshToken(requestEmployee.ApplicationUserId,
tenant?.Id.ToString(), requestEmployee.OrganizationId, _jwtSettings);
// Fetch MPIN token if exists
var mpinDetails = await _context.MPINDetails
.FirstOrDefaultAsync(p => p.UserId == userId);
// Build and return response
var response = new
{
token = accessToken,
refreshToken,
mpinToken = mpinDetails?.MPINToken
};
otpDetails.IsUsed = true;
await _context.SaveChangesAsync();
_logger.LogInfo("OTP login successful for employee {EmployeeId}", requestEmployee.Id);
return Ok(ApiResponse<object>.SuccessResponse(response, "User logged in successfully.", 200));
}
catch (Exception ex)
{
_logger.LogError(ex, "An unexpected error occurred during OTP login for email {Email}", verifyOTP.Email ?? string.Empty);
return StatusCode(500, ApiResponse<object>.ErrorResponse("Unexpected error", ex.Message, 500));
}
}
// new login APIs
[HttpPost("login")]
@ -944,7 +1009,7 @@ namespace MarcoBMS.Services.Controllers
}
[HttpPost("login-otp")]
public async Task<IActionResult> LoginWithOTP([FromBody] VerifyOTPDto verifyOTP)
public async Task<IActionResult> LoginWithOTPAsync([FromBody] VerifyOTPDto verifyOTP)
{
await using var _context = await _dbContextFactory.CreateDbContextAsync();
using var scope = _serviceScopeFactory.CreateScope();
@ -1405,6 +1470,9 @@ 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

@ -33,11 +33,11 @@ namespace Marco.Pms.Services.Controllers
#region =================================================================== Contact Get APIs ===================================================================
[HttpGet("list")]
public async Task<IActionResult> GetContactList([FromQuery] string? search, [FromQuery] string? filter, [FromQuery] Guid? projectId, [FromQuery] bool active = true,
public async Task<IActionResult> GetContactList([FromQuery] string? searchString, [FromQuery] string? filter, [FromQuery] Guid? projectId, [FromQuery] bool active = true,
[FromQuery] int pageNumber = 1, [FromQuery] int pageSize = 20)
{
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
var response = await _directoryService.GetListOfContactsAsync(search: search, filter: filter, projectId: projectId, active: active, pageSize: pageSize, pageNumber: pageNumber, tenantId, loggedInEmployee);
var response = await _directoryService.GetListOfContactsAsync(search: searchString, filter: filter, projectId: projectId, active: active, pageSize: pageSize, pageNumber: pageNumber, tenantId, loggedInEmployee);
return StatusCode(response.StatusCode, response);

View File

@ -329,10 +329,10 @@ namespace MarcoBMS.Services.Controllers
[HttpGet("basic")]
public async Task<IActionResult> GetEmployeesByProjectBasic(Guid? projectId, [FromQuery] string? searchString)
public async Task<IActionResult> GetEmployeesByProjectBasic(Guid? projectId, [FromQuery] string? searchString, [FromQuery] bool allEmployee)
{
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
var employeeQuery = _context.Employees.Where(e => e.TenantId == tenantId);
var employeeQuery = _context.Employees.Where(e => e.IsActive);
if (projectId != null && projectId != Guid.Empty)
{
var hasProjectPermission = await _permission.HasProjectPermission(loggedInEmployee, projectId.Value);
@ -344,13 +344,26 @@ namespace MarcoBMS.Services.Controllers
var employeeIds = await _context.ProjectAllocations.Where(pa => pa.ProjectId == projectId && pa.IsActive && pa.TenantId == tenantId).Select(p => p.EmployeeId).ToListAsync();
employeeQuery = employeeQuery.Where(e => employeeIds.Contains(e.Id));
}
else
{
employeeQuery = employeeQuery.Where(e => e.OrganizationId == organizationId);
}
if (!string.IsNullOrWhiteSpace(searchString))
{
var searchStringLower = searchString.ToLower();
employeeQuery = employeeQuery.Where(e => (e.FirstName + " " + e.LastName).ToLower().Contains(searchStringLower));
}
var response = await employeeQuery.Take(10).Select(e => _mapper.Map<BasicEmployeeVM>(e)).ToListAsync();
var query = employeeQuery.OrderBy(e => e.FirstName);
if (!allEmployee)
{
query = (IOrderedQueryable<Employee>)query.Take(10);
}
var response = await query
.Select(e => _mapper.Map<BasicEmployeeVM>(e))
.ToListAsync();
return Ok(ApiResponse<object>.SuccessResponse(response, $"{response.Count} records of employees fetched successfully", 200));
}
@ -621,10 +634,10 @@ namespace MarcoBMS.Services.Controllers
if (model.Id.HasValue && model.Id.Value != Guid.Empty)
{
existingEmployee = await _context.Employees
.FirstOrDefaultAsync(e => e.Id == model.Id && e.OrganizationId == model.OrganizationId);
.FirstOrDefaultAsync(e => e.Id == model.Id);
if (existingEmployee == null)
{
_logger.LogInfo("Employee not found for update. Id={EmployeeId}, Org={OrgId}", model.Id, model.OrganizationId);
_logger.LogInfo("Employee not found for update. Id={EmployeeId}", model.Id);
return NotFound(ApiResponse<object>.ErrorResponse("Employee not found", "Employee not found in database", 404));
}
}
@ -676,10 +689,10 @@ namespace MarcoBMS.Services.Controllers
if (!string.IsNullOrWhiteSpace(model.Email))
{
var emailExists = await _context.Employees
.AnyAsync(e => e.Email == model.Email && e.OrganizationId == model.OrganizationId);
.AnyAsync(e => e.Email == 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. Email={Email}", model.Email);
return StatusCode(403, ApiResponse<object>.ErrorResponse(
"Employee with email already exists",
"Employee with this email already exists", 403));
@ -910,9 +923,9 @@ namespace MarcoBMS.Services.Controllers
var emailExists = await _context.Employees
.AnyAsync(e => e.Email == model.Email && e.OrganizationId == model.OrganizationId);
if (emailExists)
if (emailExists && !string.IsNullOrWhiteSpace(model.Email))
{
_logger.LogInfo("Employee email already exists in org. Email={Email}, Org={OrgId}", model.Email ?? string.Empty, model.OrganizationId);
_logger.LogInfo("Employee email already exists in org. Email={Email}, Org={OrgId}", model.Email, model.OrganizationId);
return StatusCode(409, ApiResponse<object>.ErrorResponse("Employee with email already exists", "Employee with this email already exists", 409));
}
@ -982,7 +995,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 && e.TenantId == tenantId); // tenant-safe lookup
.FirstOrDefaultAsync(e => e.Id == employeeId); // tenant-safe lookup
if (existingEmployee is null)
{
@ -1003,7 +1016,7 @@ namespace MarcoBMS.Services.Controllers
if (string.IsNullOrWhiteSpace(existingEmployee.Email) && !string.IsNullOrWhiteSpace(model.Email))
{
var emailExists = await _context.Employees
.AnyAsync(e => e.Email == model.Email && e.OrganizationId == model.OrganizationId);
.AnyAsync(e => e.Email == model.Email);
if (emailExists)
{
@ -1241,7 +1254,6 @@ namespace MarcoBMS.Services.Controllers
}
// Prepare reset link sender helper
private async Task SendResetIfApplicableAsync(ApplicationUser u, string firstName)
{

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,6 +9,7 @@ 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;
@ -30,6 +31,7 @@ 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");
@ -47,6 +49,7 @@ 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 ===================================================================
@ -669,6 +672,8 @@ 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)
@ -717,34 +722,36 @@ namespace Marco.Pms.Services.Controllers
return NotFound(ApiResponse<object>.ErrorResponse("Organization not found", "Organization not found in database", 404));
}
if (organizationTenantMapping == null)
{
// Create new tenant-organization mapping if none exists
var newMapping = new TenantOrgMapping
{
OrganizationId = organization.Id,
SPRID = organization.SPRID,
AssignedDate = DateTime.UtcNow,
IsActive = true,
AssignedById = loggedInEmployee.Id,
TenantId = tenantId
};
_context.TenantOrgMappings.Add(newMapping);
await _context.SaveChangesAsync();
await transaction.CommitAsync();
_logger.LogInfo("Assigned organization {OrganizationId} to tenant {TenantId} successfully.", organizationId, tenantId);
}
else
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.CommitAsync();
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
{
OrganizationId = organization.Id,
SPRID = organization.SPRID,
AssignedDate = DateTime.UtcNow,
IsActive = true,
AssignedById = loggedInEmployee.Id,
TenantId = tenantId
};
_context.TenantOrgMappings.Add(newMapping);
await _context.SaveChangesAsync();
await transaction.CommitAsync();
_logger.LogInfo("Assigned organization {OrganizationId} to tenant {TenantId} successfully.", organizationId, tenantId);
// Prepare response view model
var response = _mapper.Map<BasicOrganizationVm>(organization);
await AssignApplicationRoleToOrganization(organization.Id, tenantId);
return Ok(ApiResponse<object>.SuccessResponse(response, "Organization has been assigned to tenant", 200));
}
catch (DbUpdateException dbEx)
@ -938,45 +945,98 @@ namespace Marco.Pms.Services.Controllers
#endregion
#region =================================================================== Helper Functions ===================================================================
//private ServicesProviderFilter? TryDeserializeServicesProviderFilter(string? filter)
//{
// if (string.IsNullOrWhiteSpace(filter))
// {
// return null;
// }
// var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true };
// ServicesProviderFilter? documentFilter = 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();
// 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 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";
// // 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 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);
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,6 +560,13 @@ 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()
public async Task<IActionResult> GetAttendanceReportAsync([FromQuery] bool isCurrentMonth = false)
{
Guid tenantId = _userHelper.GetTenantId();
using var scope = _serviceScopeFactory.CreateScope();
@ -458,48 +458,75 @@ 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 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 attendancesTask = Task.Run(async () =>
{
await using var context = await _dbContextFactory.CreateDbContextAsync();
return await context.Attendes
.Where(a => a.AttendanceDate >= firstDayOfMonth && a.AttendanceDate < firstDayOfNextMonth && a.Employee != null && a.TenantId == tenantId)
.GroupBy(a => a.ProjectID)
.ToListAsync();
});
var projectAllocationTask = Task.Run(async () =>
{
await using var context = await _dbContextFactory.CreateDbContextAsync();
return await context.ProjectAllocations
.Include(pa => pa.Employee)
.Where(pa => pa.TenantId == tenantId && pa.IsActive)
.ToListAsync();
});
await Task.WhenAll(attendancesTask, projectAllocationTask);
var attendances = attendancesTask.Result;
var projectAllocations = projectAllocationTask.Result;
var result = attendances.Select(g =>
{
var projectAllocation = projectAllocations.Where(pa => pa.ProjectId == g.Key && pa.Employee != null).ToList();
var projectAttendance = projectAllocation.Select(pa =>
{
var attendances = g.Where(a => a.EmployeeId == pa.EmployeeId).ToList();
var attendanceDate = attendances.Select(a => a.AttendanceDate.Date).ToList();
return new
{
FirstName = pa.Employee!.FirstName,
LastName = pa.Employee.LastName,
Attendances = allDates.Select(d =>
{
var attendance = attendances.FirstOrDefault(a => a.AttendanceDate.Date == d);
return new
{
AttendanceDate = d,
CheckIn = attendance?.InTime,
CheckOut = attendance?.OutTime,
Activity = attendance?.Activity,
IsApproved = attendance?.ApprovedById.HasValue,
};
}).ToList(),
CheckInCheckOutDone = attendances.Where(a => a.InTime.HasValue && a.OutTime.HasValue && a.Activity == ATTENDANCE_MARK_TYPE.REGULARIZE).Count(),
CheckInDone = attendances.Where(a => a.InTime.HasValue).Count(),
CheckOutPending = attendances.Where(a => a.InTime.HasValue && !a.OutTime.HasValue).Count(),
RejectedRegularize = attendances.Where(a => a.Activity == ATTENDANCE_MARK_TYPE.REGULARIZE_REJECT).Count(),
AbsentAttendance = allDates.Where(d => !attendanceDate.Contains(d) && d.DayOfWeek != DayOfWeek.Sunday).Count()
};
}).OrderBy(ar => ar.FirstName).ThenBy(ar => ar.LastName).ToList();
return new
{
ProjectName = _context.Projects.Where(p => p.Id == g.Key && p.TenantId == tenantId).Select(p => p.Name).FirstOrDefault(),
ProjectAttendance = g.GroupBy(a => a.Employee).Select(gp =>
{
var attendanceDate = gp.Select(a => a.AttendanceDate.Date).ToList();
return new
{
FirstName = gp.Key!.FirstName,
LastName = gp.Key.LastName,
Attendances = allDates.Select(d =>
{
var attendance = gp.FirstOrDefault(a => a.AttendanceDate.Date == d);
return new
{
AttendanceDate = d,
CheckIn = attendance?.InTime,
CheckOut = attendance?.OutTime,
Activity = attendance?.Activity,
IsApproved = attendance?.ApprovedById.HasValue,
};
}).ToList(),
CheckInCheckOutDone = gp.Where(a => a.InTime.HasValue && a.OutTime.HasValue && a.Activity == ATTENDANCE_MARK_TYPE.REGULARIZE).Count(),
CheckInDone = gp.Where(a => a.InTime.HasValue).Count(),
CheckOutPending = gp.Where(a => a.InTime.HasValue && !a.OutTime.HasValue).Count(),
RejectedRegularize = gp.Where(a => a.Activity == ATTENDANCE_MARK_TYPE.REGULARIZE_REJECT).Count(),
AbsentAttendance = allDates.Where(d => !attendanceDate.Contains(d) && d.DayOfWeek != DayOfWeek.Sunday).Count()
};
}).OrderBy(ar => ar.FirstName).ThenBy(ar => ar.LastName).ToList()
ProjectAttendance = projectAttendance
};
}).ToList();

View File

@ -482,6 +482,13 @@ namespace MarcoBMS.Services.Controllers
taskAllocationQuery = taskAllocationQuery.Where(t => t.WorkItem != null &&
taskFilter.ActivityIds.Contains(t.WorkItem.ActivityId));
}
if (taskFilter.ServiceIds?.Any() ?? false)
{
taskAllocationQuery = taskAllocationQuery.Where(t => t.WorkItem != null &&
t.WorkItem.ActivityMaster != null &&
t.WorkItem.ActivityMaster.ActivityGroup != null &&
taskFilter.ServiceIds.Contains(t.WorkItem.ActivityMaster.ActivityGroup.ServiceId));
}
if (taskFilter.dateFrom.HasValue && taskFilter.dateTo.HasValue)
{
taskAllocationQuery = taskAllocationQuery.Where(t => t.AssignmentDate.Date >= taskFilter.dateFrom.Value.Date &&
@ -745,6 +752,97 @@ namespace MarcoBMS.Services.Controllers
return Ok(ApiResponse<object>.SuccessResponse(taskVM, "Success", 200));
}
[HttpGet("filter/{projectId}")]
public async Task<IActionResult> GetTaskFilterObject(Guid projectId)
{
// Get the current tenant from claims/context
Guid tenantId = GetTenantId();
// Log API invocation with the project and tenant for traceability
_logger.LogInfo("Fetching filter objects for ProjectId={ProjectId}, TenantId={TenantId}", projectId, tenantId);
try
{
// AsNoTracking for improved performance—no intention to update these records
// Only fetch & project properties actually required (DTO projection)
var tasks = await _context.TaskAllocations
.Include(t => t.WorkItem)
.ThenInclude(wi => wi!.WorkArea)
.ThenInclude(wa => wa!.Floor)
.ThenInclude(f => f!.Building)
.Include(t => t.WorkItem)
.ThenInclude(wi => wi!.ActivityMaster)
.ThenInclude(a => a!.ActivityGroup)
.ThenInclude(ag => ag!.Service)
.Where(t => t.WorkItem != null &&
t.WorkItem.WorkArea != null &&
t.WorkItem.WorkArea.Floor != null &&
t.WorkItem.WorkArea.Floor.Building != null &&
t.WorkItem.WorkArea.Floor.Building.ProjectId == projectId &&
t.TenantId == tenantId).ToListAsync();
// Distinct by Id (since projection doesn't guarantee uniqueness across different allocations)
var buildings = tasks.Where(t => t.WorkItem != null && t.WorkItem.WorkArea != null && t.WorkItem.WorkArea.Floor != null && t.WorkItem.WorkArea.Floor.Building != null)
.Select(t => t.WorkItem!.WorkArea!.Floor!.Building!)
.Select(b => new
{
Id = b.Id,
Name = b.Name
}).Distinct().ToList();
var floors = tasks.Where(t => t.WorkItem != null && t.WorkItem.WorkArea != null && t.WorkItem.WorkArea.Floor != null)
.Select(t => t.WorkItem!.WorkArea!.Floor!)
.Select(f => new
{
Id = f.Id,
Name = f.FloorName,
BuildingId = f.BuildingId
}).Distinct().ToList();
var activities = tasks.Where(t => t.WorkItem != null &&
t.WorkItem.ActivityMaster != null &&
t.WorkItem.ActivityMaster.ActivityGroup != null &&
t.WorkItem.ActivityMaster.ActivityGroup.Service != null)
.Select(t => t.WorkItem!.ActivityMaster!)
.Select(a => new
{
Id = a.Id,
Name = a.ActivityName
}).Distinct().ToList();
var services = tasks.Where(t => t.WorkItem != null &&
t.WorkItem.ActivityMaster != null &&
t.WorkItem.ActivityMaster.ActivityGroup != null &&
t.WorkItem.ActivityMaster.ActivityGroup.Service != null)
.Select(t => t.WorkItem!.ActivityMaster!.ActivityGroup!.Service!)
.Select(s => new
{
Id = s.Id,
Name = s.Name
}).Distinct().ToList();
var response = new
{
Buildings = buildings,
Floors = floors,
Activities = activities,
Services = services
};
_logger.LogInfo("Successfully fetched filter objects for ProjectId={ProjectId}, TenantId={TenantId}", projectId, tenantId);
// Use DTO in API response for clarity and easier frontend usage
return Ok(ApiResponse<object>.SuccessResponse(response, "Filter object for task fetched successfully", 200));
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to fetch filter objects for ProjectId={ProjectId}, TenantId={TenantId}", projectId, tenantId);
// Return a standard error result
return StatusCode(500, ApiResponse<object>.ErrorResponse("Failed to fetch filter object.", 500));
}
}
/// <summary>
/// Approves a reported task after validation, updates status, and stores attachments/comments.
/// </summary>

View File

@ -1013,10 +1013,6 @@ namespace Marco.Pms.Services.Controllers
try
{
_ = Task.Run(async () =>
{
await ClearPermissionForTenant();
});
var features = await _featureDetailsHelper.GetFeatureDetails(subscriptionPlan.FeaturesId);
if (features == null)
{
@ -1069,7 +1065,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.TenantId == model.TenantId);
.FirstOrDefaultAsync(e => e.ApplicationUser != null && (e.ApplicationUser.IsRootUser ?? false) && e.OrganizationId == tenant.OrganizationId);
if (rootEmployee == null)
{
@ -1127,6 +1123,9 @@ 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)
@ -1325,10 +1324,6 @@ 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);
@ -1363,7 +1358,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.TenantId == model.TenantId);
.FirstOrDefaultAsync(e => e.ApplicationUser != null && (e.ApplicationUser.IsRootUser ?? false) && e.OrganizationId == tenant.OrganizationId);
if (rootEmployee == null)
{
@ -1374,7 +1369,8 @@ namespace Marco.Pms.Services.Controllers
var rootRoleId = await context.EmployeeRoleMappings
.AsNoTracking()
.Where(er => er.EmployeeId == rootEmployee.Id && er.TenantId == model.TenantId)
.Include(er => er.Role)
.Where(er => er.EmployeeId == rootEmployee.Id && er.TenantId == model.TenantId && er.Role != null && er.Role.Role == "Super User")
.Select(er => er.RoleId)
.FirstOrDefaultAsync();
@ -1439,6 +1435,9 @@ 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)
@ -1823,19 +1822,6 @@ 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,6 +956,28 @@ 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

@ -190,6 +190,7 @@ 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);
@ -263,14 +264,17 @@ 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.CompletionStatus = totalPlannedWork > 0 ? totalCompletedWork / totalPlannedWork : 0;
statisticReport.AttendancePercentage = totalCompletedTask > 0 ? (totalCompletedTask / totalPlannedTask) * 100 : 0;
statisticReport.TodaysAssignTasks = todayAssignedTasks.Count;
statisticReport.TodaysCompletedTasks = todaysCompletedTasks.Count;
statisticReport.ReportPending = reportPending.Count;
statisticReport.TeamOnSite = teamOnSite;
statisticReport.PerformedTasks = performedTasks;

View File

@ -38,7 +38,7 @@ namespace MarcoBMS.Services.Helpers
// This is safe because the query is not executed yet.
var employeeRoleIdsQuery = _context.EmployeeRoleMappings
.AsNoTracking()
.Where(erm => erm.EmployeeId == EmployeeId && erm.IsEnabled)
.Where(erm => erm.EmployeeId == EmployeeId && erm.IsEnabled && erm.TenantId == tenantId)
.Select(erm => erm.RoleId);
// --- Step 2: Asynchronously update the cache using the DbContextFactory ---
@ -52,7 +52,7 @@ namespace MarcoBMS.Services.Helpers
// Now, re-create and execute the query using this new, isolated context.
var roleIds = await contextForCache.EmployeeRoleMappings
.AsNoTracking()
.Where(erm => erm.EmployeeId == EmployeeId && erm.IsEnabled)
.Where(erm => erm.EmployeeId == EmployeeId && erm.IsEnabled && erm.TenantId == tenantId)
.Select(erm => erm.RoleId)
.ToListAsync();

View File

@ -54,7 +54,7 @@ namespace MarcoBMS.Services.Helpers
{
var user = await GetCurrentUserAsync();
if (user == null) return new Employee { };
var Employee = await _context.Employees.AsNoTracking().Include(e => e.JobRole).FirstOrDefaultAsync(e => e.ApplicationUserId == user.Id && e.IsActive);
var Employee = await _context.Employees.AsNoTracking().Include(a => a.ApplicationUser).Include(e => e.JobRole).FirstOrDefaultAsync(e => e.ApplicationUserId == user.Id && e.IsActive);
return Employee ?? new Employee { };
}

View File

@ -1,10 +1,8 @@
using MarcoBMS.Services.Service;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.SignalR;
namespace Marco.Pms.Services.Hubs
{
[Authorize]
public class MarcoHub : Hub
{
private readonly ILoggingService _logger;

View File

@ -379,6 +379,27 @@ 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

@ -2271,9 +2271,11 @@ namespace Marco.Pms.Services.Service
return ApiResponse<object>.ErrorResponse("Note not found", "Note not found", 404);
}
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 (hasContactAccess)
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);
if (!hasAdminPermission && !hasContactAccess)
{
_logger.LogWarning("Employee {EmployeeId} does not have permission to delete contact {ContactId}",
loggedInEmployee.Id, note.ContactId);

View File

@ -23,6 +23,7 @@ using MarcoBMS.Services.Service;
using Microsoft.CodeAnalysis;
using Microsoft.EntityFrameworkCore;
using System.Text.Json;
using System.Text.RegularExpressions;
using Document = Marco.Pms.Model.DocumentManager.Document;
namespace Marco.Pms.Services.Service
@ -318,6 +319,13 @@ namespace Marco.Pms.Services.Service
{
status.PermissionIds = permissionStatusMappings.Where(ps => ps.StatusId == status.Id).Select(ps => ps.PermissionIds).FirstOrDefault();
}
int index = vm.NextStatus.FindIndex(ns => ns.DisplayName == "Reject");
if (index > -1)
{
var item = vm.NextStatus[index];
vm.NextStatus.RemoveAt(index);
vm.NextStatus.Insert(0, item);
}
}
vm.ExpensesReimburse = _mapper.Map<ExpensesReimburseVM>(expensesReimburse);
@ -487,6 +495,15 @@ namespace Marco.Pms.Services.Service
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
return await dbContext.PaymentModeMatser.AsNoTracking().FirstOrDefaultAsync(pm => pm.Id == dto.PaymentModeId);
});
var expenseUIdTask = Task.Run(async () =>
{
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
var result = await dbContext.Expenses
.Where(e => !string.IsNullOrWhiteSpace(e.ExpenseUId)).ToListAsync();
return result
.Select(e => ExtractNumber(e.ExpenseUId))
.OrderByDescending(id => id).FirstOrDefault();
});
var statusMappingTask = Task.Run(async () =>
{
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
@ -506,10 +523,7 @@ namespace Marco.Pms.Services.Service
// Await all prerequisite checks at once.
await Task.WhenAll(
hasUploadPermissionTask, hasProjectPermissionTask,
projectTask, expenseTypeTask, paymentModeTask, statusMappingTask, paidByTask
);
await Task.WhenAll(hasUploadPermissionTask, hasProjectPermissionTask, projectTask, expenseTypeTask, paymentModeTask, statusMappingTask, paidByTask, expenseUIdTask);
// 2. Aggregate and Check Results
if (!await hasUploadPermissionTask || !await hasProjectPermissionTask)
@ -519,11 +533,12 @@ namespace Marco.Pms.Services.Service
}
var validationErrors = new List<string>();
var project = await projectTask;
var expenseType = await expenseTypeTask;
var paymentMode = await paymentModeTask;
var statusMapping = await statusMappingTask;
var paidBy = await paidByTask;
var project = projectTask.Result;
var expenseType = expenseTypeTask.Result;
var paymentMode = paymentModeTask.Result;
var statusMapping = statusMappingTask.Result;
var paidBy = paidByTask.Result;
var lastExpenseUId = expenseUIdTask.Result;
if (project == null) validationErrors.Add("Project not found.");
if (paidBy == null) validationErrors.Add("Paid by employee not found");
@ -539,9 +554,10 @@ 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}";
expense.CreatedById = loggedInEmployee.Id;
expense.CreatedAt = DateTime.UtcNow;
expense.TenantId = tenantId;
@ -1084,6 +1100,13 @@ namespace Marco.Pms.Services.Service
#endregion
#region =================================================================== Helper Functions ===================================================================
private int ExtractNumber(string id)
{
// Extract trailing number; handles EX_0001, EX-0001, EX0001
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

File diff suppressed because it is too large Load Diff

View File

@ -716,7 +716,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 && pa.Service != null);
.Where(pa => pa.ProjectId == projectId && pa.TenantId == tenantId);
// Conditionally apply the filter for active allocations.
if (!includeInactive)
@ -738,6 +738,7 @@ namespace Marco.Pms.Services.Service
}
var allocations = await projectAllocationQuery
.Where(pa => pa.Service != null)
.Select(pa => new
{
// Fields from ProjectAllocation
@ -1012,6 +1013,12 @@ 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();
@ -1027,6 +1034,11 @@ 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)
{
@ -1048,11 +1060,13 @@ 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);
@ -2679,6 +2693,8 @@ 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
@ -2695,7 +2711,7 @@ namespace Marco.Pms.Services.Service
// Filter and map the data to the desired view model
var response = projectOrgMappings
.Where(po => po.Organization != null)
.Where(po => po.Organization != null && po.OrganizationType != null)
.Select(po => new ProjectOrganizationVM
{
Id = po.Organization!.Id,
@ -2704,6 +2720,7 @@ 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,
@ -2733,6 +2750,7 @@ namespace Marco.Pms.Services.Service
ContactPerson = pmc.ContactPerson,
SPRID = pmc.SPRID,
logoImage = pmc.logoImage,
OrganizationType = "PMC",
AssignedBy = assignedBy,
AssignedDate = assignedDate,
CompletionDate = completionDate
@ -2748,6 +2766,7 @@ namespace Marco.Pms.Services.Service
ContactPerson = promoter.ContactPerson,
SPRID = promoter.SPRID,
logoImage = promoter.logoImage,
OrganizationType = "Promotor",
AssignedBy = assignedBy,
AssignedDate = assignedDate,
CompletionDate = completionDate
@ -2763,6 +2782,7 @@ namespace Marco.Pms.Services.Service
ContactPerson = organization.ContactPerson,
SPRID = organization.SPRID,
logoImage = organization.logoImage,
OrganizationType = "Primary",
AssignedBy = assignedBy,
AssignedDate = assignedDate,
CompletionDate = completionDate
@ -2785,6 +2805,128 @@ 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
@ -2803,9 +2945,14 @@ 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.Employee)
.ThenInclude(e => e!.Organization)
.Where(pa => pa.TenantId == TenantId && pa.ProjectId == ProjectId);
.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);
if (!IncludeInactive)
{
projectAllocationQuery = projectAllocationQuery.Where(pa => pa.IsActive);

View File

@ -49,6 +49,7 @@ 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);
}
}