Compare commits

...

43 Commits

Author SHA1 Message Date
663d94093c Enhanced the employe log API 2025-10-10 10:01:21 +05:30
dc83aa72a6 Added the API to get filter task allocation 2025-10-09 16:57:04 +05:30
ef597b2bb7 Added the project ID in attendance VM 2025-10-09 11:28:06 +05:30
4acc61f03a Added the Requested by in attendance table 2025-10-08 17:54:37 +05:30
c47b0da7b8 Showing the list of all attendance even if user have selfattendance permissons 2025-10-08 14:16:24 +05:30
d180d5d60e Changed the variable from totalCount to totalAmount 2025-10-08 12:32:09 +05:30
60eefa78c0 Corrected the total amount sent in expense pending dashboard 2025-10-08 12:30:34 +05:30
b1af96b923 Added the name of the project name in view model 2025-10-08 12:16:25 +05:30
629c4541d6 Removed the logic to save the FCM from verify FCM API 2025-10-08 11:35:43 +05:30
e93408c00d Removed the FCM token verify MPIN API 2025-10-08 11:30:47 +05:30
16e509ccbd Hided the tenant management feature permissions from tenant other than super tenant 2025-10-08 11:03:29 +05:30
8a4a056c2d Added the query parameter to get all employee in basic employee API 2025-10-08 10:56:05 +05:30
f6e8a0d5e2 Sloved missing veriable issue 2025-10-06 15:29:56 +05:30
0561e356d8 Added the condition to get all records in expense monthly report API 2025-10-06 15:29:00 +05:30
e2ee3f325c Added the Distinct in expense filter 2025-10-04 12:38:22 +05:30
6441103e30 Chnaged the search to searchString in get directory list API 2025-10-04 12:35:15 +05:30
d380dfebd2 Added the ExpensTypes in expense list API and get expenese filter API 2025-10-04 12:25:02 +05:30
658fa8cd23 Changed he expense UID format 2025-10-04 10:45:38 +05:30
3afdad29b2 Added total amout in expense pending dashboard 2025-10-03 17:13:23 +05:30
8f463ce90d Added ExpenseUId in expense table 2025-10-03 15:24:59 +05:30
590476a8aa Added the APi to get count of pending expenses 2025-10-03 14:32:59 +05:30
548e714ea9 Getting the monthly expenses report based on project and category 2025-10-03 12:53:19 +05:30
91f4305995 Added the projectId in get expense pending list API 2025-10-03 11:08:15 +05:30
7e15c517ac Optimized the dashboard APIs for expenses 2025-10-03 10:54:38 +05:30
9332d9cc0b Added the API to get list pending expenses according to permissions 2025-10-01 18:12:51 +05:30
541ed28bd2 Added the new deashboard report of expenses 2025-10-01 17:32:48 +05:30
0e1d20156f Check if email exist in total tenant 2025-10-01 17:16:27 +05:30
87c5de87a1 Checking in organization not in tenant 2025-10-01 12:19:03 +05:30
5eda1773b7 Chnaged the sequence of buttons in sequece 2025-10-01 11:33:56 +05:30
b3f54962ab Removed hasProjectAccess check from document controller 2025-10-01 10:55:00 +05:30
040e7df32b Removed the permission check from project details API 2025-10-01 10:32:04 +05:30
0066e20c43 Removed the tenant check from find employee query in document controller 2025-10-01 10:23:46 +05:30
7659eadd00 Added the check before sending mail when adding application access to employee 2025-10-01 10:10:01 +05:30
824381bb49 Added the project section in side menu 2025-10-01 09:53:40 +05:30
207a44acd7 Checking if have directory admin permission in delete contact API 2025-09-30 21:32:59 +05:30
7775f58d69 Added the check to check if has directory admin permission 2025-09-30 21:13:56 +05:30
91be729b41 Changed the logic of getting all employees in get employee list API 2025-09-30 20:53:54 +05:30
0bd57d29d8 Removed the Authorization of hub 2025-09-30 20:31:57 +05:30
b442bb4bbc Changed the logic to get tenantId in auth controller 2025-09-30 20:23:54 +05:30
ca3e47c1e6 Changed theexpenses Vms 2025-09-30 20:11:29 +05:30
061512d501 Removed the organization ID from manage employee DTO 2025-09-30 19:44:00 +05:30
71cc442054 getting the all project through get project list API 2025-09-30 19:28:09 +05:30
bab03a8e47 Getting all employees in attendence module regardless of project 2025-09-30 18:17:24 +05:30
34 changed files with 13769 additions and 610 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") b.Property<int>("Activity")
.HasColumnType("int"); .HasColumnType("int");
b.Property<DateTime?>("ApprovedAt")
.HasColumnType("datetime(6)");
b.Property<Guid?>("ApprovedById") b.Property<Guid?>("ApprovedById")
.HasColumnType("char(36)"); .HasColumnType("char(36)");
@ -200,6 +203,12 @@ namespace Marco.Pms.DataAccess.Migrations
b.Property<Guid>("ProjectID") b.Property<Guid>("ProjectID")
.HasColumnType("char(36)"); .HasColumnType("char(36)");
b.Property<DateTime?>("RequestedAt")
.HasColumnType("datetime(6)");
b.Property<Guid?>("RequestedById")
.HasColumnType("char(36)");
b.Property<Guid>("TenantId") b.Property<Guid>("TenantId")
.HasColumnType("char(36)"); .HasColumnType("char(36)");
@ -209,6 +218,8 @@ namespace Marco.Pms.DataAccess.Migrations
b.HasIndex("EmployeeId"); b.HasIndex("EmployeeId");
b.HasIndex("RequestedById");
b.HasIndex("TenantId"); b.HasIndex("TenantId");
b.ToTable("Attendes"); b.ToTable("Attendes");
@ -1832,6 +1843,10 @@ namespace Marco.Pms.DataAccess.Migrations
.IsRequired() .IsRequired()
.HasColumnType("longtext"); .HasColumnType("longtext");
b.Property<string>("ExpenseUId")
.IsRequired()
.HasColumnType("longtext");
b.Property<Guid>("ExpensesTypeId") b.Property<Guid>("ExpensesTypeId")
.HasColumnType("char(36)"); .HasColumnType("char(36)");
@ -4711,6 +4726,10 @@ namespace Marco.Pms.DataAccess.Migrations
.OnDelete(DeleteBehavior.Cascade) .OnDelete(DeleteBehavior.Cascade)
.IsRequired(); .IsRequired();
b.HasOne("Marco.Pms.Model.Employees.Employee", "RequestedBy")
.WithMany()
.HasForeignKey("RequestedById");
b.HasOne("Marco.Pms.Model.TenantModels.Tenant", "Tenant") b.HasOne("Marco.Pms.Model.TenantModels.Tenant", "Tenant")
.WithMany() .WithMany()
.HasForeignKey("TenantId") .HasForeignKey("TenantId")
@ -4721,6 +4740,8 @@ namespace Marco.Pms.DataAccess.Migrations
b.Navigation("Employee"); b.Navigation("Employee");
b.Navigation("RequestedBy");
b.Navigation("Tenant"); b.Navigation("Tenant");
}); });

View File

@ -19,6 +19,8 @@ namespace Marco.Pms.Model.AttendanceModule
public Guid ProjectID { get; set; } public Guid ProjectID { get; set; }
public DateTime AttendanceDate { get; set; } public DateTime AttendanceDate { get; set; }
public DateTime? RequestedAt { get; set; }
public DateTime? ApprovedAt { get; set; }
public DateTime? InTime { get; set; } public DateTime? InTime { get; set; }
public DateTime? OutTime { get; set; } public DateTime? OutTime { get; set; }
public bool IsApproved { get; set; } public bool IsApproved { get; set; }
@ -29,5 +31,10 @@ namespace Marco.Pms.Model.AttendanceModule
[ForeignKey("ApprovedById")] [ForeignKey("ApprovedById")]
[ValidateNever] [ValidateNever]
public Employee? Approver { get; set; } public Employee? Approver { get; set; }
public Guid? RequestedById { get; set; }
[ForeignKey("RequestedById")]
[ValidateNever]
public Employee? RequestedBy { get; set; }
} }
} }

View File

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

View File

@ -19,7 +19,7 @@
public string? EmergencyPhoneNumber { get; set; } public string? EmergencyPhoneNumber { get; set; }
public string? EmergencyContactPerson { get; set; } public string? EmergencyContactPerson { get; set; }
public Guid JobRoleId { get; set; } public Guid JobRoleId { get; set; }
public required Guid OrganizationId { get; set; } public Guid? OrganizationId { get; set; }
public required bool HasApplicationAccess { get; set; } public required bool HasApplicationAccess { get; set; }
} }
public class MobileUserManageDto public class MobileUserManageDto
@ -33,7 +33,7 @@
public required string Gender { get; set; } public required string Gender { get; set; }
public Guid JobRoleId { get; set; } public Guid JobRoleId { get; set; }
public string? ProfileImage { get; set; } public string? ProfileImage { get; set; }
public required Guid OrganizationId { get; set; } public Guid? OrganizationId { get; set; }
public required bool HasApplicationAccess { get; set; } public required bool HasApplicationAccess { get; set; }
} }

View File

@ -27,7 +27,7 @@ namespace Marco.Pms.Model.Dtos.Project
[DisplayName("Project Status")] [DisplayName("Project Status")]
[Required(ErrorMessage = "Project Status is required!")] [Required(ErrorMessage = "Project Status is required!")]
public required Guid ProjectStatusId { get; set; } public required Guid ProjectStatusId { get; set; }
public required Guid PromoterId { get; set; } public Guid? PromoterId { get; set; }
public required Guid PMCId { get; set; } public Guid? PMCId { get; set; }
} }
} }

View File

@ -27,7 +27,7 @@ namespace Marco.Pms.Model.Dtos.Project
[DisplayName("Project Status")] [DisplayName("Project Status")]
[Required(ErrorMessage = "Project Status is required!")] [Required(ErrorMessage = "Project Status is required!")]
public required Guid ProjectStatusId { get; set; } public required Guid ProjectStatusId { get; set; }
public required Guid PromoterId { get; set; } public Guid? PromoterId { get; set; }
public required Guid PMCId { get; set; } public Guid? PMCId { get; set; }
} }
} }

View File

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

View File

@ -6,6 +6,7 @@
public List<Guid>? StatusIds { get; set; } public List<Guid>? StatusIds { get; set; }
public List<Guid>? CreatedByIds { get; set; } public List<Guid>? CreatedByIds { get; set; }
public List<Guid>? PaidById { get; set; } public List<Guid>? PaidById { get; set; }
public List<Guid>? ExpenseTypeIds { get; set; }
public bool IsTransactionDate { get; set; } = false; public bool IsTransactionDate { get; set; } = false;
public DateTime? StartDate { get; set; } public DateTime? StartDate { get; set; }
public DateTime? EndDate { get; set; } public DateTime? EndDate { get; set; }

View File

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

View File

@ -20,6 +20,7 @@ namespace Marco.Pms.Model.MongoDBModels.Expenses
public DateTime ExpireAt { get; set; } = DateTime.UtcNow.Date.AddDays(1); public DateTime ExpireAt { get; set; } = DateTime.UtcNow.Date.AddDays(1);
public string SupplerName { get; set; } = string.Empty; public string SupplerName { get; set; } = string.Empty;
public double Amount { get; set; } public double Amount { get; set; }
public string? ExpenseUId { get; set; }
public ExpensesStatusMasterMongoDB Status { get; set; } = new ExpensesStatusMasterMongoDB(); public ExpensesStatusMasterMongoDB Status { get; set; } = new ExpensesStatusMasterMongoDB();
public List<ExpensesStatusMasterMongoDB> NextStatus { get; set; } = new List<ExpensesStatusMasterMongoDB>(); public List<ExpensesStatusMasterMongoDB> NextStatus { get; set; } = new List<ExpensesStatusMasterMongoDB>();
public bool PreApproved { get; set; } = false; public bool PreApproved { get; set; } = false;

View File

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

View File

@ -26,6 +26,7 @@ namespace Marco.Pms.Model.ViewModels.Expenses
public string? TransactionId { get; set; } public string? TransactionId { get; set; }
public string Description { get; set; } = string.Empty; public string Description { get; set; } = string.Empty;
public string? Location { get; set; } public string? Location { get; set; }
public string? ExpenseUId { get; set; }
public List<BasicDocumentVM> Documents { get; set; } = new List<BasicDocumentVM>(); public List<BasicDocumentVM> Documents { get; set; } = new List<BasicDocumentVM>();
public List<ExpenseLogVM> ExpenseLogs { get; set; } = new List<ExpenseLogVM>(); public List<ExpenseLogVM> ExpenseLogs { get; set; } = new List<ExpenseLogVM>();
public string? GSTNumber { get; set; } public string? GSTNumber { get; set; }

View File

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

View File

@ -470,6 +470,19 @@ namespace Marco.Pms.Services.Controllers
foreach (var item in menu.Items) foreach (var item in menu.Items)
{ {
if (item.Text == "Projects")
{
allowedItems.Add(new MenuItem
{
Text = "Projects",
Icon = "bx bx-building-house",
Available = true,
Link = "/projects",
PermissionIds = new List<string>(),
Submenu = new List<SubMenuItem>()
});
continue;
}
// --- Item permission check --- // --- Item permission check ---
if (!item.PermissionIds.Any()) if (!item.PermissionIds.Any())
{ {
@ -608,6 +621,20 @@ namespace Marco.Pms.Services.Controllers
var featureIds = await generalHelper.GetFeatureIdsByTenentIdAsync(tenantId); var featureIds = await generalHelper.GetFeatureIdsByTenentIdAsync(tenantId);
_logger.LogInfo("Enabled features for TenantId: {TenantId} -> {FeatureIds}", tenantId, string.Join(",", featureIds)); _logger.LogInfo("Enabled features for TenantId: {TenantId} -> {FeatureIds}", tenantId, string.Join(",", featureIds));
if (!(featureIds?.Any() ?? false))
{
featureIds = new List<Guid>
{
new Guid("a4e25142-449b-4334-a6e5-22f70e4732d7"), // Expense Management feature
new Guid("81ab8a87-8ccd-4015-a917-0627cee6a100"), // Employee Management feature
new Guid("52c9cf54-1eb2-44d2-81bb-524cf29c0a94"), // Attendance Management feature
new Guid("a8cf4331-8f04-4961-8360-a3f7c3cc7462"), // Document Management feature
new Guid("be3b3afc-6ccf-4566-b9b6-aafcb65546be"), // Masters Management feature
new Guid("39e66f81-efc6-446c-95bd-46bff6cfb606"), // Directory Management feature
new Guid("6d4c82d6-dbce-48ab-b8b8-f785f4d8c914") // Organization Management feature
};
}
// Aggregate menus based on enabled features // Aggregate menus based on enabled features
var response = featureIds var response = featureIds
.Where(id => featureMenus.ContainsKey(id)) .Where(id => featureMenus.ContainsKey(id))

View File

@ -1,11 +1,12 @@
using Marco.Pms.DataAccess.Data; using AutoMapper;
using Marco.Pms.DataAccess.Data;
using Marco.Pms.Model.AttendanceModule; using Marco.Pms.Model.AttendanceModule;
using Marco.Pms.Model.Dtos.Attendance; using Marco.Pms.Model.Dtos.Attendance;
using Marco.Pms.Model.Employees; using Marco.Pms.Model.Employees;
using Marco.Pms.Model.Entitlements; using Marco.Pms.Model.Entitlements;
using Marco.Pms.Model.Mapper; using Marco.Pms.Model.Mapper;
using Marco.Pms.Model.Projects;
using Marco.Pms.Model.Utilities; using Marco.Pms.Model.Utilities;
using Marco.Pms.Model.ViewModels.Activities;
using Marco.Pms.Model.ViewModels.AttendanceVM; using Marco.Pms.Model.ViewModels.AttendanceVM;
using Marco.Pms.Services.Hubs; using Marco.Pms.Services.Hubs;
using Marco.Pms.Services.Service; using Marco.Pms.Services.Service;
@ -28,48 +29,42 @@ namespace MarcoBMS.Services.Controllers
public class AttendanceController : ControllerBase public class AttendanceController : ControllerBase
{ {
private readonly ApplicationDbContext _context; private readonly ApplicationDbContext _context;
private readonly EmployeeHelper _employeeHelper; private readonly IServiceScopeFactory _serviceScopeFactory;
private readonly IProjectServices _projectServices;
private readonly UserHelper _userHelper; private readonly UserHelper _userHelper;
private readonly S3UploadService _s3Service;
private readonly PermissionServices _permission; private readonly PermissionServices _permission;
private readonly ILoggingService _logger; private readonly ILoggingService _logger;
private readonly IHubContext<MarcoHub> _signalR; private readonly Guid tenantId;
private readonly IFirebaseService _firebase; private readonly IMapper _mapper;
public AttendanceController( public AttendanceController(
ApplicationDbContext context, EmployeeHelper employeeHelper, IProjectServices projectServices, UserHelper userHelper, ApplicationDbContext context,
S3UploadService s3Service, ILoggingService logger, PermissionServices permission, IHubContext<MarcoHub> signalR, IFirebaseService firebase) UserHelper userHelper,
IServiceScopeFactory serviceScopeFactory,
ILoggingService logger,
PermissionServices permission,
IMapper mapper)
{ {
_context = context; _context = context;
_employeeHelper = employeeHelper; _serviceScopeFactory = serviceScopeFactory;
_projectServices = projectServices;
_userHelper = userHelper; _userHelper = userHelper;
_s3Service = s3Service;
_logger = logger; _logger = logger;
_permission = permission; _permission = permission;
_signalR = signalR; _mapper = mapper;
_firebase = firebase; tenantId = userHelper.GetTenantId();
}
private Guid GetTenantId()
{
return _userHelper.GetTenantId();
//var tenant = User.FindFirst("TenantId")?.Value;
//return (tenant != null ? Convert.ToInt32(tenant) : 1);
} }
[HttpGet("log/attendance/{attendanceid}")] [HttpGet("log/attendance/{attendanceid}")]
public async Task<IActionResult> GetAttendanceLogById(Guid 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 List<AttendanceLog> lstAttendance = await _context.AttendanceLogs
.Include(a => a.Document) .Include(a => a.Document)
.Include(a => a.Employee) .Include(a => a.Employee)
.Include(a => a.UpdatedByEmployee) .Include(a => a.UpdatedByEmployee)
.Where(c => c.AttendanceId == attendanceid && c.TenantId == TenantId) .Where(c => c.AttendanceId == attendanceid && c.TenantId == tenantId)
.ToListAsync(); .ToListAsync();
List<AttendanceLogVM> attendanceLogVMs = new List<AttendanceLogVM>(); List<AttendanceLogVM> attendanceLogVMs = new List<AttendanceLogVM>();
@ -85,30 +80,42 @@ namespace MarcoBMS.Services.Controllers
} }
[HttpGet("log/employee/{employeeId}")] [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) if (employeeId == Guid.Empty)
{ {
_logger.LogWarning("The employee Id sent by user is 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)); 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>(); List<EmployeeAttendanceVM> results = new List<EmployeeAttendanceVM>();
if (employee != null) if (employee != null)
@ -121,11 +128,17 @@ namespace MarcoBMS.Services.Controllers
EmployeeId = employee.Id, EmployeeId = employee.Id,
FirstName = employee.FirstName, FirstName = employee.FirstName,
LastName = employee.LastName, LastName = employee.LastName,
ProjectId = attendance.ProjectID,
ProjectName = projects.Where(p => p.Id == attendance.ProjectID).Select(p => p.Name).FirstOrDefault(),
CheckInTime = attendance.InTime, CheckInTime = attendance.InTime,
CheckOutTime = attendance.OutTime, CheckOutTime = attendance.OutTime,
JobRoleName = employee.JobRole != null ? employee.JobRole.Name : "", JobRoleName = employee.JobRole != null ? employee.JobRole.Name : "",
Activity = attendance.Activity, 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); results.Add(result);
} }
@ -146,27 +159,12 @@ namespace MarcoBMS.Services.Controllers
[HttpGet("project/log")] [HttpGet("project/log")]
public async Task<IActionResult> EmployeeAttendanceByDateRange([FromQuery] Guid projectId, [FromQuery] Guid? organizationId, [FromQuery] string? dateFrom = null, [FromQuery] string? dateTo = null) public async Task<IActionResult> EmployeeAttendanceByDateRange([FromQuery] Guid? projectId, [FromQuery] Guid? organizationId, [FromQuery] string? dateFrom = null, [FromQuery] string? dateTo = null)
{ {
Guid tenantId = GetTenantId();
var LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); var LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
var project = await _context.Projects.AsNoTracking().FirstOrDefaultAsync(p => p.Id == projectId && p.TenantId == tenantId);
if (project == null)
{
_logger.LogWarning("Project {ProjectId} not found in database", projectId);
return NotFound(ApiResponse<object>.ErrorResponse("Project not found."));
}
var hasTeamAttendancePermission = await _permission.HasPermission(PermissionsMaster.TeamAttendance, LoggedInEmployee.Id); var hasTeamAttendancePermission = await _permission.HasPermission(PermissionsMaster.TeamAttendance, LoggedInEmployee.Id);
var hasSelfAttendancePermission = await _permission.HasPermission(PermissionsMaster.SelfAttendance, LoggedInEmployee.Id); var hasSelfAttendancePermission = await _permission.HasPermission(PermissionsMaster.SelfAttendance, LoggedInEmployee.Id);
var hasProjectPermission = await _permission.HasProjectPermission(LoggedInEmployee, projectId);
if (!hasProjectPermission)
{
_logger.LogWarning("Employee {EmployeeId} tries to access attendance of project {ProjectId}, but don't have access", LoggedInEmployee.Id, projectId);
return Unauthorized(ApiResponse<object>.ErrorResponse("Unauthorized access", "Unauthorized access", 404));
}
DateTime fromDate = new DateTime(); DateTime fromDate = new DateTime();
DateTime toDate = new DateTime(); DateTime toDate = new DateTime();
@ -182,26 +180,46 @@ namespace MarcoBMS.Services.Controllers
return BadRequest(ApiResponse<object>.ErrorResponse("Invalid Date", "Invalid Date", 400)); return BadRequest(ApiResponse<object>.ErrorResponse("Invalid Date", "Invalid Date", 400));
} }
if (projectId == Guid.Empty)
{
_logger.LogWarning("The project Id sent by user is less than or equal to zero");
return BadRequest(ApiResponse<object>.ErrorResponse("Project ID is required and must be greater than zero.", "Project ID is required and must be greater than zero.", 400));
}
var result = new List<EmployeeAttendanceVM>(); var result = new List<EmployeeAttendanceVM>();
//Attendance? attendance = null; //Attendance? attendance = null;
ProjectAllocation? teamMember = null;
if (dateFrom == null) fromDate = DateTime.UtcNow.Date; if (dateFrom == null) fromDate = DateTime.UtcNow.Date;
if (dateTo == null && dateFrom != null) toDate = fromDate.AddDays(-1); if (dateTo == null && dateFrom != null) toDate = fromDate.AddDays(-1);
var lstAttendanceQuery = _context.Attendes
.Include(a => a.Employee)
.ThenInclude(e => e!.JobRole)
.Include(a => a.Employee)
.ThenInclude(e => e!.JobRole)
.Include(a => a.Employee)
.ThenInclude(e => e!.Organization)
.Include(a => a.Employee)
.ThenInclude(e => e!.JobRole)
.Where(a =>
a.AttendanceDate.Date >= fromDate.Date &&
a.AttendanceDate.Date <= toDate.Date &&
a.TenantId == tenantId &&
a.Employee != null &&
a.Employee.Organization != null &&
a.Employee.JobRole != null);
if (organizationId.HasValue)
{
lstAttendanceQuery = lstAttendanceQuery.Where(a => a.Employee != null && a.Employee.OrganizationId == organizationId);
}
if (projectId.HasValue)
{
lstAttendanceQuery = lstAttendanceQuery.Where(a => a.ProjectID == projectId);
}
if (hasTeamAttendancePermission) 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 lstAttendanceQuery.ToListAsync();
var projectIds = lstAttendance.Select(a => a.ProjectID).ToList();
var projects = await _context.Projects.Where(p => projectIds.Contains(p.Id) && p.TenantId == tenantId).ToListAsync();
List<ProjectAllocation> projectteam = await _projectServices.GetTeamByProject(tenantId, projectId, organizationId, true);
var jobRole = await _context.JobRoles.ToListAsync();
foreach (Attendance? attendance in lstAttendance) foreach (Attendance? attendance in lstAttendance)
{ {
var result1 = new EmployeeAttendanceVM() var result1 = new EmployeeAttendanceVM()
@ -209,77 +227,61 @@ namespace MarcoBMS.Services.Controllers
Id = attendance.Id, Id = attendance.Id,
CheckInTime = attendance.InTime, CheckInTime = attendance.InTime,
CheckOutTime = attendance.OutTime, CheckOutTime = attendance.OutTime,
Activity = attendance.Activity Activity = attendance.Activity,
EmployeeId = attendance.EmployeeId,
FirstName = attendance.Employee?.FirstName,
LastName = attendance.Employee?.LastName,
JobRoleName = attendance.Employee?.JobRole?.Name,
ProjectId = attendance.ProjectID,
ProjectName = projects.Where(p => p.Id == attendance.ProjectID).Select(p => p.Name).FirstOrDefault(),
OrganizationName = attendance.Employee?.Organization?.Name,
RequestedAt = attendance.RequestedAt,
RequestedBy = _mapper.Map<BasicEmployeeVM>(attendance.RequestedBy),
ApprovedAt = attendance.ApprovedAt,
Approver = _mapper.Map<BasicEmployeeVM>(attendance.Approver)
}; };
teamMember = projectteam.Find(x => x.EmployeeId == attendance.EmployeeId);
if (teamMember != null)
{
result1.EmployeeAvatar = null;
result1.EmployeeId = teamMember.EmployeeId;
if (teamMember.Employee != null)
{
result1.FirstName = teamMember.Employee.FirstName;
result1.LastName = teamMember.Employee.LastName;
result1.JobRoleName = teamMember.Employee.JobRole != null ? teamMember.Employee.JobRole.Name : null;
result1.OrganizationName = teamMember.Employee.Organization?.Name;
}
else
{
result1.FirstName = null;
result1.LastName = null;
result1.JobRoleName = null;
result1.OrganizationName = null;
}
result.Add(result1); result.Add(result1);
} }
}
} }
else if (hasSelfAttendancePermission) 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)
.ToListAsync();
var projectAllocationQuery = _context.ProjectAllocations var lstAttendances = await lstAttendanceQuery.Where(a => a.EmployeeId == LoggedInEmployee.Id).ToListAsync();
.Include(pa => pa.Employee)
.ThenInclude(e => e!.Organization)
.Where(pa => pa.ProjectId == projectId && pa.EmployeeId == LoggedInEmployee.Id && pa.TenantId == tenantId && pa.IsActive);
if (organizationId.HasValue) var projectIds = lstAttendances.Select(a => a.ProjectID).ToList();
{ var projects = await _context.Projects.Where(p => projectIds.Contains(p.Id) && p.TenantId == tenantId).ToListAsync();
projectAllocationQuery = projectAllocationQuery.Where(pa => pa.Employee != null && pa.Employee.OrganizationId == organizationId);
}
var projectAllocation = await projectAllocationQuery.FirstOrDefaultAsync();
foreach (var attendance in lstAttendances) foreach (var attendance in lstAttendances)
{
if (projectAllocation != null)
{ {
EmployeeAttendanceVM result1 = new EmployeeAttendanceVM EmployeeAttendanceVM result1 = new EmployeeAttendanceVM
{ {
Id = attendance.Id, Id = attendance.Id,
EmployeeAvatar = null, EmployeeAvatar = null,
EmployeeId = projectAllocation.EmployeeId, EmployeeId = attendance.EmployeeId,
FirstName = projectAllocation.Employee?.FirstName, FirstName = attendance.Employee?.FirstName,
LastName = projectAllocation.Employee?.LastName, LastName = attendance.Employee?.LastName,
JobRoleName = projectAllocation.Employee?.JobRole?.Name, JobRoleName = attendance.Employee?.JobRole?.Name,
OrganizationName = projectAllocation.Employee?.Organization?.Name, ProjectId = attendance.ProjectID,
ProjectName = projects.Where(p => p.Id == attendance.ProjectID).Select(p => p.Name).FirstOrDefault(),
OrganizationName = attendance.Employee?.Organization?.Name,
CheckInTime = attendance.InTime, CheckInTime = attendance.InTime,
CheckOutTime = attendance.OutTime, CheckOutTime = attendance.OutTime,
Activity = attendance.Activity Activity = attendance.Activity,
RequestedAt = attendance.RequestedAt,
RequestedBy = _mapper.Map<BasicEmployeeVM>(attendance.RequestedBy),
ApprovedAt = attendance.ApprovedAt,
Approver = _mapper.Map<BasicEmployeeVM>(attendance.Approver)
}; };
result.Add(result1); result.Add(result1);
} }
} }
}
_logger.LogInfo("{count} Attendance records fetched successfully", result.Count); _logger.LogInfo("{count} Attendance records fetched successfully", result.Count);
return Ok(ApiResponse<object>.SuccessResponse(result, System.String.Format("{0} Attendance records fetched successfully", result.Count), 200)); return Ok(ApiResponse<object>.SuccessResponse(result, System.String.Format("{0} Attendance records fetched successfully", result.Count), 200));
} }
[HttpGet("project/team")] [HttpGet("project/team")]
/// <summary> /// <summary>
/// Retrieves employee attendance records for a specified project and date. /// Retrieves employee attendance records for a specified project and date.
/// The result is filtered based on the logged-in employee's permissions (Team or Self). /// The result is filtered based on the logged-in employee's permissions (Team or Self).
@ -289,13 +291,12 @@ namespace MarcoBMS.Services.Controllers
/// <param name="includeInactive">Optional. Includes inactive employees in the team list if true.</param> /// <param name="includeInactive">Optional. Includes inactive employees in the team list if true.</param>
/// <param name="date">Optional. The date for which to fetch attendance, in "yyyy-MM-dd" format. Defaults to the current UTC date.</param> /// <param name="date">Optional. The date for which to fetch attendance, in "yyyy-MM-dd" format. Defaults to the current UTC date.</param>
/// <returns>An IActionResult containing a list of employee attendance records or an error response.</returns> /// <returns>An IActionResult containing a list of employee attendance records or an error response.</returns>
public async Task<IActionResult> EmployeeAttendanceByProjectAsync([FromQuery] Guid projectId, [FromQuery] Guid? organizationId, [FromQuery] bool includeInactive, [FromQuery] string? date = null) public async Task<IActionResult> EmployeeAttendanceByProjectAsync([FromQuery] Guid? projectId, [FromQuery] Guid? organizationId, [FromQuery] bool includeInactive, [FromQuery] string? date = null)
{ {
var tenantId = GetTenantId();
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
// --- 1. Initial Validation and Permission Checks --- // --- 1. Initial Validation and Permission Checks ---
_logger.LogInfo("Fetching attendance for ProjectId: {ProjectId}, TenantId: {TenantId}", projectId, tenantId); _logger.LogInfo("Fetching attendance for ProjectId: {ProjectId}, TenantId: {TenantId}", projectId ?? Guid.Empty, tenantId);
// Validate date format // Validate date format
if (!DateTime.TryParse(date, out var forDate)) if (!DateTime.TryParse(date, out var forDate))
@ -303,20 +304,6 @@ namespace MarcoBMS.Services.Controllers
forDate = DateTime.UtcNow.Date; // Default to today's date forDate = DateTime.UtcNow.Date; // Default to today's date
} }
// Check if the project exists and if the employee has access
var project = await _context.Projects.AsNoTracking().FirstOrDefaultAsync(p => p.Id == projectId && p.TenantId == tenantId);
if (project == null)
{
_logger.LogWarning("Project {ProjectId} not found in database", projectId);
return NotFound(ApiResponse<object>.ErrorResponse("Project not found."));
}
if (!await _permission.HasProjectPermission(loggedInEmployee, projectId))
{
_logger.LogWarning("Unauthorized access attempt by EmployeeId: {EmployeeId} for ProjectId: {ProjectId}", loggedInEmployee.Id, projectId);
return Unauthorized(ApiResponse<object>.ErrorResponse("You do not have permission to access this project."));
}
// --- 2. Delegate to Specific Logic Based on Permissions --- // --- 2. Delegate to Specific Logic Based on Permissions ---
try try
{ {
@ -325,13 +312,17 @@ namespace MarcoBMS.Services.Controllers
if (hasTeamAttendancePermission) if (hasTeamAttendancePermission)
{ {
if (!organizationId.HasValue)
{
organizationId = loggedInEmployee.OrganizationId;
}
_logger.LogInfo("EmployeeId: {EmployeeId} has Team Attendance permission. Fetching team attendance.", loggedInEmployee.Id); _logger.LogInfo("EmployeeId: {EmployeeId} has Team Attendance permission. Fetching team attendance.", loggedInEmployee.Id);
result = await GetTeamAttendanceAsync(tenantId, projectId, organizationId, forDate, includeInactive); result = await GetTeamAttendanceAsync(tenantId, projectId, organizationId.Value, forDate, includeInactive);
} }
else if (await _permission.HasPermission(PermissionsMaster.SelfAttendance, loggedInEmployee.Id)) else if (await _permission.HasPermission(PermissionsMaster.SelfAttendance, loggedInEmployee.Id))
{ {
_logger.LogInfo("EmployeeId: {EmployeeId} has Self Attendance permission. Fetching self attendance.", loggedInEmployee.Id); _logger.LogInfo("EmployeeId: {EmployeeId} has Self Attendance permission. Fetching self attendance.", loggedInEmployee.Id);
result = await GetSelfAttendanceAsync(tenantId, projectId, loggedInEmployee.Id, organizationId, forDate); result = await GetSelfAttendanceAsync(tenantId, projectId, loggedInEmployee.Id, forDate);
} }
else else
{ {
@ -339,41 +330,49 @@ namespace MarcoBMS.Services.Controllers
return StatusCode(403, ApiResponse<object>.ErrorResponse("You do not have permission to view attendance.", new { }, 403)); return StatusCode(403, ApiResponse<object>.ErrorResponse("You do not have permission to view attendance.", new { }, 403));
} }
_logger.LogInfo("Successfully fetched {Count} attendance records for ProjectId: {ProjectId}", result.Count, projectId); _logger.LogInfo("Successfully fetched {Count} attendance records for ProjectId: {ProjectId}", result.Count, projectId ?? Guid.Empty);
return Ok(ApiResponse<object>.SuccessResponse(result, $"{result.Count} attendance records fetched successfully.")); return Ok(ApiResponse<object>.SuccessResponse(result, $"{result.Count} attendance records fetched successfully."));
} }
catch (Exception ex) catch (Exception ex)
{ {
_logger.LogError(ex, "An error occurred while fetching attendance for ProjectId: {ProjectId}", projectId); _logger.LogError(ex, "An error occurred while fetching attendance for ProjectId: {ProjectId}", projectId ?? Guid.Empty);
return StatusCode(500, ApiResponse<object>.ErrorResponse("An internal server error occurred.")); return StatusCode(500, ApiResponse<object>.ErrorResponse("An internal server error occurred."));
} }
} }
[HttpGet("regularize")] [HttpGet("regularize")]
public async Task<IActionResult> GetRequestRegularizeAttendance([FromQuery] Guid projectId, [FromQuery] Guid? organizationId, [FromQuery] bool IncludeInActive) public async Task<IActionResult> GetRequestRegularizeAttendance([FromQuery] Guid? projectId, [FromQuery] Guid? organizationId, [FromQuery] bool IncludeInActive)
{ {
Guid TenantId = GetTenantId();
Employee LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); Employee LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
var result = new List<EmployeeAttendanceVM>(); var result = new List<EmployeeAttendanceVM>();
var hasProjectPermission = await _permission.HasProjectPermission(LoggedInEmployee, projectId);
if (!hasProjectPermission) var lstAttendanceQuery = _context.Attendes
.Include(a => a.RequestedBy)
.ThenInclude(e => e!.JobRole)
.Include(a => a.Employee)
.ThenInclude(e => e!.Organization)
.Include(a => a.Employee)
.ThenInclude(e => e!.JobRole)
.Where(c => c.Activity == ATTENDANCE_MARK_TYPE.REQUEST_REGULARIZE && c.Employee != null && c.Employee.JobRole != null && c.TenantId == tenantId);
if (organizationId.HasValue)
{ {
_logger.LogWarning("Employee {EmployeeId} tries to access attendance of project {ProjectId}, but don't have access", LoggedInEmployee.Id, projectId); lstAttendanceQuery = lstAttendanceQuery.Where(a => a.Employee != null && a.Employee.OrganizationId == organizationId);
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(); if (projectId.HasValue)
{
lstAttendanceQuery = lstAttendanceQuery.Where(a => a.ProjectID == projectId);
}
List<ProjectAllocation> projectteam = await _projectServices.GetTeamByProject(TenantId, projectId, organizationId, true); List<Attendance> lstAttendance = await lstAttendanceQuery.ToListAsync();
var idList = projectteam.Select(p => p.EmployeeId).ToList();
var jobRole = await _context.JobRoles.ToListAsync(); var projectIds = lstAttendance.Select(a => a.ProjectID).ToList();
var projects = await _context.Projects.Where(p => projectIds.Contains(p.Id) && p.TenantId == tenantId).ToListAsync();
foreach (Attendance attende in lstAttendance) foreach (Attendance attende in lstAttendance)
{
var teamMember = projectteam.Find(m => m.EmployeeId == attende.EmployeeId);
if (teamMember != null && teamMember.Employee != null && teamMember.Employee.JobRole != null)
{ {
var result1 = new EmployeeAttendanceVM() var result1 = new EmployeeAttendanceVM()
{ {
@ -383,13 +382,16 @@ namespace MarcoBMS.Services.Controllers
Activity = attende.Activity, Activity = attende.Activity,
EmployeeAvatar = null, EmployeeAvatar = null,
EmployeeId = attende.EmployeeId, EmployeeId = attende.EmployeeId,
FirstName = teamMember.Employee.FirstName, ProjectId = attende.ProjectID,
LastName = teamMember.Employee.LastName, FirstName = attende.Employee?.FirstName,
JobRoleName = teamMember.Employee.JobRole.Name, ProjectName = projects.Where(p => p.Id == attende.ProjectID).Select(p => p.Name).FirstOrDefault(),
OrganizationName = teamMember.Employee.Organization?.Name LastName = attende.Employee?.LastName,
JobRoleName = attende.Employee?.JobRole?.Name,
OrganizationName = attende.Employee?.Organization?.Name,
RequestedAt = attende.RequestedAt,
RequestedBy = _mapper.Map<BasicEmployeeVM>(attende.RequestedBy)
}; };
result.Add(result1); result.Add(result1);
}
} }
@ -416,13 +418,16 @@ namespace MarcoBMS.Services.Controllers
return BadRequest(ApiResponse<object>.ErrorResponse("Invalid data", errors, 400)); return BadRequest(ApiResponse<object>.ErrorResponse("Invalid data", errors, 400));
} }
Guid TenantId = GetTenantId(); using var scope = _serviceScopeFactory.CreateScope();
var _signalR = scope.ServiceProvider.GetRequiredService<IHubContext<MarcoHub>>();
var _employeeHelper = scope.ServiceProvider.GetRequiredService<EmployeeHelper>();
var currentEmployee = await _userHelper.GetCurrentEmployeeAsync(); var currentEmployee = await _userHelper.GetCurrentEmployeeAsync();
using var transaction = await _context.Database.BeginTransactionAsync(); using var transaction = await _context.Database.BeginTransactionAsync();
try 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) if (recordAttendanceDot.MarkTime == null)
{ {
@ -460,10 +465,12 @@ namespace MarcoBMS.Services.Controllers
{ {
DateTime date = attendance.AttendanceDate; DateTime date = attendance.AttendanceDate;
finalDateTime = GetDateFromTimeStamp(date.Date, recordAttendanceDot.MarkTime); finalDateTime = GetDateFromTimeStamp(date.Date, recordAttendanceDot.MarkTime);
if (attendance.InTime < finalDateTime) if (attendance.InTime <= finalDateTime)
{ {
attendance.OutTime = finalDateTime; attendance.OutTime = finalDateTime;
attendance.Activity = ATTENDANCE_MARK_TYPE.REQUEST_REGULARIZE; attendance.Activity = ATTENDANCE_MARK_TYPE.REQUEST_REGULARIZE;
attendance.RequestedAt = DateTime.UtcNow;
attendance.RequestedById = currentEmployee.Id;
} }
else else
{ {
@ -477,12 +484,15 @@ namespace MarcoBMS.Services.Controllers
attendance.IsApproved = true; attendance.IsApproved = true;
attendance.Activity = ATTENDANCE_MARK_TYPE.REGULARIZE; attendance.Activity = ATTENDANCE_MARK_TYPE.REGULARIZE;
attendance.ApprovedById = currentEmployee.Id; attendance.ApprovedById = currentEmployee.Id;
attendance.ApprovedAt = DateTime.UtcNow;
// do nothing // do nothing
} }
else if (recordAttendanceDot.Action == ATTENDANCE_MARK_TYPE.REGULARIZE_REJECT) else if (recordAttendanceDot.Action == ATTENDANCE_MARK_TYPE.REGULARIZE_REJECT)
{ {
attendance.IsApproved = false; attendance.IsApproved = false;
attendance.Activity = ATTENDANCE_MARK_TYPE.REGULARIZE_REJECT; attendance.Activity = ATTENDANCE_MARK_TYPE.REGULARIZE_REJECT;
attendance.ApprovedById = currentEmployee.Id;
attendance.ApprovedAt = DateTime.UtcNow;
// do nothing // do nothing
} }
attendance.Date = DateTime.UtcNow; attendance.Date = DateTime.UtcNow;
@ -493,7 +503,7 @@ namespace MarcoBMS.Services.Controllers
else else
{ {
attendance = new Attendance(); attendance = new Attendance();
attendance.TenantId = TenantId; attendance.TenantId = tenantId;
attendance.AttendanceDate = recordAttendanceDot.Date; attendance.AttendanceDate = recordAttendanceDot.Date;
// attendance.Activity = recordAttendanceDot.Action; // attendance.Activity = recordAttendanceDot.Action;
attendance.Comment = recordAttendanceDot.Comment; attendance.Comment = recordAttendanceDot.Comment;
@ -525,7 +535,7 @@ namespace MarcoBMS.Services.Controllers
Latitude = recordAttendanceDot.Latitude, Latitude = recordAttendanceDot.Latitude,
Longitude = recordAttendanceDot.Longitude, Longitude = recordAttendanceDot.Longitude,
TenantId = TenantId, TenantId = tenantId,
UpdatedBy = currentEmployee.Id, UpdatedBy = currentEmployee.Id,
UpdatedOn = recordAttendanceDot.Date UpdatedOn = recordAttendanceDot.Date
}; };
@ -549,6 +559,7 @@ namespace MarcoBMS.Services.Controllers
CheckOutTime = attendance.OutTime, CheckOutTime = attendance.OutTime,
EmployeeAvatar = null, EmployeeAvatar = null,
EmployeeId = recordAttendanceDot.EmployeeID, EmployeeId = recordAttendanceDot.EmployeeID,
ProjectId = attendance.ProjectID,
FirstName = employee.FirstName, FirstName = employee.FirstName,
LastName = employee.LastName, LastName = employee.LastName,
Id = attendance.Id, Id = attendance.Id,
@ -569,10 +580,10 @@ namespace MarcoBMS.Services.Controllers
// --- Push Notification Section --- // --- Push Notification Section ---
// This section attempts to send a test push notification to the user's device. // This section attempts to send a test push notification to the user's device.
// It's designed to fail gracefully and handle invalid Firebase Cloud Messaging (FCM) tokens. // It's designed to fail gracefully and handle invalid Firebase Cloud Messaging (FCM) tokens.
var _firebase = scope.ServiceProvider.GetRequiredService<IFirebaseService>();
var name = $"{vm.FirstName} {vm.LastName}"; 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 +619,12 @@ namespace MarcoBMS.Services.Controllers
return BadRequest(ApiResponse<object>.ErrorResponse("Invalid data", errors, 400)); return BadRequest(ApiResponse<object>.ErrorResponse("Invalid data", errors, 400));
} }
Guid tenantId = GetTenantId(); using var scope = _serviceScopeFactory.CreateScope();
var _s3Service = scope.ServiceProvider.GetRequiredService<S3UploadService>();
var _employeeHelper = scope.ServiceProvider.GetRequiredService<EmployeeHelper>();
var _firebase = scope.ServiceProvider.GetRequiredService<IFirebaseService>();
var _signalR = scope.ServiceProvider.GetRequiredService<IHubContext<MarcoHub>>();
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
var batchId = Guid.NewGuid(); var batchId = Guid.NewGuid();
@ -672,6 +688,8 @@ namespace MarcoBMS.Services.Controllers
{ {
attendance.OutTime = finalDateTime; attendance.OutTime = finalDateTime;
attendance.Activity = ATTENDANCE_MARK_TYPE.REQUEST_REGULARIZE; attendance.Activity = ATTENDANCE_MARK_TYPE.REQUEST_REGULARIZE;
attendance.RequestedAt = DateTime.UtcNow;
attendance.RequestedById = loggedInEmployee.Id;
} }
else else
{ {
@ -682,10 +700,14 @@ namespace MarcoBMS.Services.Controllers
case ATTENDANCE_MARK_TYPE.REGULARIZE: case ATTENDANCE_MARK_TYPE.REGULARIZE:
attendance.IsApproved = true; attendance.IsApproved = true;
attendance.Activity = ATTENDANCE_MARK_TYPE.REGULARIZE; attendance.Activity = ATTENDANCE_MARK_TYPE.REGULARIZE;
attendance.ApprovedAt = DateTime.UtcNow;
attendance.ApprovedById = loggedInEmployee.Id;
break; break;
case ATTENDANCE_MARK_TYPE.REGULARIZE_REJECT: case ATTENDANCE_MARK_TYPE.REGULARIZE_REJECT:
attendance.IsApproved = false; attendance.IsApproved = false;
attendance.Activity = ATTENDANCE_MARK_TYPE.REGULARIZE_REJECT; attendance.Activity = ATTENDANCE_MARK_TYPE.REGULARIZE_REJECT;
attendance.ApprovedAt = DateTime.UtcNow;
attendance.ApprovedById = loggedInEmployee.Id;
break; break;
} }
@ -751,6 +773,7 @@ namespace MarcoBMS.Services.Controllers
{ {
Id = attendance.Id, Id = attendance.Id,
EmployeeId = employee.Id, EmployeeId = employee.Id,
ProjectId = attendance.ProjectID,
FirstName = employee.FirstName, FirstName = employee.FirstName,
LastName = employee.LastName, LastName = employee.LastName,
CheckInTime = attendance.InTime, CheckInTime = attendance.InTime,
@ -813,56 +836,54 @@ namespace MarcoBMS.Services.Controllers
/// <summary> /// <summary>
/// Fetches attendance for an entire project team using a single, optimized database query. /// Fetches attendance for an entire project team using a single, optimized database query.
/// </summary> /// </summary>
private async Task<List<EmployeeAttendanceVM>> GetTeamAttendanceAsync(Guid tenantId, Guid projectId, Guid? organizationId, DateTime forDate, bool includeInactive) private async Task<List<EmployeeAttendanceVM>> GetTeamAttendanceAsync(Guid tenantId, Guid? projectId, Guid organizationId, DateTime forDate, bool includeInactive)
{ {
// This single query joins ProjectAllocations with Employees and performs a LEFT JOIN with Attendances. // This 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. // This is far more efficient than fetching collections and joining them in memory.
var query = _context.ProjectAllocations var query = _context.Employees
.Include(pa => pa.Employee) .Include(e => e!.Organization)
.ThenInclude(e => e!.Organization) .Include(e => e!.JobRole)
.Include(pa => pa.Employee) .Where(e => e.OrganizationId == organizationId && e.Organization != null && e.JobRole != null && e.IsActive);
.ThenInclude(e => e!.JobRole)
.Where(pa => pa.TenantId == tenantId && pa.ProjectId == projectId);
// Apply filters based on optional parameters
if (!includeInactive) var lstAttendanceQuery = _context.Attendes.Where(c => c.AttendanceDate.Date == forDate && c.TenantId == tenantId);
if (projectId.HasValue)
{ {
query = query.Where(pa => pa.IsActive); lstAttendanceQuery = lstAttendanceQuery.Where(a => a.ProjectID == projectId);
}
if (organizationId.HasValue)
{
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 lstAttendanceQuery.ToListAsync();
var teamAttendance = await query var employees = await query
.AsNoTracking() .AsNoTracking()
.ToListAsync(); .ToListAsync();
var response = teamAttendance var projectIds = lstAttendance.Select(a => a.ProjectID).ToList();
.Select(teamMember => var projects = await _context.Projects.Where(p => projectIds.Contains(p.Id) && p.TenantId == tenantId).ToListAsync();
var response = employees
.Select(employee =>
{ {
var result1 = new EmployeeAttendanceVM() var result1 = new EmployeeAttendanceVM()
{ {
EmployeeAvatar = null, EmployeeAvatar = null,
EmployeeId = teamMember.EmployeeId, EmployeeId = employee.Id,
FirstName = teamMember.Employee?.FirstName, FirstName = employee.FirstName,
LastName = teamMember.Employee?.LastName, LastName = employee.LastName,
OrganizationName = teamMember.Employee?.Organization?.Name, OrganizationName = employee.Organization!.Name,
JobRoleName = teamMember.Employee?.JobRole?.Name, JobRoleName = employee.JobRole!.Name,
}; };
//var member = emp.Where(e => e.Id == teamMember.EmployeeId); var attendance = lstAttendance.Find(x => x.EmployeeId == employee.Id) ?? new Attendance();
var attendance = lstAttendance.Find(x => x.EmployeeId == teamMember.EmployeeId) ?? new Attendance();
if (attendance != null) if (attendance != null)
{ {
result1.Id = attendance.Id; result1.Id = attendance.Id;
result1.ProjectId = attendance.ProjectID;
result1.CheckInTime = attendance.InTime; result1.CheckInTime = attendance.InTime;
result1.CheckOutTime = attendance.OutTime; result1.CheckOutTime = attendance.OutTime;
result1.Activity = attendance.Activity; result1.Activity = attendance.Activity;
result1.ProjectName = projects.Where(p => p.Id == attendance.ProjectID).Select(p => p.Name).FirstOrDefault();
} }
return result1; return result1;
}) })
@ -875,43 +896,51 @@ namespace MarcoBMS.Services.Controllers
/// <summary> /// <summary>
/// Fetches a single attendance record for the logged-in employee. /// Fetches a single attendance record for the logged-in employee.
/// </summary> /// </summary>
private async Task<List<EmployeeAttendanceVM>> GetSelfAttendanceAsync(Guid tenantId, Guid projectId, Guid employeeId, Guid? organizationId, DateTime forDate) private async Task<List<EmployeeAttendanceVM>> GetSelfAttendanceAsync(Guid tenantId, Guid? projectId, Guid employeeId, DateTime forDate)
{ {
List<EmployeeAttendanceVM> result = new List<EmployeeAttendanceVM>(); List<EmployeeAttendanceVM> result = new List<EmployeeAttendanceVM>();
// This query fetches the employee's project allocation and their attendance in a single trip. // This query fetches the employee's project allocation and their attendance in a single trip.
Attendance lstAttendance = await _context.Attendes var lstAttendanceQuery = _context.Attendes
.FirstOrDefaultAsync(c => c.ProjectID == projectId && c.EmployeeId == employeeId && c.AttendanceDate.Date == forDate && c.TenantId == tenantId) ?? new Attendance(); .Where(c => c.EmployeeId == employeeId && c.AttendanceDate.Date == forDate && c.TenantId == tenantId);
var projectAllocationQuery = _context.ProjectAllocations if (projectId.HasValue)
.Include(pa => pa.Employee)
.ThenInclude(e => e!.Organization)
.Where(pa => pa.ProjectId == projectId && pa.EmployeeId == employeeId && pa.TenantId == tenantId && pa.IsActive);
if (organizationId.HasValue)
{ {
projectAllocationQuery = projectAllocationQuery.Where(pa => pa.Employee != null && pa.Employee.OrganizationId == organizationId); lstAttendanceQuery = lstAttendanceQuery.Where(a => a.ProjectID == projectId);
} }
var projectAllocation = await projectAllocationQuery.FirstOrDefaultAsync(); List<Attendance> lstAttendances = await lstAttendanceQuery.ToListAsync() ?? new List<Attendance>();
if (projectAllocation != null) var projectIds = lstAttendances.Select(a => a.ProjectID).ToList();
var projects = await _context.Projects.Where(p => projectIds.Contains(p.Id) && p.TenantId == tenantId).ToListAsync();
var employee = await _context.Employees
.Include(e => e.Organization)
.Include(e => e.JobRole)
.FirstOrDefaultAsync(e => e.Id == employeeId && e.IsActive);
if (employee != null && employee.JobRole != null && employee.Organization != null)
{
foreach (var lstAttendance in lstAttendances)
{ {
EmployeeAttendanceVM result1 = new EmployeeAttendanceVM EmployeeAttendanceVM result1 = new EmployeeAttendanceVM
{ {
Id = lstAttendance.Id, Id = lstAttendance.Id,
EmployeeAvatar = null, EmployeeAvatar = null,
EmployeeId = projectAllocation.EmployeeId, ProjectId = lstAttendance.ProjectID,
FirstName = projectAllocation.Employee?.FirstName, EmployeeId = employee.Id,
OrganizationName = projectAllocation.Employee?.Organization?.Name, FirstName = employee.FirstName,
LastName = projectAllocation.Employee?.LastName, OrganizationName = employee.Organization.Name,
JobRoleName = projectAllocation.Employee?.JobRole?.Name, ProjectName = projects.Where(p => p.Id == lstAttendance.ProjectID).Select(p => p.Name).FirstOrDefault(),
LastName = employee.LastName,
JobRoleName = employee.JobRole.Name,
CheckInTime = lstAttendance.InTime, CheckInTime = lstAttendance.InTime,
CheckOutTime = lstAttendance.OutTime, CheckOutTime = lstAttendance.OutTime,
Activity = lstAttendance.Activity Activity = lstAttendance.Activity
}; };
result.Add(result1); result.Add(result1);
} }
}
return result; return result;
} }

View File

@ -59,7 +59,7 @@ namespace MarcoBMS.Services.Controllers
{ {
var user = await _context.ApplicationUsers var user = await _context.ApplicationUsers
.FirstOrDefaultAsync(u => u.Email == loginDto.Username || u.PhoneNumber == loginDto.Username); .FirstOrDefaultAsync(u => u.Email == loginDto.Username || u.UserName == loginDto.Username);
if (user == null) if (user == null)
{ {
@ -103,9 +103,13 @@ namespace MarcoBMS.Services.Controllers
return NotFound(ApiResponse<object>.ErrorResponse("Username not found", "Username not found", 404)); return NotFound(ApiResponse<object>.ErrorResponse("Username not found", "Username not found", 404));
} }
var tenants = await _context.Tenants.Where(t => t.OrganizationId == emp.OrganizationId).ToListAsync();
var tenant = tenants.OrderBy(t => t.OnBoardingDate).FirstOrDefault();
// Generate tokens // Generate tokens
var token = _refreshTokenService.GenerateJwtToken(user.UserName, emp.TenantId ?? Guid.Empty, emp.OrganizationId, _jwtSettings); var token = _refreshTokenService.GenerateJwtToken(user.UserName, tenant?.Id ?? Guid.Empty, emp.OrganizationId, _jwtSettings);
var refreshToken = await _refreshTokenService.CreateRefreshToken(user.Id, emp.TenantId.ToString(), emp.OrganizationId, _jwtSettings); var refreshToken = await _refreshTokenService.CreateRefreshToken(user.Id, tenant?.Id.ToString(), emp.OrganizationId, _jwtSettings);
_logger.LogInfo("User login successful - UserId: {UserId}", user.Id); _logger.LogInfo("User login successful - UserId: {UserId}", user.Id);
return Ok(ApiResponse<object>.SuccessResponse(new return Ok(ApiResponse<object>.SuccessResponse(new
@ -201,12 +205,17 @@ namespace MarcoBMS.Services.Controllers
} }
_logger.LogInfo("Successfully found employee details for tenant ID: {TenantId}", emp.TenantId ?? Guid.Empty); _logger.LogInfo("Successfully found employee details for tenant ID: {TenantId}", emp.TenantId ?? Guid.Empty);
var tenants = await _context.Tenants.Where(t => t.OrganizationId == emp.OrganizationId).ToListAsync();
var tenant = tenants.OrderBy(t => t.OnBoardingDate).FirstOrDefault();
// Generate JWT token // Generate JWT token
var token = _refreshTokenService.GenerateJwtToken(user.UserName, emp.TenantId ?? Guid.Empty, emp.OrganizationId, _jwtSettings); var token = _refreshTokenService.GenerateJwtToken(user.UserName, tenant?.Id ?? Guid.Empty, emp.OrganizationId, _jwtSettings);
// Generate a new refresh token and store it in the database. // Generate a new refresh token and store it in the database.
_logger.LogInfo("Generating and storing Refresh Token for user: {Username}", user.UserName); _logger.LogInfo("Generating and storing Refresh Token for user: {Username}", user.UserName);
var refreshToken = await _refreshTokenService.CreateRefreshToken(user.Id, emp.TenantId.ToString(), emp.OrganizationId, _jwtSettings); var refreshToken = await _refreshTokenService.CreateRefreshToken(user.Id, tenant?.Id.ToString(), emp.OrganizationId, _jwtSettings);
// Fetch MPIN Token // Fetch MPIN Token
var mpinToken = await _context.MPINDetails.FirstOrDefaultAsync(p => p.UserId == Guid.Parse(user.Id)); var mpinToken = await _context.MPINDetails.FirstOrDefaultAsync(p => p.UserId == Guid.Parse(user.Id));
@ -264,31 +273,32 @@ namespace MarcoBMS.Services.Controllers
} }
string? tokenType = claimsPrincipal.FindFirst("token_type")?.Value; string? tokenType = claimsPrincipal.FindFirst("token_type")?.Value;
string? tokenTenantId = claimsPrincipal.FindFirst("TenantId")?.Value;
string? tokenUserId = claimsPrincipal.FindFirst(ClaimTypes.NameIdentifier)?.Value; string? tokenUserId = claimsPrincipal.FindFirst(ClaimTypes.NameIdentifier)?.Value;
// Validate essential claims // Validate essential claims
if (string.IsNullOrWhiteSpace(tokenType) || string.IsNullOrWhiteSpace(tokenTenantId) || string.IsNullOrWhiteSpace(tokenUserId)) if (string.IsNullOrWhiteSpace(tokenType) || string.IsNullOrWhiteSpace(tokenUserId))
{ {
_logger.LogWarning("MPIN token claims are incomplete"); _logger.LogWarning("MPIN token claims are incomplete");
return Unauthorized(ApiResponse<object>.ErrorResponse("Invalid token claims", "MPIN token does not match your identity", 401)); return Unauthorized(ApiResponse<object>.ErrorResponse("Invalid token claims", "MPIN token does not match your identity", 401));
} }
Guid tenantId = Guid.Parse(tokenTenantId);
// Fetch employee by ID and tenant // Fetch employee by ID and tenant
var requestEmployee = await _context.Employees var requestEmployee = await _context.Employees
.Include(e => e.ApplicationUser) .Include(e => e.ApplicationUser)
.FirstOrDefaultAsync(e => e.Id == verifyMPIN.EmployeeId && e.TenantId == tenantId && e.ApplicationUserId == tokenUserId && e.IsActive); .FirstOrDefaultAsync(e => e.Id == verifyMPIN.EmployeeId && e.HasApplicationAccess && e.ApplicationUserId == tokenUserId && e.IsActive);
if (requestEmployee == null || string.IsNullOrWhiteSpace(requestEmployee.ApplicationUserId)) if (requestEmployee == null || string.IsNullOrWhiteSpace(requestEmployee.ApplicationUserId))
{ {
_logger.LogWarning("Employee not found or invalid for verification - EmployeeId: {EmployeeId}", verifyMPIN.EmployeeId); _logger.LogWarning("Employee not found or invalid for verification - EmployeeId: {EmployeeId}", verifyMPIN.EmployeeId);
return BadRequest(ApiResponse<object>.ErrorResponse("Invalid request", "Provided invalid employee information", 400)); return BadRequest(ApiResponse<object>.ErrorResponse("Invalid request", "Provided invalid employee information", 400));
} }
var tenants = await _context.Tenants.Where(t => t.OrganizationId == requestEmployee.OrganizationId).ToListAsync();
var tenant = tenants.OrderBy(t => t.OnBoardingDate).FirstOrDefault();
Guid tenantId = tenant?.Id ?? Guid.Empty;
// Validate that the token belongs to the same employee making the request // Validate that the token belongs to the same employee making the request
if (requestEmployee.ApplicationUserId != tokenUserId || tokenType != "mpin") if (requestEmployee.ApplicationUserId != tokenUserId || tokenType != "mpin" || tenantId == Guid.Empty)
{ {
_logger.LogWarning("Token identity does not match employee info - EmployeeId: {EmployeeId}", requestEmployee.Id); _logger.LogWarning("Token identity does not match employee info - EmployeeId: {EmployeeId}", requestEmployee.Id);
return Unauthorized(ApiResponse<object>.ErrorResponse("Unauthorized", "MPIN token does not match your identity", 401)); return Unauthorized(ApiResponse<object>.ErrorResponse("Unauthorized", "MPIN token does not match your identity", 401));
@ -319,36 +329,6 @@ namespace MarcoBMS.Services.Controllers
return Unauthorized(ApiResponse<object>.ErrorResponse("MPIN mismatch", "MPIN did not match", 401)); 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 // Generate new tokens
var jwtToken = _refreshTokenService.GenerateJwtToken(requestEmployee.Email, tenantId, requestEmployee.OrganizationId, _jwtSettings); var jwtToken = _refreshTokenService.GenerateJwtToken(requestEmployee.Email, tenantId, requestEmployee.OrganizationId, _jwtSettings);
var refreshToken = await _refreshTokenService.CreateRefreshToken(requestEmployee.ApplicationUserId, tenantId.ToString(), requestEmployee.OrganizationId, _jwtSettings); var refreshToken = await _refreshTokenService.CreateRefreshToken(requestEmployee.ApplicationUserId, tenantId.ToString(), requestEmployee.OrganizationId, _jwtSettings);
@ -431,7 +411,9 @@ namespace MarcoBMS.Services.Controllers
//var accessToken = _refreshTokenService.GenerateJwtTokenWithOrganization(requestEmployee.ApplicationUser?.UserName, requestEmployee.OrganizationId, _jwtSettings); //var accessToken = _refreshTokenService.GenerateJwtTokenWithOrganization(requestEmployee.ApplicationUser?.UserName, requestEmployee.OrganizationId, _jwtSettings);
//var refreshToken = await _refreshTokenService.CreateRefreshTokenWithOrganization(requestEmployee.ApplicationUserId, requestEmployee.OrganizationId, _jwtSettings); //var refreshToken = await _refreshTokenService.CreateRefreshTokenWithOrganization(requestEmployee.ApplicationUserId, requestEmployee.OrganizationId, _jwtSettings);
var tenant = await _context.Tenants.FirstOrDefaultAsync(t => t.OrganizationId == requestEmployee.OrganizationId); var tenants = await _context.Tenants.Where(t => t.OrganizationId == requestEmployee.OrganizationId).ToListAsync();
var tenant = tenants.OrderBy(t => t.OnBoardingDate).FirstOrDefault();
var accessToken = _refreshTokenService.GenerateJwtToken(requestEmployee.ApplicationUser?.UserName, var accessToken = _refreshTokenService.GenerateJwtToken(requestEmployee.ApplicationUser?.UserName,
tenant?.Id ?? Guid.Empty, requestEmployee.OrganizationId, _jwtSettings); tenant?.Id ?? Guid.Empty, requestEmployee.OrganizationId, _jwtSettings);

View File

@ -2,6 +2,8 @@
using Marco.Pms.Model.Activities; using Marco.Pms.Model.Activities;
using Marco.Pms.Model.Dtos.Attendance; using Marco.Pms.Model.Dtos.Attendance;
using Marco.Pms.Model.Employees; using Marco.Pms.Model.Employees;
using Marco.Pms.Model.Entitlements;
using Marco.Pms.Model.Expenses;
using Marco.Pms.Model.Projects; using Marco.Pms.Model.Projects;
using Marco.Pms.Model.Utilities; using Marco.Pms.Model.Utilities;
using Marco.Pms.Model.ViewModels.DashBoard; using Marco.Pms.Model.ViewModels.DashBoard;
@ -12,6 +14,7 @@ using MarcoBMS.Services.Service;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using System.Globalization;
namespace Marco.Pms.Services.Controllers namespace Marco.Pms.Services.Controllers
{ {
@ -25,19 +28,35 @@ namespace Marco.Pms.Services.Controllers
private readonly IProjectServices _projectServices; private readonly IProjectServices _projectServices;
private readonly ILoggingService _logger; private readonly ILoggingService _logger;
private readonly PermissionServices _permissionServices; private readonly PermissionServices _permissionServices;
private readonly IServiceScopeFactory _serviceScopeFactory;
public static readonly Guid ActiveId = Guid.Parse("b74da4c2-d07e-46f2-9919-e75e49b12731"); public static readonly Guid ActiveId = Guid.Parse("b74da4c2-d07e-46f2-9919-e75e49b12731");
public DashboardController(ApplicationDbContext context, UserHelper userHelper, IProjectServices projectServices, ILoggingService logger, PermissionServices permissionServices) private static readonly Guid Draft = Guid.Parse("297e0d8f-f668-41b5-bfea-e03b354251c8");
private static readonly Guid Review = Guid.Parse("6537018f-f4e9-4cb3-a210-6c3b2da999d7");
private static readonly Guid Approve = Guid.Parse("4068007f-c92f-4f37-a907-bc15fe57d4d8");
private static readonly Guid ProcessPending = Guid.Parse("f18c5cfd-7815-4341-8da2-2c2d65778e27");
private static readonly Guid Processed = Guid.Parse("61578360-3a49-4c34-8604-7b35a3787b95");
private static readonly Guid RejectedByReviewer = Guid.Parse("965eda62-7907-4963-b4a1-657fb0b2724b");
private static readonly Guid RejectedByApprover = Guid.Parse("d1ee5eec-24b6-4364-8673-a8f859c60729");
private readonly Guid tenantId;
public DashboardController(ApplicationDbContext context,
UserHelper userHelper,
IProjectServices projectServices,
IServiceScopeFactory serviceScopeFactory,
ILoggingService logger,
PermissionServices permissionServices)
{ {
_context = context; _context = context;
_userHelper = userHelper; _userHelper = userHelper;
_projectServices = projectServices; _projectServices = projectServices;
_logger = logger; _logger = logger;
_serviceScopeFactory = serviceScopeFactory;
_permissionServices = permissionServices; _permissionServices = permissionServices;
tenantId = userHelper.GetTenantId();
} }
[HttpGet("progression")] [HttpGet("progression")]
public async Task<IActionResult> GetGraph([FromQuery] double days, [FromQuery] string FromDate, [FromQuery] Guid? projectId) public async Task<IActionResult> GetGraph([FromQuery] double days, [FromQuery] string FromDate, [FromQuery] Guid? projectId)
{ {
var tenantId = _userHelper.GetTenantId();
var LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); var LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
DateTime fromDate = new DateTime(); DateTime fromDate = new DateTime();
@ -149,7 +168,6 @@ namespace Marco.Pms.Services.Controllers
[HttpGet("projects")] [HttpGet("projects")]
public async Task<IActionResult> GetProjectCount() public async Task<IActionResult> GetProjectCount()
{ {
var tenantId = _userHelper.GetTenantId();
var LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); var LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
var projects = await _context.Projects.Where(p => p.TenantId == tenantId).ToListAsync(); var projects = await _context.Projects.Where(p => p.TenantId == tenantId).ToListAsync();
@ -176,7 +194,6 @@ namespace Marco.Pms.Services.Controllers
{ {
try try
{ {
var tenantId = _userHelper.GetTenantId();
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
_logger.LogInfo("GetTotalEmployees called by user {UserId} for ProjectId: {ProjectId}", loggedInEmployee.Id, projectId ?? Guid.Empty); _logger.LogInfo("GetTotalEmployees called by user {UserId} for ProjectId: {ProjectId}", loggedInEmployee.Id, projectId ?? Guid.Empty);
@ -269,7 +286,6 @@ namespace Marco.Pms.Services.Controllers
{ {
try try
{ {
var tenantId = _userHelper.GetTenantId();
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
_logger.LogInfo("GetTotalTasks called by user {UserId} for ProjectId: {ProjectId}", loggedInEmployee.Id, projectId ?? Guid.Empty); _logger.LogInfo("GetTotalTasks called by user {UserId} for ProjectId: {ProjectId}", loggedInEmployee.Id, projectId ?? Guid.Empty);
@ -348,10 +364,10 @@ namespace Marco.Pms.Services.Controllers
return StatusCode(500, ApiResponse<object>.ErrorResponse("An internal server error occurred.", null, 500)); return StatusCode(500, ApiResponse<object>.ErrorResponse("An internal server error occurred.", null, 500));
} }
} }
[HttpGet("pending-attendance")] [HttpGet("pending-attendance")]
public async Task<IActionResult> GetPendingAttendance() public async Task<IActionResult> GetPendingAttendance()
{ {
Guid tenantId = _userHelper.GetTenantId();
var LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); var LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
var attendance = await _context.Attendes.Where(a => a.EmployeeId == LoggedInEmployee.Id && a.TenantId == tenantId).ToListAsync(); var attendance = await _context.Attendes.Where(a => a.EmployeeId == LoggedInEmployee.Id && a.TenantId == tenantId).ToListAsync();
@ -374,7 +390,6 @@ namespace Marco.Pms.Services.Controllers
[HttpGet("project-attendance/{projectId}")] [HttpGet("project-attendance/{projectId}")]
public async Task<IActionResult> GetProjectAttendance(Guid projectId, [FromQuery] string? date) public async Task<IActionResult> GetProjectAttendance(Guid projectId, [FromQuery] string? date)
{ {
Guid tenantId = _userHelper.GetTenantId();
var LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); var LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
DateTime currentDate = DateTime.UtcNow; DateTime currentDate = DateTime.UtcNow;
@ -428,7 +443,6 @@ namespace Marco.Pms.Services.Controllers
[HttpGet("activities/{projectId}")] [HttpGet("activities/{projectId}")]
public async Task<IActionResult> GetActivities(Guid projectId, [FromQuery] string? date) public async Task<IActionResult> GetActivities(Guid projectId, [FromQuery] string? date)
{ {
Guid tenantId = _userHelper.GetTenantId();
var LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); var LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
DateTime currentDate = DateTime.UtcNow; DateTime currentDate = DateTime.UtcNow;
@ -600,5 +614,317 @@ namespace Marco.Pms.Services.Controllers
return Ok(ApiResponse<object>.SuccessResponse(sortedResult, $"{sortedResult.Count} records fetched for attendance overview", 200)); return Ok(ApiResponse<object>.SuccessResponse(sortedResult, $"{sortedResult.Count} records fetched for attendance overview", 200));
} }
[HttpGet("expense/monthly")]
public async Task<IActionResult> GetExpenseReportByProjectsAsync([FromQuery] Guid? projectId, [FromQuery] Guid? categoryId, [FromQuery] int months)
{
try
{
// Read-only base filter with tenant scope and non-draft
var baseQuery = _context.Expenses
.AsNoTracking()
.Where(e =>
e.TenantId == tenantId
&& e.IsActive
&& e.StatusId != Draft); // [Server Filters]
if (months != 0)
{
months = 0 - months;
var end = DateTime.UtcNow.Date;
var start = end.AddMonths(months); // inclusive EOD
baseQuery = baseQuery.Where(e => e.TransactionDate >= start
&& e.TransactionDate <= end);
}
if (projectId.HasValue)
baseQuery = baseQuery.Where(e => e.ProjectId == projectId);
if (categoryId.HasValue)
baseQuery = baseQuery.Where(e => e.ExpensesTypeId == categoryId);
// Single server-side group/aggregate by project
var report = await baseQuery
.AsNoTracking()
.GroupBy(e => new { e.TransactionDate.Year, e.TransactionDate.Month })
.Select(g => new
{
Year = g.Key.Year,
Month = g.Key.Month,
Total = g.Sum(x => x.Amount),
Count = g.Count()
})
.OrderBy(x => x.Year).ThenBy(x => x.Month)
.ToListAsync();
var culture = CultureInfo.GetCultureInfo("en-IN"); // pick desired locale
var response = report
.Select(x => new
{
MonthName = culture.DateTimeFormat.GetMonthName(x.Month), // e.g., "January"
Year = x.Year,
Total = x.Total,
Count = x.Count
}).ToList();
_logger.LogInfo(
"GetExpenseReportByProjects completed. TenantId={TenantId}, Rows={Rows}",
tenantId, report.Count); // [Completion Log]
return Ok(ApiResponse<object>.SuccessResponse(response, "Expense report by project fetched successfully", 200)); // [Success Response]
}
catch (OperationCanceledException)
{
_logger.LogWarning("GetExpenseReportByProjects canceled by client. TenantId={TenantId}", tenantId); // [Cancel Log]
return StatusCode(499, ApiResponse<object>.ErrorResponse("Client has canceled the opration", "Client has canceled the opration", 499)); // [Cancel Response]
}
catch (Exception ex)
{
_logger.LogError(ex,
"GetExpenseReportByProjects failed. TenantId={TenantId}",
tenantId); // [Error Log]
return StatusCode(500,
ApiResponse<object>.ErrorResponse("An error occurred while fetching the expense report.", 500)); // [Error Response]
}
}
[HttpGet("expense/type")]
public async Task<IActionResult> GetExpenseReportByExpenseTypeAsync([FromQuery] Guid? projectId, [FromQuery] DateTime startDate, [FromQuery] DateTime endDate)
{
// Structured log: entering action with filters
_logger.LogDebug(
"GetExpenseReportByExpenseType started. TenantId={TenantId}, ProjectId={ProjectId}, StartDate={StartDate}, EndDate={EndDate}",
tenantId, projectId ?? Guid.Empty, startDate, endDate); // [Start Log] [memory:4][memory:1]
try
{
// Compose base query: push filters to DB, avoid client evaluation
IQueryable<Expenses> baseQuery = _context.Expenses
.AsNoTracking() // Reduce tracking overhead for read-only endpoint
.Where(e => e.TenantId == tenantId
&& e.IsActive
&& e.StatusId != Draft
&& e.TransactionDate >= startDate
&& e.TransactionDate <= endDate.AddDays(1).AddTicks(-1));
if (projectId.HasValue)
baseQuery = baseQuery.Where(e => e.ProjectId == projectId.Value); // [Filter] [memory:7]
// Project to a minimal shape before grouping to avoid loading navigation graphs
// Group by expense type name; adjust to the correct key if ExpensesCategory is an enum or navigation
var query = baseQuery
.Where(e => e.ExpensesType != null)
.Select(e => new
{
ExpenseTypeName = e.ExpensesType!.Name, // If enum, use e.ExpensesCategory.ToString()
Amount = e.Amount,
StatusId = e.StatusId
})
.GroupBy(x => x.ExpenseTypeName)
.Select(g => new
{
ProjectName = g.Key, // Original code used g.Key!.Name; here the grouping key is already a string
TotalApprovedAmount = g.Where(x => x.StatusId == Processed
|| x.StatusId == ProcessPending).Sum(x => x.Amount),
TotalPendingAmount = g.Where(x => x.StatusId != Processed
&& x.StatusId != RejectedByReviewer
&& x.StatusId != RejectedByApprover)
.Sum(x => x.Amount),
TotalRejectedAmount = g.Where(x => x.StatusId == RejectedByReviewer
|| x.StatusId == RejectedByApprover)
.Sum(x => x.Amount),
TotalProcessedAmount = g.Where(x => x.StatusId == Processed)
.Sum(x => x.Amount)
})
.OrderBy(r => r.ProjectName); // Server-side order [memory:7]
var report = await query.ToListAsync(); // Single round-trip [memory:7]
var response = new
{
Report = report,
TotalAmount = report.Sum(r => r.TotalApprovedAmount)
};
_logger.LogInfo(
"GetExpenseReportByExpenseType completed. TenantId={TenantId}, Filters: ProjectId={ProjectId}, StartDate={StartDate}, EndDate={EndDate}, Rows={RowCount}, TotalAmount={TotalAmount}",
tenantId, projectId ?? Guid.Empty, startDate, endDate, report.Count, response.TotalAmount); // [Completion Log] [memory:4]
return Ok(ApiResponse<object>.SuccessResponse(response, "Expense report by expense type fetched successfully", 200)); // [Success Response] [memory:1]
}
catch (OperationCanceledException)
{
_logger.LogWarning("GetExpenseReportByExpenseType canceled by client. TenantId={TenantId}", tenantId); // [Cancel Log] [memory:4]
return StatusCode(499, ApiResponse<object>.ErrorResponse("Client has canceled the opration", "Client has canceled the opration", 499)); // [Cancel Response] [memory:1]
}
catch (Exception ex)
{
_logger.LogError(ex,
"GetExpenseReportByExpenseType failed. TenantId={TenantId}, ProjectId={ProjectId}, StartDate={StartDate}, EndDate={EndDate}",
tenantId, projectId ?? Guid.Empty, startDate, endDate); // [Error Log] [memory:4]
return StatusCode(StatusCodes.Status500InternalServerError,
ApiResponse<object>.ErrorResponse("An error occurred while fetching the expense report.", 500)); // [Error Response] [memory:1]
}
}
[HttpGet("expense/pendings")]
public async Task<IActionResult> GetPendingExpenseListAsync([FromQuery] Guid? projectId)
{
// Start log with correlation fields
_logger.LogDebug(
"GetPendingExpenseListAsync started. Project={ProjectId} TenantId={TenantId}", projectId ?? Guid.Empty, tenantId); // [Start Log]
try
{
// Resolve current employee once; avoid using scoped services inside Task.Run
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); // [User Context]
// Resolve permission service from current scope once
using var scope = _serviceScopeFactory.CreateScope();
// Fire permission checks concurrently without Task.Run; these are async I/O methods
var hasReviewPermissionTask = Task.Run(async () =>
{
var _permission = scope.ServiceProvider.GetRequiredService<PermissionServices>();
return await _permission.HasPermission(PermissionsMaster.ExpenseReview, loggedInEmployee.Id);
});
var hasApprovePermissionTask = Task.Run(async () =>
{
var _permission = scope.ServiceProvider.GetRequiredService<PermissionServices>();
return await _permission.HasPermission(PermissionsMaster.ExpenseApprove, loggedInEmployee.Id);
});
var hasProcessPermissionTask = Task.Run(async () =>
{
var _permission = scope.ServiceProvider.GetRequiredService<PermissionServices>();
return await _permission.HasPermission(PermissionsMaster.ExpenseProcess, loggedInEmployee.Id);
});
var hasManagePermissionTask = Task.Run(async () =>
{
var _permission = scope.ServiceProvider.GetRequiredService<PermissionServices>();
return await _permission.HasPermission(PermissionsMaster.ExpenseManage, loggedInEmployee.Id);
});
await Task.WhenAll(hasReviewPermissionTask, hasApprovePermissionTask, hasProcessPermissionTask, hasManagePermissionTask); // [Parallel Await]
var hasReviewPermission = hasReviewPermissionTask.Result;
var hasApprovePermission = hasApprovePermissionTask.Result;
var hasProcessPermission = hasProcessPermissionTask.Result;
var hasManagePermission = hasManagePermissionTask.Result;
_logger.LogInfo(
"Permissions resolved: Review={Review}, Approve={Approve}, Process={Process}",
hasReviewPermission, hasApprovePermission, hasProcessPermission); // [Permissions Log]
// Build base query: read-only, tenant-scoped
var baseQuery = _context.Expenses
.Include(e => e.Status)
.AsNoTracking() // Reduce tracking overhead for read-only list
.Where(e => e.IsActive && e.TenantId == tenantId && e.StatusId != Processed && e.Status != null); // [Base Filter]
// Project to DTO in SQL to avoid heavy Include graph.
if (projectId.HasValue)
baseQuery = baseQuery.Where(e => e.ProjectId == projectId);
// Prefer ProjectTo when profiles exist; otherwise project minimal fields
var expenses = await baseQuery
.ToListAsync(); // Single round-trip; no Include needed for this shape
var draftExpenses = expenses.Where(e => e.StatusId == Draft && e.CreatedById == loggedInEmployee.Id).ToList();
var reviewExpenses = expenses.Where(e => (hasReviewPermission || e.CreatedById == loggedInEmployee.Id) && e.StatusId == Review).ToList();
var approveExpenses = expenses.Where(e => (hasApprovePermission || e.CreatedById == loggedInEmployee.Id) && e.StatusId == Approve).ToList();
var processPendingExpenses = expenses.Where(e => (hasProcessPermission || e.CreatedById == loggedInEmployee.Id) && e.StatusId == ProcessPending).ToList();
var submitedExpenses = expenses.Where(e => e.StatusId != Draft && e.CreatedById == loggedInEmployee.Id).ToList();
var totalAmount = expenses.Where(e => e.StatusId != Draft).Sum(e => e.Amount);
if (hasManagePermission)
{
var response = new
{
Draft = new
{
Count = draftExpenses.Count,
TotalAmount = draftExpenses.Sum(e => e.Amount)
},
ReviewPending = new
{
Count = reviewExpenses.Count,
TotalAmount = reviewExpenses.Sum(e => e.Amount)
},
ApprovePending = new
{
Count = approveExpenses.Count,
TotalAmount = approveExpenses.Sum(e => e.Amount)
},
ProcessPending = new
{
Count = processPendingExpenses.Count,
TotalAmount = processPendingExpenses.Sum(e => e.Amount)
},
Submited = new
{
Count = submitedExpenses.Count,
TotalAmount = submitedExpenses.Sum(e => e.Amount)
},
TotalAmount = totalAmount
};
_logger.LogInfo(
"GetPendingExpenseListAsync completed. TenantId={TenantId}",
tenantId); // [Completion Log]
return Ok(ApiResponse<object>.SuccessResponse(response, "Pending Expenses fetched successfully", 200)); // [Success Response]
}
else
{
var response = new
{
Draft = new
{
Count = draftExpenses.Count
},
ReviewPending = new
{
Count = reviewExpenses.Count
},
ApprovePending = new
{
Count = approveExpenses.Count
},
ProcessPending = new
{
Count = processPendingExpenses.Count
},
Submited = new
{
Count = submitedExpenses.Count
},
TotalAmount = totalAmount
};
_logger.LogInfo(
"GetPendingExpenseListAsync completed. TenantId={TenantId}",
tenantId); // [Completion Log]
return Ok(ApiResponse<object>.SuccessResponse(response, "Pending Expenses fetched successfully", 200)); // [Success Response]
}
}
catch (OperationCanceledException)
{
_logger.LogWarning("GetPendingExpenseListAsync canceled by client. TenantId={TenantId}", tenantId); // [Cancel Log]
return StatusCode(499, ApiResponse<object>.ErrorResponse("Client has canceled the opration", "Client has canceled the opration", 499)); // [Cancel Response]
}
catch (Exception ex)
{
_logger.LogError(ex, "GetPendingExpenseListAsync failed. TenantId={TenantId}", tenantId); // [Error Log]
return StatusCode(500,
ApiResponse<object>.ErrorResponse("An error occurred while fetching pending expenses.", "An error occurred while fetching pending expenses.", 500)); // [Error Response]
}
}
} }
} }

View File

@ -33,25 +33,25 @@ namespace Marco.Pms.Services.Controllers
#region =================================================================== Contact Get APIs =================================================================== #region =================================================================== Contact Get APIs ===================================================================
[HttpGet("list")] [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) [FromQuery] int pageNumber = 1, [FromQuery] int pageSize = 20)
{ {
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); 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); return StatusCode(response.StatusCode, response);
} }
[HttpGet] [HttpGet]
public async Task<IActionResult> GetContactList([FromQuery] string? search, [FromQuery] List<Guid>? bucketIds, [FromQuery] List<Guid>? categoryIds, [FromQuery] Guid? projectId, [FromQuery] bool active = true) public async Task<IActionResult> GetContactList([FromQuery] string? searchString, [FromQuery] List<Guid>? bucketIds, [FromQuery] List<Guid>? categoryIds, [FromQuery] Guid? projectId, [FromQuery] bool active = true)
{ {
ContactFilterDto filterDto = new ContactFilterDto ContactFilterDto filterDto = new ContactFilterDto
{ {
BucketIds = bucketIds, BucketIds = bucketIds,
CategoryIds = categoryIds CategoryIds = categoryIds
}; };
var response = await _directoryService.GetListOfContactsOld(search, active, filterDto, projectId); var response = await _directoryService.GetListOfContactsOld(searchString, active, filterDto, projectId);
return StatusCode(response.StatusCode, response); return StatusCode(response.StatusCode, response);

View File

@ -35,6 +35,7 @@ namespace Marco.Pms.Services.Controllers
private readonly ILoggingService _logger; private readonly ILoggingService _logger;
private readonly IMapper _mapper; private readonly IMapper _mapper;
private readonly Guid tenantId; private readonly Guid tenantId;
private readonly Guid organizationId;
private static readonly Guid ProjectEntity = Guid.Parse("c8fe7115-aa27-43bc-99f4-7b05fabe436e"); private static readonly Guid ProjectEntity = Guid.Parse("c8fe7115-aa27-43bc-99f4-7b05fabe436e");
private static readonly Guid EmployeeEntity = Guid.Parse("dbb9555a-7a0c-40f2-a9ed-f0463f1ceed7"); private static readonly Guid EmployeeEntity = Guid.Parse("dbb9555a-7a0c-40f2-a9ed-f0463f1ceed7");
@ -52,6 +53,7 @@ namespace Marco.Pms.Services.Controllers
_logger = logger ?? throw new ArgumentNullException(nameof(logger)); _logger = logger ?? throw new ArgumentNullException(nameof(logger));
_mapper = mapper ?? throw new ArgumentNullException(nameof(mapper)); _mapper = mapper ?? throw new ArgumentNullException(nameof(mapper));
tenantId = userHelper.GetTenantId(); tenantId = userHelper.GetTenantId();
organizationId = _userHelper.GetCurrentOrganizationId();
} }
[HttpGet("list/{entityTypeId}/entity/{entityId}")] [HttpGet("list/{entityTypeId}/entity/{entityId}")]
@ -93,21 +95,21 @@ namespace Marco.Pms.Services.Controllers
return NotFound(ApiResponse<object>.ErrorResponse("Entity type not found", "Entity Type not found in database", 404)); return NotFound(ApiResponse<object>.ErrorResponse("Entity type not found", "Entity Type not found in database", 404));
} }
// Project permission check //// Project permission check
if (ProjectEntity == entityTypeId) //if (ProjectEntity == entityTypeId)
{ //{
var hasProjectPermission = await _permission.HasProjectPermission(loggedInEmployee, entityId); // var hasProjectPermission = await _permission.HasProjectPermission(loggedInEmployee, entityId);
if (!hasProjectPermission) // if (!hasProjectPermission)
{ // {
_logger.LogWarning("Employee {EmployeeId} does not have project access for ProjectId {ProjectId}", loggedInEmployee.Id, entityId); // _logger.LogWarning("Employee {EmployeeId} does not have project access for ProjectId {ProjectId}", loggedInEmployee.Id, entityId);
return StatusCode(403, ApiResponse<object>.ErrorResponse("Access Denied.", "You do not have permission to access project documents", 403)); // return StatusCode(403, ApiResponse<object>.ErrorResponse("Access Denied.", "You do not have permission to access project documents", 403));
} // }
} //}
// Employee validation // Employee validation
else if (EmployeeEntity == entityTypeId) else if (EmployeeEntity == entityTypeId)
{ {
var isEmployeeExists = await _context.Employees var isEmployeeExists = await _context.Employees
.AnyAsync(e => e.Id == entityId && e.TenantId == tenantId); .AnyAsync(e => e.Id == entityId && e.OrganizationId == organizationId);
if (!isEmployeeExists) if (!isEmployeeExists)
{ {
@ -691,7 +693,7 @@ namespace Marco.Pms.Services.Controllers
bool entityExists = false; bool entityExists = false;
if (entityType.Equals(EmployeeEntity)) if (entityType.Equals(EmployeeEntity))
{ {
entityExists = await _context.Employees.AnyAsync(e => e.Id == model.EntityId && e.TenantId == tenantId); entityExists = await _context.Employees.AnyAsync(e => e.Id == model.EntityId && e.OrganizationId == organizationId);
} }
else if (entityType.Equals(ProjectEntity)) else if (entityType.Equals(ProjectEntity))
{ {
@ -1078,15 +1080,15 @@ namespace Marco.Pms.Services.Controllers
bool entityExists; bool entityExists;
if (entityType.Equals(EmployeeEntity)) if (entityType.Equals(EmployeeEntity))
{ {
entityExists = await _context.Employees.AnyAsync(e => e.Id == oldAttachment.EntityId && e.TenantId == tenantId); entityExists = await _context.Employees.AnyAsync(e => e.Id == oldAttachment.EntityId && e.OrganizationId == organizationId);
} }
else if (entityType.Equals(ProjectEntity)) else if (entityType.Equals(ProjectEntity))
{ {
entityExists = await _context.Projects.AnyAsync(p => p.Id == oldAttachment.EntityId && p.TenantId == tenantId); entityExists = await _context.Projects.AnyAsync(p => p.Id == oldAttachment.EntityId && p.TenantId == tenantId);
if (entityExists) //if (entityExists)
{ //{
entityExists = await _permission.HasProjectPermission(loggedInEmployee, oldAttachment.EntityId); // entityExists = await _permission.HasProjectPermission(loggedInEmployee, oldAttachment.EntityId);
} //}
} }
else else
{ {

View File

@ -233,84 +233,12 @@ namespace MarcoBMS.Services.Controllers
_logger.LogInfo("GetEmployeesByProject called. EmployeeId: {EmployeeId}, ProjectId: {ProjectId}, showInactive: {ShowInactive}", _logger.LogInfo("GetEmployeesByProject called. EmployeeId: {EmployeeId}, ProjectId: {ProjectId}, showInactive: {ShowInactive}",
loggedInEmployee.Id, projectId ?? Guid.Empty, showInactive); loggedInEmployee.Id, projectId ?? Guid.Empty, showInactive);
// Step 3: Fetch permissions concurrently var employees = await _context.Employees
var viewAllTask = Task.Run(async () =>
{
var _permission = scope.ServiceProvider.GetRequiredService<PermissionServices>();
return await _permission.HasPermission(PermissionsMaster.ViewAllEmployees, loggedInEmployee.Id);
});
var viewTeamTask = Task.Run(async () =>
{
var _permission = scope.ServiceProvider.GetRequiredService<PermissionServices>();
return await _permission.HasPermission(PermissionsMaster.ViewTeamMembers, loggedInEmployee.Id);
});
await Task.WhenAll(viewAllTask, viewTeamTask);
var hasViewAllEmployeesPermission = viewAllTask.Result;
var hasViewTeamMembersPermission = viewTeamTask.Result;
List<Employee> employees = new List<Employee>();
// Step 4: Query based on permission
if (hasViewAllEmployeesPermission && !projectId.HasValue)
{
// OrganizationId needs to be retrieved from loggedInEmployee or context based on your app's structure
var employeeQuery = _context.Employees
.AsNoTracking() // Optimize EF query for read-only operation[web:1][web:13][web:18]
.Include(e => e.JobRole) .Include(e => e.JobRole)
.Where(e => e.OrganizationId == organizationId); .Include(e => e.Organization)
.Where(e => e.OrganizationId == loggedInEmployee.OrganizationId && e.IsActive != showInactive)
employeeQuery = showInactive
? employeeQuery.Where(e => !e.IsActive)
: employeeQuery.Where(e => e.IsActive);
employees = await employeeQuery.ToListAsync();
_logger.LogInfo("Employee list fetched with full access. Count: {Count}", employees.Count);
}
else if (hasViewTeamMembersPermission && !showInactive && !projectId.HasValue)
{
// Only active team members with limited permission
var projectIds = await _projectServices.GetMyProjectIdsAsync(tenantId, loggedInEmployee);
employees = await _context.ProjectAllocations
.AsNoTracking()
.Include(pa => pa.Employee)
.ThenInclude(e => e!.JobRole)
.Where(pa =>
projectIds.Contains(pa.ProjectId)
&& pa.IsActive
&& pa.Employee != null
&& pa.Employee.IsActive
&& pa.TenantId == tenantId)
.Select(pa => pa.Employee!)
.Distinct()
.ToListAsync(); .ToListAsync();
_logger.LogInfo("Employee list fetched with limited access (active only). Count: {Count}", employees.Count);
}
// If a specific projectId is provided, override employee fetching to ensure strict project context
if (projectId.HasValue)
{
employees = await _context.ProjectAllocations
.AsNoTracking()
.Include(pa => pa.Employee)
.ThenInclude(e => e!.JobRole)
.Where(pa =>
pa.ProjectId == projectId
&& pa.IsActive
&& pa.Employee != null
&& pa.Employee.IsActive
&& pa.TenantId == tenantId)
.Select(pa => pa.Employee!)
.Distinct()
.ToListAsync();
_logger.LogInfo("Employee list fetched for specific project. ProjectId: {ProjectId}. Count: {Count}",
projectId, employees.Count);
}
// Step 5: Map to view model // Step 5: Map to view model
result = employees.Select(e => _mapper.Map<EmployeeVM>(e)).Distinct().ToList(); result = employees.Select(e => _mapper.Map<EmployeeVM>(e)).Distinct().ToList();
@ -329,7 +257,7 @@ namespace MarcoBMS.Services.Controllers
[HttpGet("basic")] [HttpGet("basic")]
public async Task<IActionResult> GetEmployeesByProjectBasic(Guid? projectId, [FromQuery] string? searchString) public async Task<IActionResult> GetEmployeesByProjectBasic(Guid? projectId, [FromQuery] string? searchString, [FromQuery] bool sendAll = false)
{ {
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
var employeeQuery = _context.Employees.Where(e => e.IsActive); var employeeQuery = _context.Employees.Where(e => e.IsActive);
@ -353,8 +281,11 @@ namespace MarcoBMS.Services.Controllers
var searchStringLower = searchString.ToLower(); var searchStringLower = searchString.ToLower();
employeeQuery = employeeQuery.Where(e => (e.FirstName + " " + e.LastName).ToLower().Contains(searchStringLower)); employeeQuery = employeeQuery.Where(e => (e.FirstName + " " + e.LastName).ToLower().Contains(searchStringLower));
} }
if (!sendAll)
var response = await employeeQuery.Take(10).Select(e => _mapper.Map<BasicEmployeeVM>(e)).ToListAsync(); {
employeeQuery = employeeQuery.Take(10);
}
var response = await employeeQuery.Select(e => _mapper.Map<BasicEmployeeVM>(e)).ToListAsync();
return Ok(ApiResponse<object>.SuccessResponse(response, $"{response.Count} records of employees fetched successfully", 200)); return Ok(ApiResponse<object>.SuccessResponse(response, $"{response.Count} records of employees fetched successfully", 200));
} }
@ -470,6 +401,9 @@ namespace MarcoBMS.Services.Controllers
Guid tenantId = _userHelper.GetTenantId(); Guid tenantId = _userHelper.GetTenantId();
var LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); var LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
Guid employeeId = Guid.Empty; Guid employeeId = Guid.Empty;
if (model == null) if (model == null)
return BadRequest(ApiResponse<object>.ErrorResponse("Invalid data", "Invaild Data", 400)); return BadRequest(ApiResponse<object>.ErrorResponse("Invalid data", "Invaild Data", 400));
@ -602,6 +536,7 @@ namespace MarcoBMS.Services.Controllers
{ {
// Correlation and context capture for logs // Correlation and context capture for logs
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
Guid organizationId = model.OrganizationId ?? loggedInEmployee.OrganizationId;
{ {
if (model == null) if (model == null)
@ -625,10 +560,10 @@ namespace MarcoBMS.Services.Controllers
if (model.Id.HasValue && model.Id.Value != Guid.Empty) if (model.Id.HasValue && model.Id.Value != Guid.Empty)
{ {
existingEmployee = await _context.Employees existingEmployee = await _context.Employees
.FirstOrDefaultAsync(e => e.Id == model.Id && e.OrganizationId == model.OrganizationId); .FirstOrDefaultAsync(e => e.Id == model.Id && e.OrganizationId == organizationId);
if (existingEmployee == null) 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}, Org={OrgId}", model.Id, organizationId);
return NotFound(ApiResponse<object>.ErrorResponse("Employee not found", "Employee not found in database", 404)); return NotFound(ApiResponse<object>.ErrorResponse("Employee not found", "Employee not found in database", 404));
} }
} }
@ -680,10 +615,10 @@ namespace MarcoBMS.Services.Controllers
if (!string.IsNullOrWhiteSpace(model.Email)) if (!string.IsNullOrWhiteSpace(model.Email))
{ {
var emailExists = await _context.Employees var emailExists = await _context.Employees
.AnyAsync(e => e.Email == model.Email && e.OrganizationId == model.OrganizationId); .AnyAsync(e => e.Email == model.Email);
if (emailExists) 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( return StatusCode(403, ApiResponse<object>.ErrorResponse(
"Employee with email already exists", "Employee with email already exists",
"Employee with this email already exists", 403)); "Employee with this email already exists", 403));
@ -723,7 +658,7 @@ namespace MarcoBMS.Services.Controllers
existingEmployee.ApplicationUserId = createdIdentityUser.Id; existingEmployee.ApplicationUserId = createdIdentityUser.Id;
await SendResetIfApplicableAsync(createdIdentityUser, existingEmployee.FirstName ?? "User"); await SendResetIfApplicableAsync(createdIdentityUser, existingEmployee.FirstName ?? "User");
} }
existingEmployee.OrganizationId = organizationId;
await _context.SaveChangesAsync(); await _context.SaveChangesAsync();
employeeId = existingEmployee.Id; employeeId = existingEmployee.Id;
@ -745,7 +680,7 @@ namespace MarcoBMS.Services.Controllers
newEmployee.ApplicationUserId = createdIdentityUser.Id; newEmployee.ApplicationUserId = createdIdentityUser.Id;
await SendResetIfApplicableAsync(createdIdentityUser, newEmployee.FirstName ?? "User"); await SendResetIfApplicableAsync(createdIdentityUser, newEmployee.FirstName ?? "User");
} }
newEmployee.OrganizationId = organizationId;
await _context.Employees.AddAsync(newEmployee); await _context.Employees.AddAsync(newEmployee);
await _context.SaveChangesAsync(); await _context.SaveChangesAsync();
@ -877,6 +812,7 @@ namespace MarcoBMS.Services.Controllers
public async Task<IActionResult> CreateUserMobileAsync([FromBody] MobileUserManageDto model) public async Task<IActionResult> CreateUserMobileAsync([FromBody] MobileUserManageDto model)
{ {
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
Guid organizationId = model.OrganizationId ?? loggedInEmployee.OrganizationId;
if (tenantId == Guid.Empty) if (tenantId == Guid.Empty)
{ {
_logger.LogWarning("Tenant resolution failed in CreateUserMobile"); // structured warning _logger.LogWarning("Tenant resolution failed in CreateUserMobile"); // structured warning
@ -912,11 +848,11 @@ namespace MarcoBMS.Services.Controllers
if (model.Id == null || model.Id == Guid.Empty) if (model.Id == null || model.Id == Guid.Empty)
{ {
var emailExists = await _context.Employees var emailExists = await _context.Employees
.AnyAsync(e => e.Email == model.Email && e.OrganizationId == model.OrganizationId); .AnyAsync(e => e.Email == model.Email);
if (emailExists) if (emailExists)
{ {
_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 ?? string.Empty, organizationId);
return StatusCode(409, ApiResponse<object>.ErrorResponse("Employee with email already exists", "Employee with this email already exists", 409)); return StatusCode(409, ApiResponse<object>.ErrorResponse("Employee with email already exists", "Employee with this email already exists", 409));
} }
@ -933,7 +869,7 @@ namespace MarcoBMS.Services.Controllers
JoiningDate = model.JoiningDate, JoiningDate = model.JoiningDate,
JobRoleId = model.JobRoleId, JobRoleId = model.JobRoleId,
Photo = imageBytes, Photo = imageBytes,
OrganizationId = model.OrganizationId, OrganizationId = organizationId,
HasApplicationAccess = model.HasApplicationAccess, HasApplicationAccess = model.HasApplicationAccess,
}; };
@ -1001,7 +937,7 @@ namespace MarcoBMS.Services.Controllers
existingEmployee.PhoneNumber = model.PhoneNumber; existingEmployee.PhoneNumber = model.PhoneNumber;
existingEmployee.JoiningDate = model.JoiningDate; existingEmployee.JoiningDate = model.JoiningDate;
existingEmployee.JobRoleId = model.JobRoleId; existingEmployee.JobRoleId = model.JobRoleId;
existingEmployee.OrganizationId = model.OrganizationId; existingEmployee.OrganizationId = organizationId;
existingEmployee.HasApplicationAccess = model.HasApplicationAccess; existingEmployee.HasApplicationAccess = model.HasApplicationAccess;
if (string.IsNullOrWhiteSpace(existingEmployee.Email) && !string.IsNullOrWhiteSpace(model.Email)) if (string.IsNullOrWhiteSpace(existingEmployee.Email) && !string.IsNullOrWhiteSpace(model.Email))
@ -1011,7 +947,7 @@ namespace MarcoBMS.Services.Controllers
if (emailExists) if (emailExists)
{ {
_logger.LogInfo("Employee email already exists in org. Email={Email}, Org={OrgId}", model.Email, model.OrganizationId); _logger.LogInfo("Employee email already exists in org. Email={Email}, Org={OrgId}", model.Email, organizationId);
return StatusCode(409, ApiResponse<object>.ErrorResponse("Employee with email already exists", "Employee with this email already exists", 409)); return StatusCode(409, ApiResponse<object>.ErrorResponse("Employee with email already exists", "Employee with this email already exists", 409));
} }
existingEmployee.Email = model.Email; existingEmployee.Email = model.Email;
@ -1075,7 +1011,7 @@ namespace MarcoBMS.Services.Controllers
Guid tenantId = _userHelper.GetTenantId(); Guid tenantId = _userHelper.GetTenantId();
var LoggedEmployee = await _userHelper.GetCurrentEmployeeAsync(); var LoggedEmployee = await _userHelper.GetCurrentEmployeeAsync();
Employee? employee = await _context.Employees.FirstOrDefaultAsync(e => e.Id == id && e.TenantId == tenantId); Employee? employee = await _context.Employees.FirstOrDefaultAsync(e => e.Id == id && e.OrganizationId == organizationId);
if (employee == null) if (employee == null)
{ {
_logger.LogWarning("Employee with ID {EmploueeId} not found in database", id); _logger.LogWarning("Employee with ID {EmploueeId} not found in database", id);
@ -1248,11 +1184,14 @@ namespace MarcoBMS.Services.Controllers
// Prepare reset link sender helper // Prepare reset link sender helper
private async Task SendResetIfApplicableAsync(ApplicationUser u, string firstName) private async Task SendResetIfApplicableAsync(ApplicationUser u, string firstName)
{
if (!string.IsNullOrWhiteSpace(u.Email))
{ {
var token = await _userManager.GeneratePasswordResetTokenAsync(u); var token = await _userManager.GeneratePasswordResetTokenAsync(u);
var resetLink = $"{_configuration["AppSettings:WebFrontendUrl"]}/reset-password?token={WebUtility.UrlEncode(token)}"; var resetLink = $"{_configuration["AppSettings:WebFrontendUrl"]}/reset-password?token={WebUtility.UrlEncode(token)}";
await _emailSender.SendResetPasswordEmailOnRegister(u.Email ?? "", firstName, resetLink); await _emailSender.SendResetPasswordEmailOnRegister(u.Email, firstName, resetLink);
_logger.LogInfo("Reset password email queued. Email={Email}", u.Email ?? ""); _logger.LogInfo("Reset password email queued. Email={Email}", u.Email);
}
} }
} }
} }

View File

@ -482,6 +482,13 @@ namespace MarcoBMS.Services.Controllers
taskAllocationQuery = taskAllocationQuery.Where(t => t.WorkItem != null && taskAllocationQuery = taskAllocationQuery.Where(t => t.WorkItem != null &&
taskFilter.ActivityIds.Contains(t.WorkItem.ActivityId)); 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) if (taskFilter.dateFrom.HasValue && taskFilter.dateTo.HasValue)
{ {
taskAllocationQuery = taskAllocationQuery.Where(t => t.AssignmentDate.Date >= taskFilter.dateFrom.Value.Date && 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)); 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> /// <summary>
/// Approves a reported task after validation, updates status, and stores attachments/comments. /// Approves a reported task after validation, updates status, and stores attachments/comments.
/// </summary> /// </summary>

View File

@ -273,12 +273,7 @@ namespace Marco.Pms.Services.Controllers
return StatusCode(403, return StatusCode(403,
ApiResponse<object>.ErrorResponse("Access denied", "User does not have the required permissions for this action.", 403)); ApiResponse<object>.ErrorResponse("Access denied", "User does not have the required permissions for this action.", 403));
} }
if (!hasManagePermission && (hasModifyPermission || hasViewPermission) && id != loggedInEmployee.TenantId)
{
_logger.LogWarning("Permission denied: User {EmployeeId} attempted to access tenant details of other tenant.", loggedInEmployee.Id);
return StatusCode(403,
ApiResponse<object>.ErrorResponse("Access denied", "User does not have the required permissions for this action.", 403));
}
// Create a single DbContext for main tenant fetch and related data requests // Create a single DbContext for main tenant fetch and related data requests
await using var _context = await _dbContextFactory.CreateDbContextAsync(); await using var _context = await _dbContextFactory.CreateDbContextAsync();
@ -297,6 +292,13 @@ namespace Marco.Pms.Services.Controllers
} }
_logger.LogInfo("Tenant {TenantId} found.", tenant.Id); _logger.LogInfo("Tenant {TenantId} found.", tenant.Id);
if (!hasManagePermission && (hasModifyPermission || hasViewPermission) && tenant.OrganizationId != loggedInEmployee.OrganizationId)
{
_logger.LogWarning("Permission denied: User {EmployeeId} attempted to access tenant details of other tenant.", loggedInEmployee.Id);
return StatusCode(403,
ApiResponse<object>.ErrorResponse("Access denied", "User does not have the required permissions for this action.", 403));
}
// Fetch dependent data in parallel to improve performance // Fetch dependent data in parallel to improve performance
var employeesTask = Task.Run(async () => var employeesTask = Task.Run(async () =>
{ {
@ -550,7 +552,7 @@ namespace Marco.Pms.Services.Controllers
JobRole = adminJobRole, // Link to the newly created role JobRole = adminJobRole, // Link to the newly created role
CurrentAddress = model.BillingAddress, CurrentAddress = model.BillingAddress,
IsActive = true, IsActive = true,
IsSystem = false, IsSystem = true,
IsPrimary = true, IsPrimary = true,
OrganizationId = organization.Id, OrganizationId = organization.Id,
HasApplicationAccess = true HasApplicationAccess = true
@ -566,43 +568,36 @@ namespace Marco.Pms.Services.Controllers
}; };
_context.ApplicationRoles.Add(applicationRole); _context.ApplicationRoles.Add(applicationRole);
var rolePermissionMappigs = new List<RolePermissionMappings> { var featureIds = new List<Guid>
new RolePermissionMappings {
new Guid("a4e25142-449b-4334-a6e5-22f70e4732d7"), // Expense Management feature
new Guid("81ab8a87-8ccd-4015-a917-0627cee6a100"), // Employee Management feature
new Guid("52c9cf54-1eb2-44d2-81bb-524cf29c0a94"), // Attendance Management feature
new Guid("a8cf4331-8f04-4961-8360-a3f7c3cc7462"), // Document Management feature
new Guid("be3b3afc-6ccf-4566-b9b6-aafcb65546be"), // Masters Management feature
new Guid("39e66f81-efc6-446c-95bd-46bff6cfb606"), // Directory Management feature
new Guid("6d4c82d6-dbce-48ab-b8b8-f785f4d8c914") // Organization Management feature
};
var permissionIds = await _context.FeaturePermissions.Where(fp => featureIds.Contains(fp.FeatureId)).Select(fp => fp.Id).ToListAsync();
var rolePermissionMappigs = permissionIds.Select(p => new RolePermissionMappings
{
ApplicationRoleId = applicationRole.Id,
FeaturePermissionId = p
}).ToList();
rolePermissionMappigs.Add(new RolePermissionMappings
{ {
ApplicationRoleId = applicationRole.Id, ApplicationRoleId = applicationRole.Id,
FeaturePermissionId = PermissionsMaster.ModifyTenant FeaturePermissionId = PermissionsMaster.ModifyTenant
}, });
new RolePermissionMappings rolePermissionMappigs.Add(new RolePermissionMappings
{ {
ApplicationRoleId = applicationRole.Id, ApplicationRoleId = applicationRole.Id,
FeaturePermissionId = PermissionsMaster.ViewTenant FeaturePermissionId = PermissionsMaster.ViewTenant
}, });
new RolePermissionMappings
{
ApplicationRoleId = applicationRole.Id,
FeaturePermissionId = PermissionsMaster.ManageMasters
},
new RolePermissionMappings
{
ApplicationRoleId = applicationRole.Id,
FeaturePermissionId = PermissionsMaster.ViewMasters
},
new RolePermissionMappings
{
ApplicationRoleId = applicationRole.Id,
FeaturePermissionId = PermissionsMaster.ViewOrganization
},
new RolePermissionMappings
{
ApplicationRoleId = applicationRole.Id,
FeaturePermissionId = PermissionsMaster.AddOrganization
},
new RolePermissionMappings
{
ApplicationRoleId = applicationRole.Id,
FeaturePermissionId = PermissionsMaster.EditOrganization
}
};
_context.RolePermissionMappings.AddRange(rolePermissionMappigs); _context.RolePermissionMappings.AddRange(rolePermissionMappigs);
_context.EmployeeRoleMappings.Add(new EmployeeRoleMapping _context.EmployeeRoleMappings.Add(new EmployeeRoleMapping
@ -651,6 +646,22 @@ namespace Marco.Pms.Services.Controllers
_context.OrgServiceMappings.AddRange(serviceOrgMappings); _context.OrgServiceMappings.AddRange(serviceOrgMappings);
} }
var _masteData = scope.ServiceProvider.GetRequiredService<MasterDataService>();
var expensesTypeMaster = _masteData.GetExpensesTypeesData(tenant.Id);
var paymentModeMatser = _masteData.GetPaymentModesData(tenant.Id);
var documentCategoryMaster = _masteData.GetDocumentCategoryData(tenant.Id);
var employeeDocumentId = documentCategoryMaster.Where(dc => dc.Name == "Employee Documents").Select(dc => dc.Id).FirstOrDefault();
var projectDocumentId = documentCategoryMaster.Where(dc => dc.Name == "Project Documents").Select(dc => dc.Id).FirstOrDefault();
var documentTypeMaster = _masteData.GetDocumentTypeData(tenant.Id, employeeDocumentId, projectDocumentId);
_context.ExpensesTypeMaster.AddRange(expensesTypeMaster);
_context.PaymentModeMatser.AddRange(paymentModeMatser);
_context.DocumentCategoryMasters.AddRange(documentCategoryMaster);
_context.DocumentTypeMasters.AddRange(documentTypeMaster);
// All entities are now added to the context. Save them all in a single database operation. // All entities are now added to the context. Save them all in a single database operation.
await _context.SaveChangesAsync(); await _context.SaveChangesAsync();

View File

@ -253,16 +253,29 @@ namespace Marco.Pms.Services.Helpers
!ts.IsCancelled && !ts.IsCancelled &&
ts.EndDate.Date >= DateTime.UtcNow.Date); // FIX: Subscription should not be expired ts.EndDate.Date >= DateTime.UtcNow.Date); // FIX: Subscription should not be expired
var featureIds = new List<Guid>
{
new Guid("a4e25142-449b-4334-a6e5-22f70e4732d7"), // Expense Management feature
new Guid("81ab8a87-8ccd-4015-a917-0627cee6a100"), // Employee Management feature
new Guid("52c9cf54-1eb2-44d2-81bb-524cf29c0a94"), // Attendance Management feature
new Guid("a8cf4331-8f04-4961-8360-a3f7c3cc7462"), // Document Management feature
new Guid("be3b3afc-6ccf-4566-b9b6-aafcb65546be"), // Masters Management feature
new Guid("39e66f81-efc6-446c-95bd-46bff6cfb606"), // Directory Management feature
new Guid("6d4c82d6-dbce-48ab-b8b8-f785f4d8c914") // Organization Management feature
//new Guid("2f3509b7-160d-410a-b9b6-daadd96c986d") // Tenant Management feature
};
if (tenantSubscription == null) if (tenantSubscription == null)
{ {
_logger.LogWarning("No active subscription found for tenant: {TenantId}", tenantId); _logger.LogWarning("No active subscription found for tenant: {TenantId}", tenantId);
return new List<Guid>(); return featureIds;
} }
_logger.LogDebug("Active subscription found for tenant: {TenantId}, PlanId: {PlanId}", _logger.LogDebug("Active subscription found for tenant: {TenantId}, PlanId: {PlanId}",
tenantId, tenantSubscription.Plan!.Id); tenantId, tenantSubscription.Plan!.Id);
var featureIds = new List<Guid> { new Guid("2f3509b7-160d-410a-b9b6-daadd96c986d"), new Guid("be3b3afc-6ccf-4566-b9b6-aafcb65546be") }; //var featureIds = new List<Guid> { new Guid("be3b3afc-6ccf-4566-b9b6-aafcb65546be") };
// Step 2: Get feature details from Plan // Step 2: Get feature details from Plan
var featureDetails = await _featureDetailsHelper.GetFeatureDetails(tenantSubscription.Plan!.FeaturesId); var featureDetails = await _featureDetailsHelper.GetFeatureDetails(tenantSubscription.Plan!.FeaturesId);

View File

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

View File

@ -248,6 +248,7 @@ namespace Marco.Pms.Services.MappingProfiles
dest => dest.Id, dest => dest.Id,
opt => opt.MapFrom(src => Guid.Parse(src.Id))); opt => opt.MapFrom(src => Guid.Parse(src.Id)));
CreateMap<Expenses, ExpenseDetailsVM>();
CreateMap<ExpenseDetailsMongoDB, ExpenseDetailsVM>() CreateMap<ExpenseDetailsMongoDB, ExpenseDetailsVM>()
.ForMember( .ForMember(
dest => dest.Id, dest => dest.Id,

View File

@ -1533,9 +1533,11 @@ namespace Marco.Pms.Services.Service
return ApiResponse<object>.ErrorResponse("Contact ID is empty", "Contact ID is empty", 400); return ApiResponse<object>.ErrorResponse("Contact ID is empty", "Contact ID is empty", 400);
} }
var (hasAdminPermission, hasManagerPermission, hasUserPermission) = await CheckPermissionsAsync(loggedInEmployee.Id);
var bucketIds = await _context.ContactBucketMappings.Where(cb => cb.ContactId == id).Select(cb => cb.BucketId).ToListAsync(); var bucketIds = await _context.ContactBucketMappings.Where(cb => cb.ContactId == id).Select(cb => cb.BucketId).ToListAsync();
var hasContactAccess = await _context.EmployeeBucketMappings.AnyAsync(eb => bucketIds.Contains(eb.BucketId) && eb.EmployeeId == loggedInEmployee.Id); var hasContactAccess = await _context.EmployeeBucketMappings.AnyAsync(eb => bucketIds.Contains(eb.BucketId) && eb.EmployeeId == loggedInEmployee.Id);
if (hasContactAccess) if (!hasAdminPermission && !hasContactAccess)
{ {
_logger.LogWarning("Employee {EmployeeId} does not have permission to delete contact {ContactId}", _logger.LogWarning("Employee {EmployeeId} does not have permission to delete contact {ContactId}",
loggedInEmployee.Id, id); loggedInEmployee.Id, id);
@ -2131,7 +2133,7 @@ namespace Marco.Pms.Services.Service
var bucketIds = await _context.ContactBucketMappings.Where(cb => cb.ContactId == noteDto.ContactId).Select(cb => cb.BucketId).ToListAsync(); var bucketIds = await _context.ContactBucketMappings.Where(cb => cb.ContactId == noteDto.ContactId).Select(cb => cb.BucketId).ToListAsync();
var hasContactAccess = await _context.EmployeeBucketMappings.AnyAsync(eb => bucketIds.Contains(eb.BucketId) && eb.EmployeeId == loggedInEmployee.Id); var hasContactAccess = await _context.EmployeeBucketMappings.AnyAsync(eb => bucketIds.Contains(eb.BucketId) && eb.EmployeeId == loggedInEmployee.Id);
if (!hasAdminPermission && hasContactAccess) if (!hasAdminPermission && !hasContactAccess)
{ {
_logger.LogWarning("Employee {EmployeeId} does not have permission to delete contact {ContactId}", _logger.LogWarning("Employee {EmployeeId} does not have permission to delete contact {ContactId}",
loggedInEmployee.Id, noteDto.ContactId); loggedInEmployee.Id, noteDto.ContactId);
@ -2270,10 +2272,11 @@ namespace Marco.Pms.Services.Service
return ApiResponse<object>.ErrorResponse("Note not found", "Note not found", 404); return ApiResponse<object>.ErrorResponse("Note not found", "Note not found", 404);
} }
var (hasAdminPermission, hasManagerPermission, hasUserPermission) = await CheckPermissionsAsync(loggedInEmployee.Id);
var bucketIds = await _context.ContactBucketMappings.Where(cb => cb.ContactId == note.ContactId).Select(cb => cb.BucketId).ToListAsync(); 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); var hasContactAccess = await _context.EmployeeBucketMappings.AnyAsync(eb => bucketIds.Contains(eb.BucketId) && eb.EmployeeId == loggedInEmployee.Id);
if (hasContactAccess) if (!hasAdminPermission && !hasContactAccess)
{ {
_logger.LogWarning("Employee {EmployeeId} does not have permission to delete contact {ContactId}", _logger.LogWarning("Employee {EmployeeId} does not have permission to delete contact {ContactId}",
loggedInEmployee.Id, note.ContactId); loggedInEmployee.Id, note.ContactId);

View File

@ -23,6 +23,7 @@ using MarcoBMS.Services.Service;
using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using System.Text.Json; using System.Text.Json;
using System.Text.RegularExpressions;
using Document = Marco.Pms.Model.DocumentManager.Document; using Document = Marco.Pms.Model.DocumentManager.Document;
namespace Marco.Pms.Services.Service namespace Marco.Pms.Services.Service
@ -129,6 +130,16 @@ namespace Marco.Pms.Services.Service
// 3. --- Build Base Query and Apply Permissions --- // 3. --- Build Base Query and Apply Permissions ---
// Start with a base IQueryable. Filters will be chained onto this. // Start with a base IQueryable. Filters will be chained onto this.
var expensesQuery = _context.Expenses var expensesQuery = _context.Expenses
.Include(e => e.PaidBy)
.Include(e => e.CreatedBy)
.Include(e => e.ProcessedBy)
.Include(e => e.ApprovedBy)
.Include(e => e.ReviewedBy)
.Include(e => e.PaymentMode)
.Include(e => e.Project)
.Include(e => e.PaymentMode)
.Include(e => e.ExpensesType)
.Include(e => e.Status)
.Where(e => e.TenantId == tenantId); // Always filter by TenantId first. .Where(e => e.TenantId == tenantId); // Always filter by TenantId first.
if (cacheList == null) if (cacheList == null)
@ -176,6 +187,10 @@ namespace Marco.Pms.Services.Service
{ {
expensesQuery = expensesQuery.Where(e => expenseFilter.PaidById.Contains(e.PaidById)); expensesQuery = expensesQuery.Where(e => expenseFilter.PaidById.Contains(e.PaidById));
} }
if (expenseFilter.ExpenseTypeIds?.Any() == true)
{
expensesQuery = expensesQuery.Where(e => expenseFilter.ExpenseTypeIds.Contains(e.ExpensesTypeId));
}
// Only allow filtering by 'CreatedBy' if the user has 'View All' permission. // Only allow filtering by 'CreatedBy' if the user has 'View All' permission.
if (expenseFilter.CreatedByIds?.Any() == true && hasViewAllPermissionTask.Result) if (expenseFilter.CreatedByIds?.Any() == true && hasViewAllPermissionTask.Result)
@ -213,7 +228,8 @@ namespace Marco.Pms.Services.Service
return ApiResponse<object>.SuccessResponse(new List<ExpenseList>(), "No expenses found for the given criteria.", 200); return ApiResponse<object>.SuccessResponse(new List<ExpenseList>(), "No expenses found for the given criteria.", 200);
} }
expenseVM = await GetAllExpnesRelatedTables(expensesList, tenantId); //expenseVM = await GetAllExpnesRelatedTables(expensesList, tenantId);
expenseVM = _mapper.Map<List<ExpenseList>>(expensesList);
totalPages = (int)Math.Ceiling((double)totalEntites / pageSize); totalPages = (int)Math.Ceiling((double)totalEntites / pageSize);
} }
@ -276,7 +292,18 @@ namespace Marco.Pms.Services.Service
ExpenseDetailsMongoDB? expenseDetails = null; ExpenseDetailsMongoDB? expenseDetails = null;
if (expenseDetails == null) if (expenseDetails == null)
{ {
var expense = await _context.Expenses.AsNoTracking().FirstOrDefaultAsync(e => e.Id == id && e.TenantId == tenantId); var expense = await _context.Expenses
.Include(e => e.PaidBy)
.Include(e => e.CreatedBy)
.Include(e => e.ProcessedBy)
.Include(e => e.ApprovedBy)
.Include(e => e.ReviewedBy)
.Include(e => e.PaymentMode)
.Include(e => e.Project)
.Include(e => e.PaymentMode)
.Include(e => e.ExpensesType)
.Include(e => e.Status)
.AsNoTracking().FirstOrDefaultAsync(e => e.Id == id && e.TenantId == tenantId);
if (expense == null) if (expense == null)
{ {
@ -318,6 +345,13 @@ namespace Marco.Pms.Services.Service
{ {
status.PermissionIds = permissionStatusMappings.Where(ps => ps.StatusId == status.Id).Select(ps => ps.PermissionIds).FirstOrDefault(); 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); vm.ExpensesReimburse = _mapper.Map<ExpensesReimburseVM>(expensesReimburse);
@ -361,66 +395,25 @@ namespace Marco.Pms.Services.Service
{ {
try try
{ {
// Task 1: Get all distinct projects associated with the tenant's expenses. var expenses = await _context.Expenses
var projectsTask = Task.Run(async () => .Include(e => e.PaidBy)
{ .Include(e => e.Project)
await using var dbContext = await _dbContextFactory.CreateDbContextAsync(); .Include(e => e.CreatedBy)
return await dbContext.Expenses .Include(e => e.Status)
.Where(e => e.TenantId == tenantId && e.Project != null) .Include(e => e.ExpensesType)
.Select(e => e.Project!) .Where(e => e.TenantId == tenantId)
.Distinct()
.Select(p => new { p.Id, Name = $"{p.Name}" })
.ToListAsync(); .ToListAsync();
});
// Task 2: Get all distinct users who paid for the tenant's expenses.
var paidByTask = Task.Run(async () =>
{
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
return await dbContext.Expenses
.Where(e => e.TenantId == tenantId && e.PaidBy != null)
.Select(e => e.PaidBy!)
.Distinct()
.Select(u => new { u.Id, Name = $"{u.FirstName} {u.LastName}" })
.ToListAsync();
});
// Task 3: Get all distinct users who created the tenant's expenses.
var createdByTask = Task.Run(async () =>
{
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
return await dbContext.Expenses
.Where(e => e.TenantId == tenantId && e.CreatedBy != null)
.Select(e => e.CreatedBy!)
.Distinct()
.Select(u => new { u.Id, Name = $"{u.FirstName} {u.LastName}" })
.ToListAsync();
});
// Task 4: Get all distinct statuses associated with the tenant's expenses.
var statusTask = Task.Run(async () =>
{
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
return await dbContext.Expenses
.Where(e => e.TenantId == tenantId && e.Status != null)
.Select(e => e.Status!)
.Distinct()
.Select(s => new { s.Id, s.Name })
.ToListAsync();
});
// Execute all four queries concurrently. The total wait time will be determined
// by the longest-running query, not the sum of all four.
await Task.WhenAll(projectsTask, paidByTask, createdByTask, statusTask);
// Construct the final object from the results of the completed tasks. // Construct the final object from the results of the completed tasks.
return ApiResponse<object>.SuccessResponse(new var response = new
{ {
Projects = await projectsTask, Projects = expenses.Where(e => e.Project != null).Select(e => new { Id = e.Project!.Id, Name = e.Project.Name }).Distinct().ToList(),
PaidBy = await paidByTask, PaidBy = expenses.Where(e => e.PaidBy != null).Select(e => new { Id = e.PaidBy!.Id, Name = $"{e.PaidBy.FirstName} {e.PaidBy.LastName}" }).Distinct().ToList(),
CreatedBy = await createdByTask, CreatedBy = expenses.Where(e => e.CreatedBy != null).Select(e => new { Id = e.CreatedBy!.Id, Name = $"{e.CreatedBy.FirstName} {e.CreatedBy.LastName}" }).Distinct().ToList(),
Status = await statusTask Status = expenses.Where(e => e.Status != null).Select(e => new { Id = e.Status!.Id, Name = e.Status.Name }).Distinct().ToList(),
}, "Successfully fetched the filter list", 200); ExpensesType = expenses.Where(e => e.ExpensesType != null).Select(e => new { Id = e.ExpensesType!.Id, Name = e.ExpensesType.Name }).Distinct().ToList()
};
return ApiResponse<object>.SuccessResponse(response, "Successfully fetched the filter list", 200);
} }
catch (Exception ex) catch (Exception ex)
{ {
@ -458,13 +451,6 @@ namespace Marco.Pms.Services.Service
return await permissionService.HasPermission(PermissionsMaster.ExpenseUpload, loggedInEmployee.Id); return await permissionService.HasPermission(PermissionsMaster.ExpenseUpload, loggedInEmployee.Id);
}); });
var hasProjectPermissionTask = Task.Run(async () =>
{
using var scope = _serviceScopeFactory.CreateScope();
var permissionService = scope.ServiceProvider.GetRequiredService<PermissionServices>();
return await permissionService.HasProjectPermission(loggedInEmployee, dto.ProjectId);
});
// VALIDATION CHECKS: Use IDbContextFactory for thread-safe, parallel database queries. // VALIDATION CHECKS: Use IDbContextFactory for thread-safe, parallel database queries.
// Each task gets its own DbContext instance. // Each task gets its own DbContext instance.
var projectTask = Task.Run(async () => var projectTask = Task.Run(async () =>
@ -487,6 +473,15 @@ namespace Marco.Pms.Services.Service
await using var dbContext = await _dbContextFactory.CreateDbContextAsync(); await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
return await dbContext.PaymentModeMatser.AsNoTracking().FirstOrDefaultAsync(pm => pm.Id == dto.PaymentModeId); 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 () => var statusMappingTask = Task.Run(async () =>
{ {
await using var dbContext = await _dbContextFactory.CreateDbContextAsync(); await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
@ -506,13 +501,10 @@ namespace Marco.Pms.Services.Service
// Await all prerequisite checks at once. // Await all prerequisite checks at once.
await Task.WhenAll( await Task.WhenAll(hasUploadPermissionTask, projectTask, expenseTypeTask, paymentModeTask, statusMappingTask, paidByTask, expenseUIdTask);
hasUploadPermissionTask, hasProjectPermissionTask,
projectTask, expenseTypeTask, paymentModeTask, statusMappingTask, paidByTask
);
// 2. Aggregate and Check Results // 2. Aggregate and Check Results
if (!await hasUploadPermissionTask || !await hasProjectPermissionTask) if (!await hasUploadPermissionTask)
{ {
_logger.LogWarning("Access DENIED for employee {EmployeeId} on project {ProjectId}.", loggedInEmployee.Id, dto.ProjectId); _logger.LogWarning("Access DENIED for employee {EmployeeId} on project {ProjectId}.", loggedInEmployee.Id, dto.ProjectId);
return ApiResponse<object>.ErrorResponse("Access Denied.", "You do not have permission to upload expenses for this project.", 403); return ApiResponse<object>.ErrorResponse("Access Denied.", "You do not have permission to upload expenses for this project.", 403);
@ -524,6 +516,7 @@ namespace Marco.Pms.Services.Service
var paymentMode = await paymentModeTask; var paymentMode = await paymentModeTask;
var statusMapping = await statusMappingTask; var statusMapping = await statusMappingTask;
var paidBy = await paidByTask; var paidBy = await paidByTask;
var lastExpenseUId = expenseUIdTask.Result;
if (project == null) validationErrors.Add("Project not found."); if (project == null) validationErrors.Add("Project not found.");
if (paidBy == null) validationErrors.Add("Paid by employee not found"); if (paidBy == null) validationErrors.Add("Paid by employee not found");
@ -540,8 +533,11 @@ namespace Marco.Pms.Services.Service
return ApiResponse<object>.ErrorResponse("Invalid input data.", errorMessage, 400); return ApiResponse<object>.ErrorResponse("Invalid input data.", errorMessage, 400);
} }
var currentexpenseUId = (lastExpenseUId + 1).ToString("D5");
// 3. Entity Creation // 3. Entity Creation
var expense = _mapper.Map<Expenses>(dto); var expense = _mapper.Map<Expenses>(dto);
expense.ExpenseUId = $"EX-{currentexpenseUId}";
expense.CreatedById = loggedInEmployee.Id; expense.CreatedById = loggedInEmployee.Id;
expense.CreatedAt = DateTime.UtcNow; expense.CreatedAt = DateTime.UtcNow;
expense.TenantId = tenantId; expense.TenantId = tenantId;
@ -1084,6 +1080,14 @@ namespace Marco.Pms.Services.Service
#endregion #endregion
#region =================================================================== Helper Functions =================================================================== #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) private static object ExceptionMapper(Exception ex)
{ {
return new return new
@ -1217,46 +1221,6 @@ namespace Marco.Pms.Services.Service
private async Task<ExpenseDetailsMongoDB> GetAllExpnesRelatedTablesForSingle(Expenses model, Guid tenantId) private async Task<ExpenseDetailsMongoDB> GetAllExpnesRelatedTablesForSingle(Expenses model, Guid tenantId)
{ {
var projectTask = Task.Run(async () =>
{
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
return await dbContext.Projects.AsNoTracking().FirstOrDefaultAsync(p => p.Id == model.ProjectId && p.TenantId == tenantId);
});
var paidByTask = Task.Run(async () =>
{
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
return await dbContext.Employees.Include(e => e.JobRole).AsNoTracking().FirstOrDefaultAsync(e => e.Id == model.PaidById && e.TenantId == tenantId);
});
var createdByTask = Task.Run(async () =>
{
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
return await dbContext.Employees.Include(e => e.JobRole).AsNoTracking().FirstOrDefaultAsync(e => e.Id == model.CreatedById && e.TenantId == tenantId);
});
var reviewedByTask = Task.Run(async () =>
{
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
return await dbContext.Employees.Include(e => e.JobRole).AsNoTracking().FirstOrDefaultAsync(e => e.Id == model.ReviewedById && e.TenantId == tenantId);
});
var approvedByTask = Task.Run(async () =>
{
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
return await dbContext.Employees.Include(e => e.JobRole).AsNoTracking().FirstOrDefaultAsync(e => e.Id == model.ApprovedById && e.TenantId == tenantId);
});
var processedByTask = Task.Run(async () =>
{
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
return await dbContext.Employees.Include(e => e.JobRole).AsNoTracking().FirstOrDefaultAsync(e => e.Id == model.ProcessedById && e.TenantId == tenantId);
});
var expenseTypeTask = Task.Run(async () =>
{
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
return await dbContext.ExpensesTypeMaster.AsNoTracking().FirstOrDefaultAsync(et => et.Id == model.ExpensesTypeId && et.TenantId == tenantId);
});
var paymentModeTask = Task.Run(async () =>
{
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
return await dbContext.PaymentModeMatser.AsNoTracking().FirstOrDefaultAsync(pm => pm.Id == model.PaymentModeId && pm.TenantId == tenantId);
});
var statusMappingTask = Task.Run(async () => var statusMappingTask = Task.Run(async () =>
{ {
await using var dbContext = await _dbContextFactory.CreateDbContextAsync(); await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
@ -1304,29 +1268,20 @@ namespace Marco.Pms.Services.Service
}); });
// Await all prerequisite checks at once. // Await all prerequisite checks at once.
await Task.WhenAll(projectTask, expenseTypeTask, paymentModeTask, statusMappingTask, paidByTask, createdByTask, reviewedByTask, approvedByTask, await Task.WhenAll(statusTask, billAttachmentsTask);
processedByTask, statusTask, billAttachmentsTask);
var project = projectTask.Result;
var expenseType = expenseTypeTask.Result;
var paymentMode = paymentModeTask.Result;
var statusMapping = statusMappingTask.Result; var statusMapping = statusMappingTask.Result;
var paidBy = paidByTask.Result;
var createdBy = createdByTask.Result;
var reviewedBy = reviewedByTask.Result;
var approvedBy = approvedByTask.Result;
var processedBy = processedByTask.Result;
var billAttachment = billAttachmentsTask.Result; var billAttachment = billAttachmentsTask.Result;
var response = _mapper.Map<ExpenseDetailsMongoDB>(model); var response = _mapper.Map<ExpenseDetailsMongoDB>(model);
response.Project = _mapper.Map<ProjectBasicMongoDB>(project); response.Project = _mapper.Map<ProjectBasicMongoDB>(model.Project);
response.PaidBy = _mapper.Map<BasicEmployeeMongoDB>(paidBy); response.PaidBy = _mapper.Map<BasicEmployeeMongoDB>(model.PaidBy);
response.CreatedBy = _mapper.Map<BasicEmployeeMongoDB>(createdBy); response.CreatedBy = _mapper.Map<BasicEmployeeMongoDB>(model.CreatedBy);
response.ReviewedBy = _mapper.Map<BasicEmployeeMongoDB>(reviewedBy); response.ReviewedBy = _mapper.Map<BasicEmployeeMongoDB>(model.ReviewedBy);
response.ApprovedBy = _mapper.Map<BasicEmployeeMongoDB>(approvedBy); response.ApprovedBy = _mapper.Map<BasicEmployeeMongoDB>(model.ApprovedBy);
response.ProcessedBy = _mapper.Map<BasicEmployeeMongoDB>(processedBy); response.ProcessedBy = _mapper.Map<BasicEmployeeMongoDB>(model.ProcessedBy);
if (statusMapping != null) if (statusMapping != null)
{ {
response.Status = _mapper.Map<ExpensesStatusMasterMongoDB>(statusMapping.Status); response.Status = _mapper.Map<ExpensesStatusMasterMongoDB>(statusMapping.Status);
@ -1337,8 +1292,8 @@ namespace Marco.Pms.Services.Service
var status = statusTask.Result; var status = statusTask.Result;
response.Status = _mapper.Map<ExpensesStatusMasterMongoDB>(status); response.Status = _mapper.Map<ExpensesStatusMasterMongoDB>(status);
} }
response.PaymentMode = _mapper.Map<PaymentModeMatserMongoDB>(paymentMode); response.PaymentMode = _mapper.Map<PaymentModeMatserMongoDB>(model.PaymentMode);
response.ExpensesType = _mapper.Map<ExpensesTypeMasterMongoDB>(expenseType); response.ExpensesType = _mapper.Map<ExpensesTypeMasterMongoDB>(model.ExpensesType);
if (billAttachment != null) response.Documents = billAttachment.Documents; if (billAttachment != null) response.Documents = billAttachment.Documents;
return response; return response;

View File

@ -1,4 +1,5 @@
using Marco.Pms.Model.Forum; using Marco.Pms.Model.DocumentManager;
using Marco.Pms.Model.Forum;
using Marco.Pms.Model.Master; using Marco.Pms.Model.Master;
namespace Marco.Pms.Services.Service namespace Marco.Pms.Services.Service
@ -335,6 +336,194 @@ namespace Marco.Pms.Services.Service
} }
}; };
} }
public List<DocumentCategoryMaster> GetDocumentCategoryData(Guid tenantId)
{
return new List<DocumentCategoryMaster> {
new DocumentCategoryMaster
{
Id = Guid.NewGuid(),
Name = "Project Documents",
Description = "Project documents are formal records that outline the plans, progress, and details necessary to execute and manage a project effectively.",
EntityTypeId = Guid.Parse("c8fe7115-aa27-43bc-99f4-7b05fabe436e"),
CreatedAt = new DateTime(2025, 9, 15, 12, 42, 3, 202, DateTimeKind.Utc),
TenantId = tenantId
},
new DocumentCategoryMaster
{
Id = Guid.NewGuid(),
Name = "Employee Documents",
Description = "Employment details along with legal IDs like passports or drivers licenses to verify identity and work authorization.",
EntityTypeId = Guid.Parse("dbb9555a-7a0c-40f2-a9ed-f0463f1ceed7"),
CreatedAt = new DateTime(2025, 9, 15, 12, 42, 3, 202, DateTimeKind.Utc),
TenantId = tenantId
}
};
}
public List<DocumentTypeMaster> GetDocumentTypeData(Guid tenantId, Guid employeeDocumentId, Guid projectDocumentId)
{
return new List<DocumentTypeMaster> {
new DocumentTypeMaster
{
Id = Guid.NewGuid(),
Name = "Aadhaar card",
RegexExpression = "^[2-9][0-9]{11}$",
AllowedContentType = "application/pdf,image/jpeg",
MaxSizeAllowedInMB = 2,
IsValidationRequired = true,
IsMandatory = true,
CreatedAt = new DateTime(2025, 9, 3, 10, 46, 49, 955, DateTimeKind.Utc),
IsSystem = true,
IsActive = true,
DocumentCategoryId = employeeDocumentId,
TenantId = tenantId
},
new DocumentTypeMaster
{
Id = Guid.NewGuid(),
Name = "Pan Card",
RegexExpression = "^[A-Z]{5}[0-9]{4}[A-Z]{1}$",
AllowedContentType = "application/pdf,image/jpeg",
MaxSizeAllowedInMB = 2,
IsValidationRequired = true,
IsMandatory = true,
CreatedAt = new DateTime(2025, 9, 3, 10, 46, 49, 955, DateTimeKind.Utc),
IsSystem = true,
IsActive = true,
DocumentCategoryId = employeeDocumentId,
TenantId = tenantId
},
new DocumentTypeMaster
{
Id = Guid.NewGuid(),
Name = "Voter Card",
RegexExpression = "^[A-Z]{3}[0-9]{7}$",
AllowedContentType = "application/pdf,image/jpeg",
MaxSizeAllowedInMB = 2,
IsValidationRequired = true,
IsMandatory = true,
CreatedAt = new DateTime(2025, 9, 3, 10, 46, 49, 955, DateTimeKind.Utc),
IsSystem = true,
IsActive = true,
DocumentCategoryId = employeeDocumentId,
TenantId = tenantId
},
new DocumentTypeMaster
{
Id = Guid.NewGuid(),
Name = "Passport",
RegexExpression = "^[A-PR-WY][1-9]\\d\\s?\\d{4}[1-9]$",
AllowedContentType = "application/pdf,image/jpeg",
MaxSizeAllowedInMB = 2,
IsValidationRequired = true,
IsMandatory = true,
CreatedAt = new DateTime(2025, 9, 3, 10, 46, 49, 955, DateTimeKind.Utc),
IsSystem = true,
IsActive = true,
DocumentCategoryId = employeeDocumentId,
TenantId = tenantId
},
new DocumentTypeMaster
{
Id = Guid.NewGuid(),
Name = "Bank Passbook",
RegexExpression = "^\\d{9,18}$",
AllowedContentType = "application/pdf,image/jpeg",
MaxSizeAllowedInMB = 2,
IsValidationRequired = true,
IsMandatory = true,
CreatedAt = new DateTime(2025, 9, 3, 10, 46, 49, 955, DateTimeKind.Utc),
IsSystem = true,
IsActive = true,
DocumentCategoryId = employeeDocumentId,
TenantId = tenantId
},
new DocumentTypeMaster
{
Id = Guid.NewGuid(),
Name = "Bill of Quantities (BOQ)",
AllowedContentType = "application/pdf,application/msword,application/vnd.ms-excel,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet,application/vnd.openxmlformats-officedocument.wordprocessingml.document",
MaxSizeAllowedInMB = 1,
IsValidationRequired = false,
IsMandatory = false,
CreatedAt = new DateTime(2025, 9, 3, 10, 46, 49, 955, DateTimeKind.Utc),
IsSystem = true,
IsActive = true,
DocumentCategoryId = projectDocumentId,
TenantId = tenantId
},
new DocumentTypeMaster
{
Id = Guid.NewGuid(),
Name = "Work Order",
AllowedContentType = "application/pdf,application/msword,application/vnd.ms-excel,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet,application/vnd.openxmlformats-officedocument.wordprocessingml.document",
MaxSizeAllowedInMB = 1,
IsValidationRequired = false,
IsMandatory = false,
CreatedAt = new DateTime(2025, 9, 3, 10, 46, 49, 955, DateTimeKind.Utc),
IsSystem = true,
IsActive = true,
DocumentCategoryId = projectDocumentId,
TenantId = tenantId
},
new DocumentTypeMaster
{
Id = Guid.NewGuid(),
Name = "Letter of Agreement",
AllowedContentType = "application/pdf,application/msword,application/vnd.ms-excel,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet,application/vnd.openxmlformats-officedocument.wordprocessingml.document",
MaxSizeAllowedInMB = 1,
IsValidationRequired = false,
IsMandatory = false,
CreatedAt = new DateTime(2025, 9, 3, 10, 46, 49, 955, DateTimeKind.Utc),
IsSystem = true,
IsActive = true,
DocumentCategoryId = projectDocumentId,
TenantId = tenantId
},
new DocumentTypeMaster
{
Id = Guid.NewGuid(),
Name = "Health and Safety Document",
AllowedContentType = "application/pdf,application/msword,application/vnd.ms-excel,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet,application/vnd.openxmlformats-officedocument.wordprocessingml.document",
MaxSizeAllowedInMB = 1,
IsValidationRequired = false,
IsMandatory = false,
CreatedAt = new DateTime(2025, 9, 3, 10, 46, 49, 955, DateTimeKind.Utc),
IsSystem = true,
IsActive = true,
DocumentCategoryId = projectDocumentId,
TenantId = tenantId
},
new DocumentTypeMaster
{
Id = Guid.NewGuid(),
Name = "Standard Operating Procedure (SOP)",
AllowedContentType = "application/pdf,application/msword,application/vnd.ms-excel,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet,application/vnd.openxmlformats-officedocument.wordprocessingml.document",
MaxSizeAllowedInMB = 1,
IsValidationRequired = false,
IsMandatory = false,
CreatedAt = new DateTime(2025, 9, 3, 10, 46, 49, 955, DateTimeKind.Utc),
IsSystem = true,
IsActive = true,
DocumentCategoryId = projectDocumentId,
TenantId = tenantId
},
new DocumentTypeMaster
{
Id = Guid.NewGuid(),
Name = "Drawings",
AllowedContentType = "application/pdf,image/vnd.dwg,application/acad",
MaxSizeAllowedInMB = 20,
IsValidationRequired = false,
IsMandatory = false,
CreatedAt = new DateTime(2025, 9, 3, 10, 46, 49, 955, DateTimeKind.Utc),
IsSystem = true,
IsActive = true,
DocumentCategoryId = projectDocumentId,
TenantId = tenantId
}
};
}
public List<object> GetData(Guid tenantId) public List<object> GetData(Guid tenantId)
{ {
return new List<object> return new List<object>

View File

@ -67,7 +67,7 @@ namespace Marco.Pms.Services.Service
_logger.LogInfo("Basic project list requested by EmployeeId {EmployeeId}", loggedInEmployee.Id); _logger.LogInfo("Basic project list requested by EmployeeId {EmployeeId}", loggedInEmployee.Id);
// Step 2: Get the list of project IDs the user has access to // Step 2: Get the list of project IDs the user has access to
List<Guid> accessibleProjectIds = await GetMyProjects(tenantId, loggedInEmployee); List<Guid> accessibleProjectIds = await _context.Projects.Where(p => p.TenantId == tenantId).Select(p => p.Id).ToListAsync();
if (accessibleProjectIds == null || !accessibleProjectIds.Any()) if (accessibleProjectIds == null || !accessibleProjectIds.Any())
{ {
@ -96,7 +96,7 @@ namespace Marco.Pms.Services.Service
_logger.LogInfo("Starting GetAllProjects for TenantId: {TenantId}, User: {UserId}", tenantId, loggedInEmployee.Id); _logger.LogInfo("Starting GetAllProjects for TenantId: {TenantId}, User: {UserId}", tenantId, loggedInEmployee.Id);
// --- Step 1: Get a list of project IDs the user can access --- // --- Step 1: Get a list of project IDs the user can access ---
List<Guid> projectIds = await GetMyProjects(tenantId, loggedInEmployee); List<Guid> projectIds = await _context.Projects.Where(p => p.TenantId == tenantId).Select(p => p.Id).ToListAsync();
if (!projectIds.Any()) if (!projectIds.Any())
{ {
_logger.LogInfo("User has no assigned projects. Returning empty list."); _logger.LogInfo("User has no assigned projects. Returning empty list.");
@ -201,21 +201,21 @@ namespace Marco.Pms.Services.Service
using var scope = _serviceScopeFactory.CreateScope(); using var scope = _serviceScopeFactory.CreateScope();
var _permission = scope.ServiceProvider.GetRequiredService<PermissionServices>(); var _permission = scope.ServiceProvider.GetRequiredService<PermissionServices>();
// Step 1: Check global view project permission //// Step 1: Check global view project permission
var hasViewProjectPermission = await _permission.HasPermission(PermissionsMaster.ViewProject, loggedInEmployee.Id, id); //var hasViewProjectPermission = await _permission.HasPermission(PermissionsMaster.ViewProject, loggedInEmployee.Id, id);
if (!hasViewProjectPermission) //if (!hasViewProjectPermission)
{ //{
_logger.LogWarning("ViewProjects permission denied for EmployeeId: {EmployeeId}", loggedInEmployee.Id); // _logger.LogWarning("ViewProjects permission denied for EmployeeId: {EmployeeId}", loggedInEmployee.Id);
return ApiResponse<object>.ErrorResponse("Access denied", "You don't have permission to view projects", 403); // return ApiResponse<object>.ErrorResponse("Access denied", "You don't have permission to view projects", 403);
} //}
// Step 2: Check permission for this specific project //// Step 2: Check permission for this specific project
var hasProjectPermission = await _permission.HasProjectPermission(loggedInEmployee, id); //var hasProjectPermission = await _permission.HasProjectPermission(loggedInEmployee, id);
if (!hasProjectPermission) //if (!hasProjectPermission)
{ //{
_logger.LogWarning("Project-specific access denied. EmployeeId: {EmployeeId}, ProjectId: {ProjectId}", loggedInEmployee.Id, id); // _logger.LogWarning("Project-specific access denied. EmployeeId: {EmployeeId}, ProjectId: {ProjectId}", loggedInEmployee.Id, id);
return ApiResponse<object>.ErrorResponse("Access denied", "You don't have access to this project", 403); // return ApiResponse<object>.ErrorResponse("Access denied", "You don't have access to this project", 403);
} //}
// Step 3: Fetch project with status // Step 3: Fetch project with status
var projectDetails = await _cache.GetProjectDetails(id); var projectDetails = await _cache.GetProjectDetails(id);
@ -368,17 +368,20 @@ namespace Marco.Pms.Services.Service
return ApiResponse<object>.ErrorResponse("Access Denied", "You do not have permission to create a project for this tenant.", 403); return ApiResponse<object>.ErrorResponse("Access Denied", "You do not have permission to create a project for this tenant.", 403);
} }
var promoterId = model.PromoterId ?? loggedInEmployee.OrganizationId;
var pmcId = model.PMCId ?? loggedInEmployee.OrganizationId;
// Step 2: Concurrent validation for Promoter and PMC organization existence. // Step 2: Concurrent validation for Promoter and PMC organization existence.
// Run database queries in parallel for better performance. // Run database queries in parallel for better performance.
var promoterTask = Task.Run(async () => var promoterTask = Task.Run(async () =>
{ {
await using var context = await _dbContextFactory.CreateDbContextAsync(); await using var context = await _dbContextFactory.CreateDbContextAsync();
return await context.Organizations.FirstOrDefaultAsync(o => o.Id == model.PromoterId); return await context.Organizations.FirstOrDefaultAsync(o => o.Id == promoterId);
}); });
var pmcTask = Task.Run(async () => var pmcTask = Task.Run(async () =>
{ {
await using var context = await _dbContextFactory.CreateDbContextAsync(); await using var context = await _dbContextFactory.CreateDbContextAsync();
return await context.Organizations.FirstOrDefaultAsync(o => o.Id == model.PMCId); return await context.Organizations.FirstOrDefaultAsync(o => o.Id == pmcId);
}); });
await Task.WhenAll(promoterTask, pmcTask); await Task.WhenAll(promoterTask, pmcTask);
@ -388,18 +391,20 @@ namespace Marco.Pms.Services.Service
if (promoter == null) if (promoter == null)
{ {
_logger.LogWarning("Promoter check failed. PromoterId={PromoterId} not found.", model.PromoterId); _logger.LogWarning("Promoter check failed. PromoterId={PromoterId} not found.", promoterId);
return ApiResponse<object>.ErrorResponse("Promoter not found", "Promoter not found in database.", 404); return ApiResponse<object>.ErrorResponse("Promoter not found", "Promoter not found in database.", 404);
} }
if (pmc == null) if (pmc == null)
{ {
_logger.LogWarning("PMC check failed. PMCId={PMCId} not found.", model.PMCId); _logger.LogWarning("PMC check failed. PMCId={PMCId} not found.", pmcId);
return ApiResponse<object>.ErrorResponse("PMC not found", "PMC not found in database.", 404); return ApiResponse<object>.ErrorResponse("PMC not found", "PMC not found in database.", 404);
} }
// Step 3: Prepare the project entity. // Step 3: Prepare the project entity.
var loggedInUserId = loggedInEmployee.Id; var loggedInUserId = loggedInEmployee.Id;
var project = _mapper.Map<Project>(model); var project = _mapper.Map<Project>(model);
project.PromoterId = promoterId;
project.PMCId = pmcId;
project.TenantId = tenantId; project.TenantId = tenantId;
// Step 4: Save the new project to the database. // Step 4: Save the new project to the database.
@ -476,6 +481,7 @@ namespace Marco.Pms.Services.Service
// --- Step 1: Fetch the Existing Entity from the Database --- // --- Step 1: Fetch the Existing Entity from the Database ---
// This is crucial to avoid the data loss bug. We only want to modify an existing record. // This is crucial to avoid the data loss bug. We only want to modify an existing record.
var existingProject = await _context.Projects var existingProject = await _context.Projects
.AsNoTracking()
.Where(p => p.Id == id && p.TenantId == tenantId) .Where(p => p.Id == id && p.TenantId == tenantId)
.SingleOrDefaultAsync(); .SingleOrDefaultAsync();
@ -496,17 +502,20 @@ namespace Marco.Pms.Services.Service
return ApiResponse<object>.ErrorResponse("Access Denied", "You do not have permission to update a project for this tenant.", 403); return ApiResponse<object>.ErrorResponse("Access Denied", "You do not have permission to update a project for this tenant.", 403);
} }
var promoterId = model.PromoterId ?? loggedInEmployee.OrganizationId;
var pmcId = model.PMCId ?? loggedInEmployee.OrganizationId;
// 1bb. Concurrent validation for Promoter and PMC organization existence. // 1bb. Concurrent validation for Promoter and PMC organization existence.
// Run database queries in parallel for better performance. // Run database queries in parallel for better performance.
var promoterTask = Task.Run(async () => var promoterTask = Task.Run(async () =>
{ {
await using var context = await _dbContextFactory.CreateDbContextAsync(); await using var context = await _dbContextFactory.CreateDbContextAsync();
return await context.Organizations.FirstOrDefaultAsync(o => o.Id == model.PromoterId); return await context.Organizations.FirstOrDefaultAsync(o => o.Id == promoterId);
}); });
var pmcTask = Task.Run(async () => var pmcTask = Task.Run(async () =>
{ {
await using var context = await _dbContextFactory.CreateDbContextAsync(); await using var context = await _dbContextFactory.CreateDbContextAsync();
return await context.Organizations.FirstOrDefaultAsync(o => o.Id == model.PMCId); return await context.Organizations.FirstOrDefaultAsync(o => o.Id == pmcId);
}); });
await Task.WhenAll(promoterTask, pmcTask); await Task.WhenAll(promoterTask, pmcTask);
@ -516,12 +525,12 @@ namespace Marco.Pms.Services.Service
if (promoter == null) if (promoter == null)
{ {
_logger.LogWarning("Promoter check failed. PromoterId={PromoterId} not found.", model.PromoterId); _logger.LogWarning("Promoter check failed. PromoterId={PromoterId} not found.", promoterId);
return ApiResponse<object>.ErrorResponse("Promoter not found", "Promoter not found in database.", 404); return ApiResponse<object>.ErrorResponse("Promoter not found", "Promoter not found in database.", 404);
} }
if (pmc == null) if (pmc == null)
{ {
_logger.LogWarning("PMC check failed. PMCId={PMCId} not found.", model.PMCId); _logger.LogWarning("PMC check failed. PMCId={PMCId} not found.", pmcId);
return ApiResponse<object>.ErrorResponse("PMC not found", "PMC not found in database.", 404); return ApiResponse<object>.ErrorResponse("PMC not found", "PMC not found in database.", 404);
} }
@ -538,8 +547,11 @@ namespace Marco.Pms.Services.Service
// This only modifies the properties defined in the mapping, preventing data loss. // This only modifies the properties defined in the mapping, preventing data loss.
_mapper.Map(model, existingProject); _mapper.Map(model, existingProject);
existingProject.PromoterId = promoterId;
existingProject.PMCId = pmcId;
// Mark the entity as modified (if your mapping doesn't do it automatically). // Mark the entity as modified (if your mapping doesn't do it automatically).
_context.Entry(existingProject).State = EntityState.Modified; _context.Projects.Update(existingProject);
try try
{ {

View File

@ -48,7 +48,7 @@
}, },
"MongoDB": { "MongoDB": {
"SerilogDatabaseUrl": "mongodb://localhost:27017/DotNetLogs", "SerilogDatabaseUrl": "mongodb://localhost:27017/DotNetLogs",
"ConnectionString": "mongodb://localhost:27017/MarcoBMS_Caches?socketTimeoutMS=500&serverSelectionTimeoutMS=500&connectTimeoutMS=500", "ConnectionString": "mongodb://devuser:DevPass123@147.93.98.152:27017/MarcoBMSCacheLocalDev?authSource=admin&replicaSet=rs01",
"ModificationConnectionString": "mongodb://devuser:DevPass123@147.93.98.152:27017/MarcoBMSLocalDev?authSource=admin&eplicaSet=rs01&directConnection=true" "ModificationConnectionString": "mongodb://devuser:DevPass123@147.93.98.152:27017/MarcoBMSLocalDev?authSource=admin&replicaSet=rs01&directConnection=true"
} }
} }