Compare commits

..

No commits in common. "main" and "Ashutosh_Enhancement_#1355" have entirely different histories.

60 changed files with 895 additions and 22045 deletions

File diff suppressed because one or more lines are too long

View File

@ -1,92 +0,0 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Marco.Pms.DataAccess.Migrations
{
/// <inheritdoc />
public partial class Added_Forgin_Key_For_Approver : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_Attendes_Employees_EmployeeID",
table: "Attendes");
migrationBuilder.RenameColumn(
name: "EmployeeID",
table: "Attendes",
newName: "EmployeeId");
migrationBuilder.RenameColumn(
name: "ApprovedBy",
table: "Attendes",
newName: "ApprovedById");
migrationBuilder.RenameIndex(
name: "IX_Attendes_EmployeeID",
table: "Attendes",
newName: "IX_Attendes_EmployeeId");
migrationBuilder.CreateIndex(
name: "IX_Attendes_ApprovedById",
table: "Attendes",
column: "ApprovedById");
migrationBuilder.AddForeignKey(
name: "FK_Attendes_Employees_ApprovedById",
table: "Attendes",
column: "ApprovedById",
principalTable: "Employees",
principalColumn: "Id");
migrationBuilder.AddForeignKey(
name: "FK_Attendes_Employees_EmployeeId",
table: "Attendes",
column: "EmployeeId",
principalTable: "Employees",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_Attendes_Employees_ApprovedById",
table: "Attendes");
migrationBuilder.DropForeignKey(
name: "FK_Attendes_Employees_EmployeeId",
table: "Attendes");
migrationBuilder.DropIndex(
name: "IX_Attendes_ApprovedById",
table: "Attendes");
migrationBuilder.RenameColumn(
name: "EmployeeId",
table: "Attendes",
newName: "EmployeeID");
migrationBuilder.RenameColumn(
name: "ApprovedById",
table: "Attendes",
newName: "ApprovedBy");
migrationBuilder.RenameIndex(
name: "IX_Attendes_EmployeeId",
table: "Attendes",
newName: "IX_Attendes_EmployeeID");
migrationBuilder.AddForeignKey(
name: "FK_Attendes_Employees_EmployeeID",
table: "Attendes",
column: "EmployeeID",
principalTable: "Employees",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
}
}
}

File diff suppressed because one or more lines are too long

View File

@ -1,29 +0,0 @@
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

@ -1,70 +0,0 @@
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,10 +172,7 @@ namespace Marco.Pms.DataAccess.Migrations
b.Property<int>("Activity") b.Property<int>("Activity")
.HasColumnType("int"); .HasColumnType("int");
b.Property<DateTime?>("ApprovedAt") b.Property<Guid?>("ApprovedBy")
.HasColumnType("datetime(6)");
b.Property<Guid?>("ApprovedById")
.HasColumnType("char(36)"); .HasColumnType("char(36)");
b.Property<DateTime>("AttendanceDate") b.Property<DateTime>("AttendanceDate")
@ -188,7 +185,7 @@ namespace Marco.Pms.DataAccess.Migrations
b.Property<DateTime>("Date") b.Property<DateTime>("Date")
.HasColumnType("datetime(6)"); .HasColumnType("datetime(6)");
b.Property<Guid>("EmployeeId") b.Property<Guid>("EmployeeID")
.HasColumnType("char(36)"); .HasColumnType("char(36)");
b.Property<DateTime?>("InTime") b.Property<DateTime?>("InTime")
@ -203,22 +200,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)");
b.HasKey("Id"); b.HasKey("Id");
b.HasIndex("ApprovedById"); b.HasIndex("EmployeeID");
b.HasIndex("EmployeeId");
b.HasIndex("RequestedById");
b.HasIndex("TenantId"); b.HasIndex("TenantId");
@ -1843,10 +1830,6 @@ 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)");
@ -4718,18 +4701,10 @@ namespace Marco.Pms.DataAccess.Migrations
{ {
b.HasOne("Marco.Pms.Model.Employees.Employee", "Approver") b.HasOne("Marco.Pms.Model.Employees.Employee", "Approver")
.WithMany() .WithMany()
.HasForeignKey("ApprovedById"); .HasForeignKey("EmployeeID")
b.HasOne("Marco.Pms.Model.Employees.Employee", "Employee")
.WithMany()
.HasForeignKey("EmployeeId")
.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")
@ -4738,10 +4713,6 @@ namespace Marco.Pms.DataAccess.Migrations
b.Navigation("Approver"); b.Navigation("Approver");
b.Navigation("Employee");
b.Navigation("RequestedBy");
b.Navigation("Tenant"); b.Navigation("Tenant");
}); });

View File

@ -212,48 +212,6 @@ namespace Marco.Pms.Helpers.CacheHelper
return true; return true;
} }
public async Task<bool> ClearAllEmployeesFromCacheByOnlyEmployeeId(Guid employeeId)
{
var employeeIdString = employeeId.ToString();
try
{
var filter = Builders<EmployeePermissionMongoDB>.Filter.Eq(e => e.Id, employeeIdString);
var result = await _collection.DeleteManyAsync(filter);
if (result.DeletedCount == 0)
return false;
return true;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error occured while deleting employee profile");
return false;
}
}
public async Task<bool> ClearAllEmployeesFromCacheByTenantId(Guid tenantId)
{
var tenantIdString = tenantId.ToString();
try
{
var filter = Builders<EmployeePermissionMongoDB>.Filter.Eq(e => e.TenantId, tenantIdString);
var result = await _collection.DeleteManyAsync(filter);
if (result.DeletedCount == 0)
return false;
return true;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error occured while deleting employee profile");
return false;
}
}
public async Task<bool> ClearAllEmployeesFromCacheByEmployeeIds(List<string> employeeIds, Guid tenantId) public async Task<bool> ClearAllEmployeesFromCacheByEmployeeIds(List<string> employeeIds, Guid tenantId)
{ {
var tenantIdString = tenantId.ToString(); var tenantIdString = tenantId.ToString();

View File

@ -74,20 +74,14 @@ namespace Marco.Pms.Helpers
Id = promotor.Id.ToString(), Id = promotor.Id.ToString(),
Name = promotor.Name, Name = promotor.Name,
ContactPerson = promotor.ContactPerson, ContactPerson = promotor.ContactPerson,
Email = promotor.Email, Email = promotor.Email
Address = promotor.Address,
ContactNumber = promotor.ContactNumber,
SPRID = promotor.SPRID
}), }),
Builders<ProjectMongoDB>.Update.Set(r => r.PMC, new OrganizationMongoDB Builders<ProjectMongoDB>.Update.Set(r => r.PMC, new OrganizationMongoDB
{ {
Id = pmc.Id.ToString(), Id = pmc.Id.ToString(),
Name = pmc.Name, Name = pmc.Name,
ContactPerson = pmc.ContactPerson, ContactPerson = pmc.ContactPerson,
Email = pmc.Email, Email = pmc.Email
Address = promotor.Address,
ContactNumber = promotor.ContactNumber,
SPRID = promotor.SPRID
}), }),
Builders<ProjectMongoDB>.Update.Set(r => r.StartDate, project.StartDate), Builders<ProjectMongoDB>.Update.Set(r => r.StartDate, project.StartDate),
Builders<ProjectMongoDB>.Update.Set(r => r.EndDate, project.EndDate), Builders<ProjectMongoDB>.Update.Set(r => r.EndDate, project.EndDate),

View File

@ -1,8 +1,8 @@
using Marco.Pms.Model.Dtos.Attendance; using System.ComponentModel.DataAnnotations.Schema;
using Marco.Pms.Model.Dtos.Attendance;
using Marco.Pms.Model.Employees; using Marco.Pms.Model.Employees;
using Marco.Pms.Model.Utilities; using Marco.Pms.Model.Utilities;
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation; using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
using System.ComponentModel.DataAnnotations.Schema;
namespace Marco.Pms.Model.AttendanceModule namespace Marco.Pms.Model.AttendanceModule
{ {
@ -10,11 +10,9 @@ namespace Marco.Pms.Model.AttendanceModule
{ {
public Guid Id { get; set; } public Guid Id { get; set; }
public string Comment { get; set; } = string.Empty; public string Comment { get; set; } = string.Empty;
public Guid EmployeeId { get; set; } public Guid EmployeeID { get; set; }
[ForeignKey("EmployeeId")]
[ValidateNever]
public Employee? Employee { get; set; }
public DateTime Date { get; set; } public DateTime Date { get; set; }
public Guid ProjectID { get; set; } public Guid ProjectID { get; set; }
@ -24,17 +22,9 @@ namespace Marco.Pms.Model.AttendanceModule
public bool IsApproved { get; set; } public bool IsApproved { get; set; }
public ATTENDANCE_MARK_TYPE Activity { get; set; } public ATTENDANCE_MARK_TYPE Activity { get; set; }
public Guid? ApprovedById { get; set; } public Guid? ApprovedBy { get; set; }
[ForeignKey("EmployeeID")]
[ForeignKey("ApprovedById")]
[ValidateNever] [ValidateNever]
public Employee? Approver { get; set; } public Employee? Approver { get; set; }
public DateTime? RequestedAt { get; set; }
public DateTime? ApprovedAt { get; set; }
public Guid? RequestedById { get; set; }
[ForeignKey("RequestedById")]
[ValidateNever]
public Employee? RequestedBy { get; set; }
} }
} }

View File

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

View File

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

View File

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

@ -28,13 +28,11 @@
public required string FirstName { get; set; } public required string FirstName { get; set; }
public required string LastName { get; set; } public required string LastName { get; set; }
public required string PhoneNumber { get; set; } public required string PhoneNumber { get; set; }
public string? Email { get; set; }
public required DateTime JoiningDate { get; set; } public required DateTime JoiningDate { get; set; }
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 required Guid OrganizationId { get; set; }
public required bool HasApplicationAccess { get; set; }
} }
} }

View File

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

View File

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

View File

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

View File

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

View File

@ -8,6 +8,6 @@
public required string Address { get; set; } public required string Address { get; set; }
public required string ContactNumber { get; set; } public required string ContactNumber { get; set; }
public string? logoImage { get; set; } public string? logoImage { get; set; }
public List<Guid>? ServiceIds { get; set; } public required List<Guid> ServiceIds { get; set; }
} }
} }

View File

@ -7,6 +7,6 @@
public required string ContactPerson { get; set; } public required string ContactPerson { get; set; }
public required string Address { get; set; } public required string Address { get; set; }
public required string ContactNumber { get; set; } public required string ContactNumber { get; set; }
public List<Guid>? ServiceIds { get; set; } public required List<Guid> ServiceIds { get; set; }
} }
} }

View File

@ -5,7 +5,7 @@
public Guid EmployeeId { get; set; } public Guid EmployeeId { get; set; }
public Guid JobRoleId { get; set; } public Guid JobRoleId { get; set; }
public Guid ProjectId { get; set; } public Guid ProjectId { get; set; }
public Guid? ServiceId { get; set; } public Guid ServiceId { get; set; }
public bool Status { get; set; } public bool Status { get; set; }
} }
@ -14,7 +14,7 @@
{ {
public Guid ProjectId { get; set; } public Guid ProjectId { get; set; }
public Guid JobRoleId { get; set; } public Guid JobRoleId { get; set; }
public Guid? ServiceId { get; set; } public Guid ServiceId { get; set; }
public bool Status { get; set; } public bool Status { get; set; }
} }
} }

View File

@ -17,6 +17,6 @@
public required string OrganizationSize { get; set; } public required string OrganizationSize { get; set; }
public required Guid IndustryId { get; set; } public required Guid IndustryId { get; set; }
public required string Reference { get; set; } public required string Reference { get; set; }
public List<Guid>? ServiceIds { get; set; } public required List<Guid> ServiceIds { get; set; }
} }
} }

View File

@ -1,224 +0,0 @@
using Marco.Pms.Model.AppMenu;
namespace Marco.Pms.Model.Entitlements
{
public static class MenuStaticMaster
{
public static readonly MenuSection menu = new MenuSection
{
Header = "Main Navigation",
Title = "Main Menu",
Items = new List<MenuItem>
{
new MenuItem
{
Text = "Dashboard",
Icon = "bx bx-home",
Available = true,
Link = "/dashboard",
PermissionIds = new List<string>(),
Submenu = new List<SubMenuItem>()
},
new MenuItem
{
Text = "Projects",
Icon = "bx bx-building-house",
Available = true,
Link = "",
PermissionIds = new List<string>
{
"6ea44136-987e-44ba-9e5d-1cf8f5837ebc",
"172fc9b6-755b-4f62-ab26-55c34a330614",
"b94802ce-0689-4643-9e1d-11c86950c35b",
"8d7cc6e3-9147-41f7-aaa7-fa507e450bd4",
"cf2825ad-453b-46aa-91d9-27c124d63373",
"9fcc5f87-25e3-4846-90ac-67a71ab92e3c",
"08752f33-3b29-4816-b76b-ea8a968ed3c5",
"6a32379b-8b3f-49a6-8c48-4b7ac1b55dc2",
"db4e40c5-2ba9-4b6d-b8a6-a16a250ff99c"
},
Submenu = new List<SubMenuItem>
{
new SubMenuItem
{
Text = "Project List",
Available = true,
Link = "/projects",
PermissionIds = new List<string>
{
"6ea44136-987e-44ba-9e5d-1cf8f5837ebc",
"172fc9b6-755b-4f62-ab26-55c34a330614",
"b94802ce-0689-4643-9e1d-11c86950c35b",
"8d7cc6e3-9147-41f7-aaa7-fa507e450bd4",
"cf2825ad-453b-46aa-91d9-27c124d63373",
"9fcc5f87-25e3-4846-90ac-67a71ab92e3c",
"08752f33-3b29-4816-b76b-ea8a968ed3c5"
}
},
new SubMenuItem
{
Text = "Daily Task Planning",
Available = true,
Link = "/activities/task",
PermissionIds = new List<string>
{
"8d7cc6e3-9147-41f7-aaa7-fa507e450bd4",
"9fcc5f87-25e3-4846-90ac-67a71ab92e3c",
"6a32379b-8b3f-49a6-8c48-4b7ac1b55dc2"
}
},
new SubMenuItem
{
Text = "Daily Progress Report",
Available = true,
Link = "/activities/records",
PermissionIds = new List<string>
{
"9fcc5f87-25e3-4846-90ac-67a71ab92e3c",
"6a32379b-8b3f-49a6-8c48-4b7ac1b55dc2",
"db4e40c5-2ba9-4b6d-b8a6-a16a250ff99c"
}
},
new SubMenuItem
{
Text = "Image Gallary",
Available = true,
Link = "/gallary",
PermissionIds = new List<string>()
},
new SubMenuItem
{
Text = "Organizations",
Available = true,
Link = "/organizations",
PermissionIds = new List<string>
{
"068cb3c1-49c5-4746-9f29-1fce16e820ac",
"c1ae1363-ab8a-4bd9-a9d1-8c2c6083873a",
"7a6cf830-0008-4e03-b31d-0d050cb634f4"
}
},
new SubMenuItem
{
Text = "Project Report",
Available = true,
Link = "/activities/reports",
PermissionIds = new List<string>()
},
}
},
new MenuItem
{
Text = "Employees",
Icon = "bx bx-user",
Available = true,
Link = "/employees",
PermissionIds = new List<string>
{
"60611762-7f8a-4fb5-b53f-b1139918796b",
"b82d2b7e-0d52-45f3-997b-c008ea460e7f",
"a97d366a-c2bb-448d-be93-402bd2324566",
"fbd213e0-0250-46f1-9f5f-4b2a1e6e76a3"
},
Submenu = new List<SubMenuItem>()
},
new MenuItem
{
Text = "Attendance",
Icon = "bx bx-list-ul",
Available = true,
Link = "/activities/Attendance",
PermissionIds = new List<string>
{
"915e6bff-65f6-4e3f-aea8-3fd217d3ea9e",
"57802c4a-00aa-4a1f-a048-fd2f70dd44b6",
"ccb0589f-712b-43de-92ed-5b6088e7dc4e"
},
Submenu = new List<SubMenuItem>()
},
new MenuItem
{
Text = "Directory",
Icon = "bx bx-group",
Available = true,
Link = "/directory",
PermissionIds = new List<string>
{
"4286a13b-bb40-4879-8c6d-18e9e393beda",
"62668630-13ce-4f52-a0f0-db38af2230c5",
"0f919170-92d4-4337-abd3-49b66fc871bb"
},
Submenu = new List<SubMenuItem>()
},
new MenuItem
{
Text = "Expense",
Icon = "bx bx-receipt",
Available = true,
Link = "/expenses",
PermissionIds = new List<string>
{
"385be49f-8fde-440e-bdbc-3dffeb8dd116",
"01e06444-9ca7-4df4-b900-8c3fa051b92f",
"0f57885d-bcb2-4711-ac95-d841ace6d5a7",
"1f4bda08-1873-449a-bb66-3e8222bd871b",
"eaafdd76-8aac-45f9-a530-315589c6deca",
"ea5a1529-4ee8-4828-80ea-0e23c9d4dd11",
"ea5a1529-4ee8-4828-80ea-0e23c9d4dd11"
},
Submenu = new List<SubMenuItem>()
},
new MenuItem
{
Text = "Administration",
Icon = "bx bx-box",
Available = true,
Link = "",
PermissionIds = new List<string>
{
"5ffbafe0-7ab0-48b1-bb50-c1bf76b65f9d",
"588a8824-f924-4955-82d8-fc51956cf323",
"d032cb1a-3f30-462c-bef0-7ace73a71c0b",
"00e20637-ce8d-4417-bec4-9b31b5e65092",
"647145c6-2108-4c98-aab4-178602236e55"
},
Submenu = new List<SubMenuItem>
{
new SubMenuItem
{
Text = "Tenant",
Available = true,
Link = "/tenants",
PermissionIds = new List<string>
{
"d032cb1a-3f30-462c-bef0-7ace73a71c0b",
"00e20637-ce8d-4417-bec4-9b31b5e65092",
"647145c6-2108-4c98-aab4-178602236e55"
}
},
new SubMenuItem
{
Text = "Masters",
Available = true,
Link = "/masters",
PermissionIds = new List<string>
{
"5ffbafe0-7ab0-48b1-bb50-c1bf76b65f9d",
"588a8824-f924-4955-82d8-fc51956cf323"
}
},
}
},
new MenuItem
{
Text = "Inventory",
Icon = "bx bx-store",
Available = true,
Link = "/inventory",
PermissionIds = new List<string>(),
Submenu = new List<SubMenuItem>()
},
}
};
}
}

View File

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

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

@ -36,9 +36,7 @@ namespace Marco.Pms.Model.Mapper
IsRootUser = model.ApplicationUser?.IsRootUser ?? false, IsRootUser = model.ApplicationUser?.IsRootUser ?? false,
IsSystem = model.IsSystem, IsSystem = model.IsSystem,
JoiningDate = model.JoiningDate, JoiningDate = model.JoiningDate,
TenantId = model.TenantId ?? Guid.Empty, TenantId = model.TenantId ?? Guid.Empty
HasApplicationAccess = model.HasApplicationAccess,
OrganizationId = model.OrganizationId
}; };
} }
public static BasicEmployeeVM ToBasicEmployeeVMFromEmployee(this Employee employee) public static BasicEmployeeVM ToBasicEmployeeVMFromEmployee(this Employee employee)

View File

@ -19,7 +19,6 @@ namespace Marco.Pms.Model.MongoDBModels.Expenses
public DateTime CreatedAt { get; set; } public DateTime CreatedAt { get; set; }
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 string? ExpenseUId { get; set; }
public double Amount { get; set; } public double Amount { 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>();

View File

@ -3,11 +3,8 @@
public class OrganizationMongoDB public class OrganizationMongoDB
{ {
public string Id { get; set; } = string.Empty; public string Id { get; set; } = string.Empty;
public string? Name { get; set; } public string Name { get; set; } = string.Empty;
public string? Email { get; set; } public string Email { get; set; } = string.Empty;
public string? ContactPerson { get; set; } public string ContactPerson { get; set; } = string.Empty;
public string? Address { get; set; }
public string? ContactNumber { get; set; }
public double SPRID { get; set; }
} }
} }

View File

@ -1,5 +1,4 @@
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
{ {
@ -7,20 +6,15 @@ 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

@ -25,8 +25,6 @@
public Guid TenantId { get; set; } public Guid TenantId { get; set; }
public bool IsSystem { get; set; } public bool IsSystem { get; set; }
public string? JobRole { get; set; } public string? JobRole { get; set; }
public bool HasApplicationAccess { get; set; }
public Guid OrganizationId { get; set; }
} }
} }

View File

@ -19,7 +19,6 @@ namespace Marco.Pms.Model.ViewModels.Expenses
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 double Amount { get; set; } public double Amount { get; set; }
public ExpensesStatusMasterVM? Status { get; set; } public ExpensesStatusMasterVM? Status { get; set; }
public List<ExpensesStatusMasterVM>? NextStatus { get; set; } public List<ExpensesStatusMasterVM>? NextStatus { get; set; }

View File

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

@ -1,26 +0,0 @@
using Marco.Pms.Model.Master;
using Marco.Pms.Model.ViewModels.Activities;
namespace Marco.Pms.Model.ViewModels.Organization
{
public class OrganizationDetailsVM
{
public Guid Id { get; set; }
public string? Name { get; set; }
public string? Email { get; set; }
public string? ContactPerson { get; set; }
public string? Address { get; set; }
public string? ContactNumber { get; set; }
public double SPRID { get; set; }
public int ActiveEmployeeCount { get; set; }
public int ActiveApplicationUserCount { get; set; }
public DateTime CreatedAt { get; set; }
public BasicEmployeeVM? CreatedBy { get; set; }
public BasicEmployeeVM? UpdatedBy { get; set; }
public DateTime? UpdatedAt { get; set; }
public bool IsActive { get; set; }
public List<ProjectServiceMappingVM>? Projects { get; set; }
public List<GlobalServiceMaster>? Services { get; set; }
public string? logoImage { get; set; }
}
}

View File

@ -11,7 +11,6 @@ namespace Marco.Pms.Model.ViewModels.Organization
public string? ContactPerson { get; set; } public string? ContactPerson { get; set; }
public double SPRID { get; set; } public double SPRID { get; set; }
public string? logoImage { get; set; } public string? logoImage { get; set; }
public string? OrganizationType { get; set; }
public DateTime AssignedDate { get; set; } public DateTime AssignedDate { get; set; }
public BasicEmployeeVM? AssignedBy { get; set; } public BasicEmployeeVM? AssignedBy { get; set; }
public ServiceMasterVM? Service { get; set; } public ServiceMasterVM? Service { get; set; }

View File

@ -1,16 +0,0 @@
using Marco.Pms.Model.ViewModels.Master;
using Marco.Pms.Model.ViewModels.Projects;
namespace Marco.Pms.Model.ViewModels.Organization
{
public class ProjectServiceMappingVM
{
public BasicProjectVM? Project { get; set; }
public ServiceMasterVM? Service { get; set; }
public DateTime PlannedStartDate { get; set; }
public DateTime PlannedEndDate { get; set; }
public DateTime ActualStartDate { get; set; }
public DateTime? ActualEndDate { get; set; }
public bool IsActive { get; set; }
}
}

View File

@ -7,18 +7,15 @@
public required string TimeStamp { get; set; } public required string TimeStamp { get; set; }
public int TodaysAttendances { get; set; } public int TodaysAttendances { get; set; }
public int TotalEmployees { get; set; } public int TotalEmployees { get; set; }
public double AttendancePercentage { get; set; }
public int RegularizationPending { get; set; } public int RegularizationPending { get; set; }
public int CheckoutPending { get; set; } public int CheckoutPending { get; set; }
public double TotalPlannedWork { get; set; } public double TotalPlannedWork { get; set; }
public double TotalCompletedWork { get; set; } public double TotalCompletedWork { get; set; }
public double CompletionStatus { get; set; }
public double TotalPlannedTask { get; set; } public double TotalPlannedTask { get; set; }
public double TotalCompletedTask { get; set; } public double TotalCompletedTask { get; set; }
public double TaskPercentage { get; set; } public double CompletionStatus { get; set; }
public int ReportPending { get; set; } public int ReportPending { get; set; }
public int TodaysAssignTasks { get; set; } public int TodaysAssignTasks { get; set; }
public int TodaysCompletedTasks { get; set; }
public List<TeamOnSite> TeamOnSite { get; set; } = new List<TeamOnSite>(); public List<TeamOnSite> TeamOnSite { get; set; } = new List<TeamOnSite>();
public List<PerformedTask> PerformedTasks { get; set; } = new List<PerformedTask>(); public List<PerformedTask> PerformedTasks { get; set; } = new List<PerformedTask>();
public List<PerformedAttendance> PerformedAttendance { get; set; } = new List<PerformedAttendance>(); public List<PerformedAttendance> PerformedAttendance { get; set; } = new List<PerformedAttendance>();

View File

@ -456,14 +456,6 @@ namespace Marco.Pms.Services.Controllers
// Step 2: Fetch all menu sections for the tenant // Step 2: Fetch all menu sections for the tenant
var menus = await _sideBarMenuHelper.GetAllMenuSectionsAsync(tenantId); var menus = await _sideBarMenuHelper.GetAllMenuSectionsAsync(tenantId);
if (!(menus?.Any() ?? false))
{
menus = new List<MenuSection>
{
MenuStaticMaster.menu
};
}
foreach (var menu in menus) foreach (var menu in menus)
{ {
var allowedItems = new List<MenuItem>(); var allowedItems = new List<MenuItem>();
@ -575,23 +567,24 @@ namespace Marco.Pms.Services.Controllers
{ {
ProjectManagement, new List<MasterMenuVM> ProjectManagement, new List<MasterMenuVM>
{ {
new MasterMenuVM { Id = 3, Name = "Work Category" }, new MasterMenuVM { Id = 3, Name = "Activity" },
new MasterMenuVM { Id = 8, Name = "Services" } new MasterMenuVM { Id = 4, Name = "Work Category" },
new MasterMenuVM { Id = 9, Name = "Services" }
//new MasterMenuVM { Id = 10, Name = "Payment Mode" } //new MasterMenuVM { Id = 10, Name = "Payment Mode" }
} }
}, },
{ {
DirectoryManagement, new List<MasterMenuVM> DirectoryManagement, new List<MasterMenuVM>
{ {
new MasterMenuVM { Id = 4, Name = "Contact Category" }, new MasterMenuVM { Id = 5, Name = "Contact Category" },
new MasterMenuVM { Id = 5, Name = "Contact Tag" } new MasterMenuVM { Id = 6, Name = "Contact Tag" }
} }
}, },
{ {
ExpenseManagement, new List<MasterMenuVM> ExpenseManagement, new List<MasterMenuVM>
{ {
new MasterMenuVM { Id = 6, Name = "Expense Type" }, new MasterMenuVM { Id = 7, Name = "Expense Type" },
new MasterMenuVM { Id = 7, Name = "Payment Mode" } new MasterMenuVM { Id = 8, Name = "Payment Mode" }
} }
} }
}; };
@ -640,13 +633,6 @@ namespace Marco.Pms.Services.Controllers
{ {
// Step 2: Fetch all menu sections for the tenant // Step 2: Fetch all menu sections for the tenant
var menus = await _sideBarMenuHelper.GetAllMenuSectionsAsync(tenantId); var menus = await _sideBarMenuHelper.GetAllMenuSectionsAsync(tenantId);
if (!(menus?.Any() ?? false))
{
menus = new List<MenuSection>
{
MenuStaticMaster.menu
};
}
List<MenuSectionApplicationVM> response = new List<MenuSectionApplicationVM>(); List<MenuSectionApplicationVM> response = new List<MenuSectionApplicationVM>();
foreach (var menu in menus) foreach (var menu in menus)
@ -733,42 +719,7 @@ namespace Marco.Pms.Services.Controllers
menu.Items = allowedItems; menu.Items = allowedItems;
} }
var viewDocumentTask = Task.Run(async () => if (await _permissions.HasPermission(PermissionsMaster.ViewDocument, employeeId))
{
using var taskScope = _serviceScopeFactory.CreateScope();
var permissions = taskScope.ServiceProvider.GetRequiredService<PermissionServices>();
return await permissions.HasPermission(PermissionsMaster.ViewDocument, employeeId);
});
var uploadDocumentTask = Task.Run(async () =>
{
using var taskScope = _serviceScopeFactory.CreateScope();
var permissions = taskScope.ServiceProvider.GetRequiredService<PermissionServices>();
return await permissions.HasPermission(PermissionsMaster.UploadDocument, employeeId);
});
var verifyDocumentTask = Task.Run(async () =>
{
using var taskScope = _serviceScopeFactory.CreateScope();
var permissions = taskScope.ServiceProvider.GetRequiredService<PermissionServices>();
return await permissions.HasPermission(PermissionsMaster.VerifyDocument, employeeId);
});
var downloadDocumentTask = Task.Run(async () =>
{
using var taskScope = _serviceScopeFactory.CreateScope();
var permissions = taskScope.ServiceProvider.GetRequiredService<PermissionServices>();
return await permissions.HasPermission(PermissionsMaster.DownloadDocument, employeeId);
});
await Task.WhenAll(viewDocumentTask, uploadDocumentTask, verifyDocumentTask, downloadDocumentTask);
var viewDocument = viewDocumentTask.Result;
var uploadDocument = uploadDocumentTask.Result;
var verifyDocument = verifyDocumentTask.Result;
var downloadDocument = downloadDocumentTask.Result;
if (viewDocument || uploadDocument || verifyDocument || downloadDocument)
{ {
response.Add(new MenuSectionApplicationVM response.Add(new MenuSectionApplicationVM
{ {

View File

@ -1,5 +1,4 @@
using AutoMapper; using Marco.Pms.DataAccess.Data;
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;
@ -7,7 +6,6 @@ using Marco.Pms.Model.Entitlements;
using Marco.Pms.Model.Mapper; using Marco.Pms.Model.Mapper;
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.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;
@ -30,41 +28,48 @@ namespace MarcoBMS.Services.Controllers
public class AttendanceController : ControllerBase public class AttendanceController : ControllerBase
{ {
private readonly ApplicationDbContext _context; private readonly ApplicationDbContext _context;
private readonly IServiceScopeFactory _serviceScopeFactory; private readonly EmployeeHelper _employeeHelper;
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 Guid tenantId; private readonly IHubContext<MarcoHub> _signalR;
private readonly IMapper _mapper; private readonly IFirebaseService _firebase;
public AttendanceController( public AttendanceController(
ApplicationDbContext context, ApplicationDbContext context, EmployeeHelper employeeHelper, IProjectServices projectServices, UserHelper userHelper,
UserHelper userHelper, S3UploadService s3Service, ILoggingService logger, PermissionServices permission, IHubContext<MarcoHub> signalR, IFirebaseService firebase)
IServiceScopeFactory serviceScopeFactory,
ILoggingService logger,
PermissionServices permission,
IMapper mapper)
{ {
_context = context; _context = context;
_serviceScopeFactory = serviceScopeFactory; _employeeHelper = employeeHelper;
_projectServices = projectServices;
_userHelper = userHelper; _userHelper = userHelper;
_s3Service = s3Service;
_logger = logger; _logger = logger;
_permission = permission; _permission = permission;
_mapper = mapper; _signalR = signalR;
tenantId = userHelper.GetTenantId(); _firebase = firebase;
}
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)
{ {
using var scope = _serviceScopeFactory.CreateScope(); Guid TenantId = GetTenantId();
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>();
@ -80,42 +85,30 @@ namespace MarcoBMS.Services.Controllers
} }
[HttpGet("log/employee/{employeeId}")] [HttpGet("log/employee/{employeeId}")]
public async Task<IActionResult> GetAttendanceLogByEmployeeId(Guid employeeId, [FromQuery] DateTime? dateFrom = null, [FromQuery] DateTime? dateTo = null) public async Task<IActionResult> GetAttendanceLogByEmployeeId(Guid employeeId, [FromQuery] string? dateFrom = null, [FromQuery] string? 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); Employee? employee = await _context.Employees.Include(e => e.JobRole).FirstOrDefaultAsync(e => e.Id == employeeId && e.TenantId == TenantId && e.IsActive);
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)
@ -128,17 +121,11 @@ 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);
} }
@ -158,12 +145,11 @@ namespace MarcoBMS.Services.Controllers
/// <returns></returns> /// <returns></returns>
[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)
{ {
using var scope = _serviceScopeFactory.CreateScope(); Guid tenantId = GetTenantId();
var _projectServices = scope.ServiceProvider.GetRequiredService<IProjectServices>(); var LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
var project = await _context.Projects.AsNoTracking().FirstOrDefaultAsync(p => p.Id == projectId && p.TenantId == tenantId); var project = await _context.Projects.AsNoTracking().FirstOrDefaultAsync(p => p.Id == projectId && p.TenantId == tenantId);
if (project == null) if (project == null)
@ -172,13 +158,13 @@ namespace MarcoBMS.Services.Controllers
return NotFound(ApiResponse<object>.ErrorResponse("Project not found.")); 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); var hasProjectPermission = await _permission.HasProjectPermission(LoggedInEmployee, projectId);
if (!hasProjectPermission) if (!hasProjectPermission)
{ {
_logger.LogWarning("Employee {EmployeeId} tries to access attendance of project {ProjectId}, but don't have access", loggedInEmployee.Id, projectId); _logger.LogWarning("Employee {EmployeeId} tries to access attendance of project {ProjectId}, but don't have access", LoggedInEmployee.Id, projectId);
return Unauthorized(ApiResponse<object>.ErrorResponse("Unauthorized access", "Unauthorized access", 404)); return Unauthorized(ApiResponse<object>.ErrorResponse("Unauthorized access", "Unauthorized access", 404));
} }
@ -211,13 +197,7 @@ namespace MarcoBMS.Services.Controllers
if (hasTeamAttendancePermission) if (hasTeamAttendancePermission)
{ {
List<Attendance> lstAttendance = await _context.Attendes 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();
.Include(a => a.RequestedBy)
.ThenInclude(e => e!.JobRole)
.Include(a => a.Approver)
.ThenInclude(e => e!.JobRole)
.Where(c => c.ProjectID == projectId && c.AttendanceDate.Date >= fromDate.Date && c.AttendanceDate.Date <= toDate.Date && c.TenantId == tenantId)
.ToListAsync();
List<ProjectAllocation> projectteam = await _projectServices.GetTeamByProject(tenantId, projectId, organizationId, true); List<ProjectAllocation> projectteam = await _projectServices.GetTeamByProject(tenantId, projectId, organizationId, true);
@ -229,13 +209,9 @@ 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
ApprovedAt = attendance.ApprovedAt,
Approver = _mapper.Map<BasicEmployeeVM>(attendance.Approver),
RequestedAt = attendance.RequestedAt,
RequestedBy = _mapper.Map<BasicEmployeeVM>(attendance.RequestedBy)
}; };
teamMember = projectteam.Find(x => x.EmployeeId == attendance.EmployeeId); teamMember = projectteam.Find(x => x.EmployeeId == attendance.EmployeeID);
if (teamMember != null) if (teamMember != null)
{ {
result1.EmployeeAvatar = null; result1.EmployeeAvatar = null;
@ -246,8 +222,6 @@ namespace MarcoBMS.Services.Controllers
result1.LastName = teamMember.Employee.LastName; result1.LastName = teamMember.Employee.LastName;
result1.JobRoleName = teamMember.Employee.JobRole != null ? teamMember.Employee.JobRole.Name : null; result1.JobRoleName = teamMember.Employee.JobRole != null ? teamMember.Employee.JobRole.Name : null;
result1.OrganizationName = teamMember.Employee.Organization?.Name; result1.OrganizationName = teamMember.Employee.Organization?.Name;
result1.ProjectId = projectId;
result1.ProjectName = teamMember.Project?.Name;
} }
else else
{ {
@ -265,23 +239,13 @@ namespace MarcoBMS.Services.Controllers
else if (hasSelfAttendancePermission) else if (hasSelfAttendancePermission)
{ {
List<Attendance> lstAttendances = await _context.Attendes List<Attendance> lstAttendances = await _context.Attendes
.Include(a => a.RequestedBy) .Where(c => c.ProjectID == projectId && c.EmployeeID == LoggedInEmployee.Id && c.AttendanceDate.Date >= fromDate.Date && c.AttendanceDate.Date <= toDate.Date && c.TenantId == tenantId)
.ThenInclude(e => e!.JobRole)
.Include(a => a.Approver)
.ThenInclude(e => e!.JobRole)
.Where(c => c.ProjectID == projectId && c.EmployeeId == loggedInEmployee.Id && c.AttendanceDate.Date >= fromDate.Date &&
c.AttendanceDate.Date <= toDate.Date && c.TenantId == tenantId)
.ToListAsync(); .ToListAsync();
var projectAllocationQuery = _context.ProjectAllocations var projectAllocationQuery = _context.ProjectAllocations
.Include(pa => pa.Project)
.Include(pa => pa.Employee) .Include(pa => pa.Employee)
.ThenInclude(e => e!.Organization) .ThenInclude(e => e!.Organization)
.Include(pa => pa.Employee) .Where(pa => pa.ProjectId == projectId && pa.EmployeeId == LoggedInEmployee.Id && pa.TenantId == tenantId && pa.IsActive);
.ThenInclude(e => e!.JobRole)
.Where(pa => pa.EmployeeId == loggedInEmployee.Id && pa.TenantId == tenantId && pa.IsActive &&
pa.ProjectId == projectId && pa.Project != null &&
pa.Employee != null && pa.Employee.Organization != null && pa.Employee.JobRole != null);
if (organizationId.HasValue) if (organizationId.HasValue)
{ {
@ -303,15 +267,9 @@ namespace MarcoBMS.Services.Controllers
LastName = projectAllocation.Employee?.LastName, LastName = projectAllocation.Employee?.LastName,
JobRoleName = projectAllocation.Employee?.JobRole?.Name, JobRoleName = projectAllocation.Employee?.JobRole?.Name,
OrganizationName = projectAllocation.Employee?.Organization?.Name, OrganizationName = projectAllocation.Employee?.Organization?.Name,
ProjectId = attendance.ProjectID,
ProjectName = projectAllocation.Project?.Name,
CheckInTime = attendance.InTime, CheckInTime = attendance.InTime,
CheckOutTime = attendance.OutTime, CheckOutTime = attendance.OutTime,
Activity = attendance.Activity, Activity = attendance.Activity
ApprovedAt = attendance.ApprovedAt,
Approver = _mapper.Map<BasicEmployeeVM>(attendance.Approver),
RequestedAt = attendance.RequestedAt,
RequestedBy = _mapper.Map<BasicEmployeeVM>(attendance.RequestedBy)
}; };
result.Add(result1); result.Add(result1);
} }
@ -333,6 +291,7 @@ namespace MarcoBMS.Services.Controllers
/// <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 ---
@ -394,9 +353,7 @@ namespace MarcoBMS.Services.Controllers
[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)
{ {
using var scope = _serviceScopeFactory.CreateScope(); Guid TenantId = GetTenantId();
var _projectServices = scope.ServiceProvider.GetRequiredService<IProjectServices>();
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); var hasProjectPermission = await _permission.HasProjectPermission(LoggedInEmployee, projectId);
@ -407,20 +364,15 @@ namespace MarcoBMS.Services.Controllers
return Unauthorized(ApiResponse<object>.ErrorResponse("Unauthorized access", "Unauthorized access", 404)); return Unauthorized(ApiResponse<object>.ErrorResponse("Unauthorized access", "Unauthorized access", 404));
} }
List<Attendance> lstAttendance = await _context.Attendes List<Attendance> lstAttendance = await _context.Attendes.Where(c => c.ProjectID == projectId && c.Activity == ATTENDANCE_MARK_TYPE.REQUEST_REGULARIZE && c.TenantId == TenantId).ToListAsync();
.Include(a => a.RequestedBy)
.ThenInclude(e => e!.JobRole)
.Include(a => a.Approver)
.ThenInclude(e => e!.JobRole)
.Where(c => c.ProjectID == projectId && c.Activity == ATTENDANCE_MARK_TYPE.REQUEST_REGULARIZE && c.TenantId == tenantId)
.ToListAsync();
List<ProjectAllocation> projectteam = await _projectServices.GetTeamByProject(tenantId, projectId, organizationId, true); List<ProjectAllocation> projectteam = await _projectServices.GetTeamByProject(TenantId, projectId, organizationId, true);
var idList = projectteam.Select(p => p.EmployeeId).ToList(); var idList = projectteam.Select(p => p.EmployeeId).ToList();
var jobRole = await _context.JobRoles.ToListAsync();
foreach (Attendance attende in lstAttendance) foreach (Attendance attende in lstAttendance)
{ {
var teamMember = projectteam.Find(m => m.EmployeeId == attende.EmployeeId); var teamMember = projectteam.Find(m => m.EmployeeId == attende.EmployeeID);
if (teamMember != null && teamMember.Employee != null && teamMember.Employee.JobRole != null) if (teamMember != null && teamMember.Employee != null && teamMember.Employee.JobRole != null)
{ {
var result1 = new EmployeeAttendanceVM() var result1 = new EmployeeAttendanceVM()
@ -430,17 +382,11 @@ namespace MarcoBMS.Services.Controllers
CheckOutTime = attende.OutTime, CheckOutTime = attende.OutTime,
Activity = attende.Activity, Activity = attende.Activity,
EmployeeAvatar = null, EmployeeAvatar = null,
EmployeeId = attende.EmployeeId, EmployeeId = attende.EmployeeID,
FirstName = teamMember.Employee.FirstName, FirstName = teamMember.Employee.FirstName,
LastName = teamMember.Employee.LastName, LastName = teamMember.Employee.LastName,
JobRoleName = teamMember.Employee.JobRole.Name, JobRoleName = teamMember.Employee.JobRole.Name,
OrganizationName = teamMember.Employee.Organization?.Name, OrganizationName = teamMember.Employee.Organization?.Name
ProjectId = projectId,
ProjectName = teamMember.Project?.Name,
ApprovedAt = attende.ApprovedAt,
Approver = _mapper.Map<BasicEmployeeVM>(attende.Approver),
RequestedAt = attende.RequestedAt,
RequestedBy = _mapper.Map<BasicEmployeeVM>(attende.RequestedBy)
}; };
result.Add(result1); result.Add(result1);
} }
@ -470,17 +416,13 @@ namespace MarcoBMS.Services.Controllers
return BadRequest(ApiResponse<object>.ErrorResponse("Invalid data", errors, 400)); return BadRequest(ApiResponse<object>.ErrorResponse("Invalid data", errors, 400));
} }
using var scope = _serviceScopeFactory.CreateScope(); Guid TenantId = GetTenantId();
var _signalR = scope.ServiceProvider.GetRequiredService<IHubContext<MarcoHub>>();
var _employeeHelper = scope.ServiceProvider.GetRequiredService<EmployeeHelper>();
var _firebase = scope.ServiceProvider.GetRequiredService<IFirebaseService>();
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)
{ {
@ -508,6 +450,10 @@ namespace MarcoBMS.Services.Controllers
{ {
attendance.IsApproved = true; attendance.IsApproved = true;
attendance.Activity = ATTENDANCE_MARK_TYPE.REGULARIZE; attendance.Activity = ATTENDANCE_MARK_TYPE.REGULARIZE;
//string timeString = "10:30 PM"; // Format: "hh:mm tt"
attendance.OutTime = finalDateTime; attendance.OutTime = finalDateTime;
} }
else if (recordAttendanceDot.Action == ATTENDANCE_MARK_TYPE.REQUEST_REGULARIZE) else if (recordAttendanceDot.Action == ATTENDANCE_MARK_TYPE.REQUEST_REGULARIZE)
@ -518,8 +464,6 @@ 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.RequestedById = currentEmployee.Id;
attendance.RequestedAt = DateTime.UtcNow;
} }
else else
{ {
@ -532,16 +476,13 @@ 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.ApprovedBy = 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;
@ -552,11 +493,11 @@ 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;
attendance.EmployeeId = recordAttendanceDot.EmployeeID; attendance.EmployeeID = recordAttendanceDot.EmployeeID;
attendance.ProjectID = recordAttendanceDot.ProjectID; attendance.ProjectID = recordAttendanceDot.ProjectID;
attendance.Date = DateTime.UtcNow; attendance.Date = DateTime.UtcNow;
@ -584,7 +525,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
}; };
@ -631,7 +572,7 @@ namespace MarcoBMS.Services.Controllers
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);
}); });
@ -667,12 +608,7 @@ namespace MarcoBMS.Services.Controllers
return BadRequest(ApiResponse<object>.ErrorResponse("Invalid data", errors, 400)); return BadRequest(ApiResponse<object>.ErrorResponse("Invalid data", errors, 400));
} }
using var scope = _serviceScopeFactory.CreateScope(); Guid tenantId = GetTenantId();
var _s3Service = scope.ServiceProvider.GetRequiredService<S3UploadService>();
var _signalR = scope.ServiceProvider.GetRequiredService<IHubContext<MarcoHub>>();
var _employeeHelper = scope.ServiceProvider.GetRequiredService<EmployeeHelper>();
var _firebase = scope.ServiceProvider.GetRequiredService<IFirebaseService>();
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
var batchId = Guid.NewGuid(); var batchId = Guid.NewGuid();
@ -704,7 +640,7 @@ namespace MarcoBMS.Services.Controllers
TenantId = tenantId, TenantId = tenantId,
AttendanceDate = recordAttendanceDot.Date, AttendanceDate = recordAttendanceDot.Date,
Comment = recordAttendanceDot.Comment, Comment = recordAttendanceDot.Comment,
EmployeeId = recordAttendanceDot.EmployeeID, EmployeeID = recordAttendanceDot.EmployeeID,
ProjectID = recordAttendanceDot.ProjectID, ProjectID = recordAttendanceDot.ProjectID,
Date = DateTime.UtcNow, Date = DateTime.UtcNow,
InTime = finalDateTime, InTime = finalDateTime,
@ -736,8 +672,6 @@ 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.RequestedById = loggedInEmployee.Id;
attendance.RequestedAt = DateTime.UtcNow;
} }
else else
{ {
@ -748,14 +682,10 @@ 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.ApprovedById = loggedInEmployee.Id;
attendance.ApprovedAt = DateTime.UtcNow;
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.ApprovedById = loggedInEmployee.Id;
attendance.ApprovedAt = DateTime.UtcNow;
break; break;
} }
@ -851,7 +781,7 @@ namespace MarcoBMS.Services.Controllers
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);
}); });
@ -888,7 +818,6 @@ namespace MarcoBMS.Services.Controllers
// 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.ProjectAllocations
.Include(pa => pa.Project)
.Include(pa => pa.Employee) .Include(pa => pa.Employee)
.ThenInclude(e => e!.Organization) .ThenInclude(e => e!.Organization)
.Include(pa => pa.Employee) .Include(pa => pa.Employee)
@ -905,12 +834,7 @@ namespace MarcoBMS.Services.Controllers
query = query.Where(pa => pa.Employee != null && pa.Employee.OrganizationId == organizationId); query = query.Where(pa => pa.Employee != null && pa.Employee.OrganizationId == organizationId);
} }
List<Attendance> lstAttendance = await _context.Attendes List<Attendance> lstAttendance = await _context.Attendes.Where(c => c.ProjectID == projectId && c.AttendanceDate.Date == forDate && c.TenantId == tenantId).ToListAsync();
.Include(a => a.RequestedBy)
.ThenInclude(e => e!.JobRole)
.Include(a => a.Approver)
.ThenInclude(e => e!.JobRole)
.Where(c => c.ProjectID == projectId && c.AttendanceDate.Date == forDate && c.TenantId == tenantId).ToListAsync();
var teamAttendance = await query var teamAttendance = await query
.AsNoTracking() .AsNoTracking()
@ -927,21 +851,18 @@ namespace MarcoBMS.Services.Controllers
LastName = teamMember.Employee?.LastName, LastName = teamMember.Employee?.LastName,
OrganizationName = teamMember.Employee?.Organization?.Name, OrganizationName = teamMember.Employee?.Organization?.Name,
JobRoleName = teamMember.Employee?.JobRole?.Name, JobRoleName = teamMember.Employee?.JobRole?.Name,
ProjectId = projectId,
ProjectName = teamMember.Project?.Name
}; };
var attendance = lstAttendance.Find(x => x.EmployeeId == teamMember.EmployeeId) ?? new Attendance(); //var member = emp.Where(e => e.Id == teamMember.EmployeeId);
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.CheckInTime = attendance.InTime; result1.CheckInTime = attendance.InTime;
result1.CheckOutTime = attendance.OutTime; result1.CheckOutTime = attendance.OutTime;
result1.Activity = attendance.Activity; result1.Activity = attendance.Activity;
result1.ApprovedAt = attendance.ApprovedAt;
result1.Approver = _mapper.Map<BasicEmployeeVM>(attendance.Approver);
result1.RequestedAt = attendance.RequestedAt;
result1.RequestedBy = _mapper.Map<BasicEmployeeVM>(attendance.RequestedBy);
} }
return result1; return result1;
}) })
@ -960,21 +881,12 @@ namespace MarcoBMS.Services.Controllers
// 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 Attendance lstAttendance = await _context.Attendes
.Include(a => a.RequestedBy) .FirstOrDefaultAsync(c => c.ProjectID == projectId && c.EmployeeID == employeeId && c.AttendanceDate.Date == forDate && c.TenantId == tenantId) ?? new Attendance();
.ThenInclude(e => e!.JobRole)
.Include(a => a.Approver)
.ThenInclude(e => e!.JobRole)
.FirstOrDefaultAsync(c => c.ProjectID == projectId && c.EmployeeId == employeeId && c.AttendanceDate.Date == forDate && c.TenantId == tenantId) ?? new Attendance();
var projectAllocationQuery = _context.ProjectAllocations var projectAllocationQuery = _context.ProjectAllocations
.Include(pa => pa.Project)
.Include(pa => pa.Employee) .Include(pa => pa.Employee)
.ThenInclude(e => e!.Organization) .ThenInclude(e => e!.Organization)
.Include(pa => pa.Employee) .Where(pa => pa.ProjectId == projectId && pa.EmployeeId == employeeId && pa.TenantId == tenantId && pa.IsActive);
.ThenInclude(e => e!.JobRole)
.Where(pa => pa.EmployeeId == employeeId && pa.TenantId == tenantId && pa.IsActive &&
pa.ProjectId == projectId && pa.Project != null &&
pa.Employee != null && pa.Employee.Organization != null && pa.Employee.JobRole != null);
if (organizationId.HasValue) if (organizationId.HasValue)
{ {
@ -994,15 +906,9 @@ namespace MarcoBMS.Services.Controllers
OrganizationName = projectAllocation.Employee?.Organization?.Name, OrganizationName = projectAllocation.Employee?.Organization?.Name,
LastName = projectAllocation.Employee?.LastName, LastName = projectAllocation.Employee?.LastName,
JobRoleName = projectAllocation.Employee?.JobRole?.Name, JobRoleName = projectAllocation.Employee?.JobRole?.Name,
ProjectId = projectId,
ProjectName = projectAllocation.Project?.Name,
CheckInTime = lstAttendance.InTime, CheckInTime = lstAttendance.InTime,
CheckOutTime = lstAttendance.OutTime, CheckOutTime = lstAttendance.OutTime,
Activity = lstAttendance.Activity, Activity = lstAttendance.Activity
ApprovedAt = lstAttendance.ApprovedAt,
Approver = _mapper.Map<BasicEmployeeVM>(lstAttendance.Approver),
RequestedAt = lstAttendance.RequestedAt,
RequestedBy = _mapper.Map<BasicEmployeeVM>(lstAttendance.RequestedBy)
}; };
result.Add(result1); result.Add(result1);
} }

View File

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

View File

@ -235,7 +235,7 @@ namespace Marco.Pms.Services.Controllers
int inTodays = await _context.Attendes int inTodays = await _context.Attendes
.Where(a => a.InTime >= today && a.InTime < tomorrow && .Where(a => a.InTime >= today && a.InTime < tomorrow &&
finalProjectIds.Contains(a.ProjectID)) finalProjectIds.Contains(a.ProjectID))
.Select(a => a.EmployeeId) .Select(a => a.EmployeeID)
.Distinct() .Distinct()
.CountAsync(); .CountAsync();
@ -354,7 +354,7 @@ namespace Marco.Pms.Services.Controllers
Guid tenantId = _userHelper.GetTenantId(); 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();
if (attendance.Any()) if (attendance.Any())
{ {
var pendingRegularization = attendance.Where(a => a.Activity == ATTENDANCE_MARK_TYPE.REQUEST_REGULARIZE).ToList().Count; var pendingRegularization = attendance.Where(a => a.Activity == ATTENDANCE_MARK_TYPE.REQUEST_REGULARIZE).ToList().Count;
@ -395,12 +395,12 @@ namespace Marco.Pms.Services.Controllers
var employeeIds = projectAllocation.Select(p => p.EmployeeId).Distinct().ToList(); var employeeIds = projectAllocation.Select(p => p.EmployeeId).Distinct().ToList();
List<Employee>? employees = await _context.Employees.Where(e => employeeIds.Contains(e.Id)).ToListAsync(); List<Employee>? employees = await _context.Employees.Where(e => employeeIds.Contains(e.Id)).ToListAsync();
var attendances = await _context.Attendes.Where(a => employeeIds.Contains(a.EmployeeId) && a.ProjectID == projectId && a.InTime.HasValue && a.InTime.Value.Date == currentDate.Date).ToListAsync(); var attendances = await _context.Attendes.Where(a => employeeIds.Contains(a.EmployeeID) && a.ProjectID == projectId && a.InTime.HasValue && a.InTime.Value.Date == currentDate.Date).ToListAsync();
List<EmployeeAttendanceVM> employeeAttendanceVMs = new List<EmployeeAttendanceVM>(); List<EmployeeAttendanceVM> employeeAttendanceVMs = new List<EmployeeAttendanceVM>();
foreach (var attendance in attendances) foreach (var attendance in attendances)
{ {
Employee? employee = employees.FirstOrDefault(e => e.Id == attendance.EmployeeId); Employee? employee = employees.FirstOrDefault(e => e.Id == attendance.EmployeeID);
if (employee != null) if (employee != null)
{ {
EmployeeAttendanceVM employeeAttendanceVM = new EmployeeAttendanceVM EmployeeAttendanceVM employeeAttendanceVM = new EmployeeAttendanceVM
@ -579,7 +579,7 @@ namespace Marco.Pms.Services.Controllers
.ToList(); .ToList();
int presentCount = attendances int presentCount = attendances
.Count(a => employeeIds.Contains(a.EmployeeId) && a.InTime!.Value.Date == date); .Count(a => employeeIds.Contains(a.EmployeeID) && a.InTime!.Value.Date == date);
overviewList.Add(new AttendanceOverviewVM overviewList.Add(new AttendanceOverviewVM
{ {

View File

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

View File

@ -123,7 +123,7 @@ namespace MarcoBMS.Services.Controllers
} }
[HttpGet("list/organizations/{projectId}")] [HttpGet("list/organizations/{projectId}")]
public async Task<IActionResult> GetEmployeesByProjectAsync(Guid projectId, [FromQuery] string searchString, [FromQuery] Guid? organizationId) public async Task<IActionResult> GetEmployeesByProjectAsync(Guid projectId, [FromQuery] string searchString)
{ {
try try
{ {
@ -179,17 +179,10 @@ namespace MarcoBMS.Services.Controllers
} }
// Fetch employees allocated to the project matching the search criteria // Fetch employees allocated to the project matching the search criteria
var employeesQuery = _context.Employees var employees = await _context.Employees
.AsNoTracking() // Improves performance by disabling change tracking for read-only query .AsNoTracking() // Improves performance by disabling change tracking for read-only query
.Include(e => e.JobRole) .Include(e => e.JobRole)
.Where(e => (e.FirstName + " " + e.LastName).Contains(searchString) && organizationIds.Contains(e.OrganizationId)); .Where(e => (e.FirstName + " " + e.LastName).Contains(searchString) && organizationIds.Contains(e.OrganizationId))
if (organizationId.HasValue)
{
employeesQuery = employeesQuery.Where(e => e.OrganizationId == organizationId);
}
var employees = await employeesQuery
.ToListAsync(); .ToListAsync();
var result = employees.Select(e => _mapper.Map<EmployeeVM>(e)).Distinct().ToList(); var result = employees.Select(e => _mapper.Map<EmployeeVM>(e)).Distinct().ToList();
@ -329,10 +322,10 @@ namespace MarcoBMS.Services.Controllers
[HttpGet("basic")] [HttpGet("basic")]
public async Task<IActionResult> GetEmployeesByProjectBasic(Guid? projectId, [FromQuery] string? searchString, [FromQuery] bool allEmployee) public async Task<IActionResult> GetEmployeesByProjectBasic(Guid? projectId, [FromQuery] string? searchString)
{ {
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.TenantId == tenantId);
if (projectId != null && projectId != Guid.Empty) if (projectId != null && projectId != Guid.Empty)
{ {
var hasProjectPermission = await _permission.HasProjectPermission(loggedInEmployee, projectId.Value); var hasProjectPermission = await _permission.HasProjectPermission(loggedInEmployee, projectId.Value);
@ -344,26 +337,13 @@ namespace MarcoBMS.Services.Controllers
var employeeIds = await _context.ProjectAllocations.Where(pa => pa.ProjectId == projectId && pa.IsActive && pa.TenantId == tenantId).Select(p => p.EmployeeId).ToListAsync(); var employeeIds = await _context.ProjectAllocations.Where(pa => pa.ProjectId == projectId && pa.IsActive && pa.TenantId == tenantId).Select(p => p.EmployeeId).ToListAsync();
employeeQuery = employeeQuery.Where(e => employeeIds.Contains(e.Id)); employeeQuery = employeeQuery.Where(e => employeeIds.Contains(e.Id));
} }
else
{
employeeQuery = employeeQuery.Where(e => e.OrganizationId == organizationId);
}
if (!string.IsNullOrWhiteSpace(searchString)) if (!string.IsNullOrWhiteSpace(searchString))
{ {
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));
} }
var query = employeeQuery.OrderBy(e => e.FirstName); var response = await employeeQuery.Take(10).Select(e => _mapper.Map<BasicEmployeeVM>(e)).ToListAsync();
if (!allEmployee)
{
query = (IOrderedQueryable<Employee>)query.Take(10);
}
var response = await query
.Select(e => _mapper.Map<BasicEmployeeVM>(e))
.ToListAsync();
return Ok(ApiResponse<object>.SuccessResponse(response, $"{response.Count} records of employees fetched successfully", 200)); return Ok(ApiResponse<object>.SuccessResponse(response, $"{response.Count} records of employees fetched successfully", 200));
} }
@ -634,10 +614,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); .FirstOrDefaultAsync(e => e.Id == model.Id && e.OrganizationId == model.OrganizationId);
if (existingEmployee == null) if (existingEmployee == null)
{ {
_logger.LogInfo("Employee not found for update. Id={EmployeeId}", model.Id); _logger.LogInfo("Employee not found for update. Id={EmployeeId}, Org={OrgId}", model.Id, model.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));
} }
} }
@ -689,10 +669,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); .AnyAsync(e => e.Email == model.Email && e.OrganizationId == model.OrganizationId);
if (emailExists) if (emailExists)
{ {
_logger.LogInfo("Employee email already exists. Email={Email}", model.Email); _logger.LogInfo("Employee email already exists in org. Email={Email}, Org={OrgId}", model.Email, model.OrganizationId);
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));
@ -717,6 +697,14 @@ namespace MarcoBMS.Services.Controllers
createdIdentityUser.Id, createdIdentityUser.Email); createdIdentityUser.Id, createdIdentityUser.Email);
} }
// Prepare reset link sender helper
async Task SendResetIfApplicableAsync(ApplicationUser u, string firstName)
{
var token = await _userManager.GeneratePasswordResetTokenAsync(u);
var resetLink = $"{_configuration["AppSettings:WebFrontendUrl"]}/reset-password?token={WebUtility.UrlEncode(token)}";
await _emailSender.SendResetPasswordEmailOnRegister(u.Email ?? "", firstName, resetLink);
_logger.LogInfo("Reset password email queued. Email={Email}", u.Email ?? "");
}
Guid employeeId; Guid employeeId;
EmployeeVM employeeVM; EmployeeVM employeeVM;
@ -920,15 +908,6 @@ 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
.AnyAsync(e => e.Email == model.Email && e.OrganizationId == model.OrganizationId);
if (emailExists && !string.IsNullOrWhiteSpace(model.Email))
{
_logger.LogInfo("Employee email already exists in org. Email={Email}, Org={OrgId}", model.Email, model.OrganizationId);
return StatusCode(409, ApiResponse<object>.ErrorResponse("Employee with email already exists", "Employee with this email already exists", 409));
}
// Create path: map only allowed fields // Create path: map only allowed fields
var employee = new Employee var employee = new Employee
{ {
@ -936,42 +915,15 @@ namespace MarcoBMS.Services.Controllers
TenantId = tenantId, TenantId = tenantId,
FirstName = model.FirstName.Trim(), FirstName = model.FirstName.Trim(),
LastName = model.LastName?.Trim(), LastName = model.LastName?.Trim(),
Email = model.Email,
Gender = model.Gender, Gender = model.Gender,
PhoneNumber = model.PhoneNumber, PhoneNumber = model.PhoneNumber,
JoiningDate = model.JoiningDate, JoiningDate = model.JoiningDate,
JobRoleId = model.JobRoleId, JobRoleId = model.JobRoleId,
Photo = imageBytes, Photo = imageBytes,
OrganizationId = model.OrganizationId, OrganizationId = model.OrganizationId
HasApplicationAccess = model.HasApplicationAccess,
}; };
if (!string.IsNullOrWhiteSpace(model.Email) && model.HasApplicationAccess) await _context.Employees.AddAsync(employee);
{
var existingUser = await _userManager.FindByEmailAsync(model.Email);
if (existingUser == null)
{
existingUser = new ApplicationUser
{
UserName = model.Email,
Email = model.Email,
EmailConfirmed = true
};
var createResult = await _userManager.CreateAsync(existingUser, "User@123");
if (!createResult.Succeeded)
{
_logger.LogWarning("Failed to create identity user for {Email}. Errors={Errors}",
existingUser.Email,
string.Join(", ", createResult.Errors.Select(e => $"{e.Code}:{e.Description}")));
return BadRequest(ApiResponse<object>.ErrorResponse("Failed to create user", createResult.Errors, 400));
}
await SendResetIfApplicableAsync(existingUser, employee.FirstName ?? "User");
employee.ApplicationUserId = existingUser.Id;
}
}
_context.Employees.Add(employee);
await _context.SaveChangesAsync(); await _context.SaveChangesAsync();
var employeeVM = _mapper.Map<EmployeeVM>(employee); var employeeVM = _mapper.Map<EmployeeVM>(employee);
@ -995,7 +947,7 @@ namespace MarcoBMS.Services.Controllers
// Update path: fetch scoped to tenant // Update path: fetch scoped to tenant
var employeeId = model.Id.Value; var employeeId = model.Id.Value;
var existingEmployee = await _context.Employees var existingEmployee = await _context.Employees
.FirstOrDefaultAsync(e => e.Id == employeeId); // tenant-safe lookup .FirstOrDefaultAsync(e => e.Id == employeeId && e.TenantId == tenantId); // tenant-safe lookup
if (existingEmployee is null) if (existingEmployee is null)
{ {
@ -1011,45 +963,6 @@ namespace MarcoBMS.Services.Controllers
existingEmployee.JoiningDate = model.JoiningDate; existingEmployee.JoiningDate = model.JoiningDate;
existingEmployee.JobRoleId = model.JobRoleId; existingEmployee.JobRoleId = model.JobRoleId;
existingEmployee.OrganizationId = model.OrganizationId; existingEmployee.OrganizationId = model.OrganizationId;
existingEmployee.HasApplicationAccess = model.HasApplicationAccess;
if (string.IsNullOrWhiteSpace(existingEmployee.Email) && !string.IsNullOrWhiteSpace(model.Email))
{
var emailExists = await _context.Employees
.AnyAsync(e => e.Email == model.Email);
if (emailExists)
{
_logger.LogInfo("Employee email already exists in org. Email={Email}, Org={OrgId}", model.Email, model.OrganizationId);
return StatusCode(409, ApiResponse<object>.ErrorResponse("Employee with email already exists", "Employee with this email already exists", 409));
}
existingEmployee.Email = model.Email;
}
if (model.HasApplicationAccess && !string.IsNullOrWhiteSpace(model.Email) && string.IsNullOrWhiteSpace(existingEmployee.ApplicationUserId))
{
var existingUser = await _userManager.FindByEmailAsync(model.Email);
if (existingUser == null)
{
existingUser = new ApplicationUser
{
UserName = model.Email,
Email = model.Email,
EmailConfirmed = true
};
var createResult = await _userManager.CreateAsync(existingUser, "User@123");
if (!createResult.Succeeded)
{
_logger.LogWarning("Failed to create identity user for {Email}. Errors={Errors}",
existingUser.Email,
string.Join(", ", createResult.Errors.Select(e => $"{e.Code}:{e.Description}")));
return BadRequest(ApiResponse<object>.ErrorResponse("Failed to create user", createResult.Errors, 400));
}
await SendResetIfApplicableAsync(existingUser, existingEmployee.FirstName ?? "User");
existingEmployee.ApplicationUserId = existingUser.Id;
}
}
if (imageBytes != null) if (imageBytes != null)
{ {
@ -1111,7 +1024,7 @@ namespace MarcoBMS.Services.Controllers
} }
} }
} }
var attendance = await _context.Attendes.Where(a => a.EmployeeId == employee.Id && (a.OutTime == null || a.Activity == ATTENDANCE_MARK_TYPE.REQUEST_REGULARIZE)).ToListAsync(); var attendance = await _context.Attendes.Where(a => a.EmployeeID == employee.Id && (a.OutTime == null || a.Activity == ATTENDANCE_MARK_TYPE.REQUEST_REGULARIZE)).ToListAsync();
if (attendance.Count != 0) if (attendance.Count != 0)
{ {
_logger.LogWarning("Employee with ID {EmployeeId} have any pending check-out or regularization requests", employee.Id); _logger.LogWarning("Employee with ID {EmployeeId} have any pending check-out or regularization requests", employee.Id);
@ -1253,14 +1166,5 @@ namespace MarcoBMS.Services.Controllers
return info; return info;
} }
// Prepare reset link sender helper
private async Task SendResetIfApplicableAsync(ApplicationUser u, string firstName)
{
var token = await _userManager.GeneratePasswordResetTokenAsync(u);
var resetLink = $"{_configuration["AppSettings:WebFrontendUrl"]}/reset-password?token={WebUtility.UrlEncode(token)}";
await _emailSender.SendResetPasswordEmailOnRegister(u.Email ?? "", firstName, resetLink);
_logger.LogInfo("Reset password email queued. Email={Email}", u.Email ?? "");
}
} }
} }

View File

@ -106,7 +106,7 @@ namespace Marco.Pms.Services.Controllers
t.WorkItem.WorkArea != null && t.WorkItem.WorkArea != null &&
t.WorkItem.WorkArea.Floor != null && t.WorkItem.WorkArea.Floor != null &&
t.WorkItem.WorkArea.Floor.Building != null && t.WorkItem.WorkArea.Floor.Building != null &&
t.WorkItem.WorkArea.Floor.Building.ProjectId == projectId && t.WorkItem.WorkArea.Floor.Building.ProjectId != projectId &&
t.TenantId == tenantId); t.TenantId == tenantId);
// Step 4: Extract filter values // Step 4: Extract filter values

View File

@ -9,7 +9,6 @@ using Marco.Pms.Model.ViewModels.Activities;
using Marco.Pms.Model.ViewModels.Master; using Marco.Pms.Model.ViewModels.Master;
using Marco.Pms.Model.ViewModels.Organization; using Marco.Pms.Model.ViewModels.Organization;
using Marco.Pms.Model.ViewModels.Projects; using Marco.Pms.Model.ViewModels.Projects;
using Marco.Pms.Services.Helpers;
using Marco.Pms.Services.Service; using Marco.Pms.Services.Service;
using MarcoBMS.Services.Helpers; using MarcoBMS.Services.Helpers;
using MarcoBMS.Services.Service; using MarcoBMS.Services.Service;
@ -31,7 +30,6 @@ namespace Marco.Pms.Services.Controllers
private readonly UserHelper _userHelper; private readonly UserHelper _userHelper;
private readonly Guid tenantId; private readonly Guid tenantId;
private readonly IMapper _mapper; private readonly IMapper _mapper;
private readonly Guid loggedOrganizationId;
private readonly ILoggingService _logger; private readonly ILoggingService _logger;
private static readonly Guid PMCProvider = Guid.Parse("b1877a3b-8832-47b1-bbe3-dc7e98672f49"); private static readonly Guid PMCProvider = Guid.Parse("b1877a3b-8832-47b1-bbe3-dc7e98672f49");
@ -49,7 +47,6 @@ namespace Marco.Pms.Services.Controllers
_userHelper = userHelper ?? throw new ArgumentNullException(nameof(userHelper)); _userHelper = userHelper ?? throw new ArgumentNullException(nameof(userHelper));
_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));
loggedOrganizationId = _userHelper.GetCurrentOrganizationId();
tenantId = userHelper.GetTenantId(); tenantId = userHelper.GetTenantId();
} }
#region =================================================================== Get Functions =================================================================== #region =================================================================== Get Functions ===================================================================
@ -58,235 +55,66 @@ namespace Marco.Pms.Services.Controllers
public async Task<IActionResult> GetOrganizarionListAsync([FromQuery] string? searchString, [FromQuery] double? sprid, [FromQuery] bool active = true, public async Task<IActionResult> GetOrganizarionListAsync([FromQuery] string? searchString, [FromQuery] double? sprid, [FromQuery] bool active = true,
[FromQuery] int pageNumber = 1, [FromQuery] int pageSize = 20) [FromQuery] int pageNumber = 1, [FromQuery] int pageSize = 20)
{ {
_logger.LogDebug("Fetching organization list. SearchString: {SearchString}, SPRID: {SPRID}, Active: {Active}, Page: {PageNumber}, Size: {PageSize}",
searchString ?? "", sprid ?? 0, active, pageNumber, pageSize);
await using var _context = await _dbContextFactory.CreateDbContextAsync(); await using var _context = await _dbContextFactory.CreateDbContextAsync();
using var scope = _serviceScope.CreateScope();
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
// Base query filtering by active status var organizationQuery = _context.Organizations
IQueryable<Organization> organizationQuery = _context.Organizations.Where(o => o.IsActive == active); .Where(o => o.IsActive == active);
if (sprid.HasValue) if (sprid.HasValue)
{ {
// Filter by SPRID if provided
organizationQuery = organizationQuery.Where(o => o.SPRID == sprid.Value); organizationQuery = organizationQuery.Where(o => o.SPRID == sprid.Value);
_logger.LogDebug("Filtering organizations by SPRID: {SPRID}", sprid.Value);
} }
else else
{ {
// Get organization IDs mapped to current tenant that are active var organizationIds = await _context.TenantOrgMappings.Where(to => to.TenantId == tenantId && to.IsActive).Select(to => to.OrganizationId).ToListAsync();
var organizationIdsTask = Task.Run(async () =>
{
await using var context = await _dbContextFactory.CreateDbContextAsync();
return await context.TenantOrgMappings
.Where(to => to.TenantId == tenantId && to.IsActive)
.Select(to => to.OrganizationId)
.ToListAsync();
});
var tenantTask = Task.Run(async () =>
{
await using var context = await _dbContextFactory.CreateDbContextAsync();
return await context.Tenants
.FirstOrDefaultAsync(t => t.Id == tenantId && t.IsActive);
});
await Task.WhenAll(organizationIdsTask, tenantTask);
var organizationIds = organizationIdsTask.Result;
var tenant = tenantTask.Result;
if (tenant == null)
{
_logger.LogWarning("Tenant {TenantId} is not found", tenantId);
return NotFound(ApiResponse<object>.ErrorResponse("Tenant not found", "Tenant not found in database", 404));
}
organizationIds.Add(tenant.OrganizationId);
organizationIds = organizationIds.Distinct().ToList();
organizationQuery = organizationQuery.Where(o => organizationIds.Contains(o.Id)); organizationQuery = organizationQuery.Where(o => organizationIds.Contains(o.Id));
_logger.LogDebug("Filtering organizations by tenant's mapped IDs count: {Count}", organizationIds.Count);
if (!string.IsNullOrWhiteSpace(searchString)) if (!string.IsNullOrWhiteSpace(searchString))
{ {
// Filter by search string on organization name -- extend here if needed organizationQuery = organizationQuery.Where(sp =>
organizationQuery = organizationQuery.Where(o => o.Name.Contains(searchString)); sp.Name.Contains(searchString)
_logger.LogDebug("Filtering organizations by search string: {SearchString}", searchString); //|| sp.ContactPerson.Contains(searchString)
//|| sp.Address.Contains(searchString)
//|| sp.Email.Contains(searchString)
//|| sp.ContactNumber.Contains(searchString)
);
} }
} }
// Get total count for pagination
var totalCount = await organizationQuery.CountAsync(); var totalCount = await organizationQuery.CountAsync();
var totalPages = (int)Math.Ceiling((double)totalCount / pageSize); var totalPages = (int)Math.Ceiling((double)totalCount / pageSize);
// Fetch page of organizations sorted by name
var organizations = await organizationQuery var organizations = await organizationQuery
.OrderBy(o => o.Name) .OrderBy(e => e.Name)
.Skip((pageNumber - 1) * pageSize) .Skip((pageNumber - 1) * pageSize)
.Take(pageSize) .Take(pageSize)
.ToListAsync(); .ToListAsync();
if (!organizations.Any() && !sprid.HasValue) var createdByIds = organizations.Where(o => o.CreatedById != null).Select(o => o.CreatedById).ToList();
{ var updatedByIds = organizations.Where(o => o.UpdatedById != null).Select(o => o.UpdatedById).ToList();
organizations = await _context.Tenants.AsNoTracking()
.Include(t => t.Organization)
.Where(t => t.Id == tenantId && t.OrganizationId == loggedInEmployee.OrganizationId && t.Organization != null && t.IsActive)
.Select(t => t.Organization!).ToListAsync();
}
// Collect creator and updater employee IDs var employees = await _context.Employees.Where(e => createdByIds.Contains(e.Id) || updatedByIds.Contains(e.Id)).ToListAsync();
var createdByIds = organizations.Where(o => o.CreatedById != null).Select(o => o.CreatedById!.Value).Distinct().ToList();
var updatedByIds = organizations.Where(o => o.UpdatedById != null).Select(o => o.UpdatedById!.Value).Distinct().ToList();
// Fetch corresponding employee details in one query
var employeeIds = createdByIds.Union(updatedByIds).ToList();
var employees = await _context.Employees.Where(e => employeeIds.Contains(e.Id)).ToListAsync();
// Map data to view models including created and updated by employees
var vm = organizations.Select(o => var vm = organizations.Select(o =>
{ {
var orgVm = _mapper.Map<OrganizationVM>(o); var result = _mapper.Map<OrganizationVM>(o);
orgVm.CreatedBy = employees.Where(e => e.Id == o.CreatedById).Select(e => _mapper.Map<BasicEmployeeVM>(e)).FirstOrDefault(); result.CreatedBy = employees.Where(e => e.Id == o.CreatedById).Select(e => _mapper.Map<BasicEmployeeVM>(e)).FirstOrDefault();
orgVm.UpdatedBy = employees.Where(e => e.Id == o.UpdatedById).Select(e => _mapper.Map<BasicEmployeeVM>(e)).FirstOrDefault(); result.UpdatedBy = employees.Where(e => e.Id == o.UpdatedById).Select(e => _mapper.Map<BasicEmployeeVM>(e)).FirstOrDefault();
return orgVm; return result;
}).ToList(); });
var response = new var response = new
{ {
CurrentPage = pageNumber, CurrentPage = pageNumber,
TotalPages = totalPages, TotalPages = totalPages,
TotalEntities = totalCount, TotalEntites = totalCount,
Data = vm, Data = vm,
}; };
_logger.LogInfo("Fetched {Count} organizations (Page {PageNumber} of {TotalPages})", vm.Count, pageNumber, totalPages); return Ok(ApiResponse<object>.SuccessResponse(response, "Successfully fetched the Service Provider list", 200));
return Ok(ApiResponse<object>.SuccessResponse(response, "Successfully fetched the organization list", 200));
}
[HttpGet("details/{id}")]
public async Task<IActionResult> GetOrganizationDetailsAsync(Guid id)
{
_logger.LogDebug("Started fetching details for OrganizationId: {OrganizationId}", id);
await using var _context = await _dbContextFactory.CreateDbContextAsync();
try
{
// Get the logged-in employee (for filter/permission checks)
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
// Fetch the organization entity by Id
var organization = await _context.Organizations.FirstOrDefaultAsync(o => o.Id == id);
if (organization == null)
{
_logger.LogWarning("Organization not found for OrganizationId: {OrganizationId}", id);
return NotFound(ApiResponse<object>.ErrorResponse("Organization not found", "Organization not found", 404));
}
// Fetch CreatedBy employee (with JobRole)
var createdByTask = Task.Run(async () =>
{
await using var context = await _dbContextFactory.CreateDbContextAsync();
return await context.Employees
.Include(e => e.JobRole)
.FirstOrDefaultAsync(e => e.Id == organization.CreatedById);
});
// Fetch UpdatedBy employee (with JobRole)
var updatedByTask = Task.Run(async () =>
{
if (organization.UpdatedById.HasValue)
{
await using var context = await _dbContextFactory.CreateDbContextAsync();
return await context.Employees
.Include(e => e.JobRole)
.FirstOrDefaultAsync(e => e.Id == organization.UpdatedById);
}
return null;
});
// Fetch the organization's service mappings and corresponding services
var orgServiceMappingTask = Task.Run(async () =>
{
await using var context = await _dbContextFactory.CreateDbContextAsync();
return await context.OrgServiceMappings
.Include(os => os.Service)
.Where(os => os.OrganizationId == id).ToListAsync();
});
// Fetch active employees in the organization
var employeeListTask = Task.Run(async () =>
{
await using var context = await _dbContextFactory.CreateDbContextAsync();
return await context.Employees
.Where(e => e.OrganizationId == id && e.IsActive).ToListAsync();
});
await Task.WhenAll(createdByTask, updatedByTask, orgServiceMappingTask, employeeListTask);
var createdByEmployee = createdByTask.Result;
var updatedByEmployee = updatedByTask.Result;
var orgServiceMappings = orgServiceMappingTask.Result;
var employeeList = employeeListTask.Result;
var activeEmployeeCount = employeeList.Count;
var activeApplicationUserCount = employeeList.Count(e => e.HasApplicationAccess);
// Start query for projects mapped to this organization (including project and service info)
var baseProjectOrgMappingQuery = _context.ProjectOrgMappings
.Include(po => po.ProjectService)
.ThenInclude(ps => ps!.Service)
.Include(po => po.ProjectService)
.ThenInclude(ps => ps!.Project)
.Where(po => po.OrganizationId == id && po.ProjectService != null);
// If logged-in employee is not from the requested organization, restrict projects to those also mapped to their org
List<ProjectOrgMapping> projectOrgMappings;
if (loggedInEmployee.OrganizationId != id)
{
var projectIds = await _context.ProjectOrgMappings
.Include(po => po.ProjectService)
.Where(po => po.OrganizationId == loggedInEmployee.OrganizationId && po.ProjectService != null)
.Select(po => po.ProjectService!.ProjectId)
.ToListAsync();
projectOrgMappings = await baseProjectOrgMappingQuery
.Where(po => projectIds.Contains(po.ProjectService!.ProjectId))
.ToListAsync();
}
else
{
projectOrgMappings = await baseProjectOrgMappingQuery.ToListAsync();
}
// Map results to output view model
var response = _mapper.Map<OrganizationDetailsVM>(organization);
response.ActiveApplicationUserCount = activeApplicationUserCount;
response.ActiveEmployeeCount = activeEmployeeCount;
response.CreatedBy = _mapper.Map<BasicEmployeeVM>(createdByEmployee);
response.UpdatedBy = _mapper.Map<BasicEmployeeVM>(updatedByEmployee);
response.Projects = _mapper.Map<List<ProjectServiceMappingVM>>(projectOrgMappings.Select(po => po.ProjectService).ToList());
response.Services = orgServiceMappings.Where(os => os.Service != null).Select(os => os.Service!).ToList();
_logger.LogInfo("Fetched organization details for OrganizationId: {OrganizationId}, Employee count: {EmployeeCount}, App user count: {AppUserCount}, Project count: {ProjectCount}",
id, activeEmployeeCount, activeApplicationUserCount, response.Projects.Count);
return Ok(ApiResponse<object>.SuccessResponse(response, "Successfully fetched the organization details", 200));
}
catch (DbUpdateException dbEx)
{
_logger.LogError(dbEx, "Database exception while fetching details for OrganizationId: {OrganizationId}", id);
return StatusCode(500, ApiResponse<object>.ErrorResponse("Internal error", "A database exception occurred", 500));
}
catch (Exception ex)
{
_logger.LogError(ex, "Unhandled exception while fetching details for OrganizationId: {OrganizationId}", id);
return StatusCode(500, ApiResponse<object>.ErrorResponse("Internal error", "An internal exception occurred", 500));
}
} }
#endregion #endregion
@ -379,7 +207,7 @@ namespace Marco.Pms.Services.Controllers
?? await _context.JobRoles.FirstOrDefaultAsync(jr => jr.TenantId == tenantId); ?? await _context.JobRoles.FirstOrDefaultAsync(jr => jr.TenantId == tenantId);
// Parse full name safely (consider improving split logic for multi-part names) // Parse full name safely (consider improving split logic for multi-part names)
var fullName = model.ContactPerson.Split(' ', StringSplitOptions.RemoveEmptyEntries); var fullName = model.ContactPerson?.Split(' ', StringSplitOptions.RemoveEmptyEntries) ?? Array.Empty<string>();
Employee newEmployee = new Employee Employee newEmployee = new Employee
{ {
@ -395,22 +223,19 @@ namespace Marco.Pms.Services.Controllers
IsSystem = false, IsSystem = false,
IsPrimary = true, IsPrimary = true,
OrganizationId = organization.Id, OrganizationId = organization.Id,
HasApplicationAccess = true HasApplicationAccess = user.Id != null
}; };
_context.Employees.Add(newEmployee); _context.Employees.Add(newEmployee);
// Map organization services // Map organization services
if (model.ServiceIds?.Any() ?? false) var serviceOrgMappings = model.ServiceIds.Select(s => new OrgServiceMapping
{ {
var serviceOrgMappings = model.ServiceIds.Select(s => new OrgServiceMapping ServiceId = s,
{ OrganizationId = organization.Id
ServiceId = s, }).ToList();
OrganizationId = organization.Id
}).ToList();
_context.OrgServiceMappings.AddRange(serviceOrgMappings); _context.OrgServiceMappings.AddRange(serviceOrgMappings);
}
// Persist all changes // Persist all changes
await _context.SaveChangesAsync(); await _context.SaveChangesAsync();
@ -444,25 +269,19 @@ namespace Marco.Pms.Services.Controllers
} }
} }
[HttpPost("assign/project")] [HttpPost("assign/project")]
public async Task<IActionResult> AssignOrganizationToProjectAsync([FromBody] AssignOrganizationDto model) public async Task<IActionResult> AssignOrganizationToProjectAsync([FromBody] AssignOrganizationDto model)
{ {
_logger.LogDebug("Started assigning organization {OrganizationId} to project {ProjectId} with service IDs {@ServiceIds}",
model.OrganizationId, model.ProjectId, model.ServiceIds);
// Create DbContext for the method scope
await using var _context = await _dbContextFactory.CreateDbContextAsync(); await using var _context = await _dbContextFactory.CreateDbContextAsync();
using var scope = _serviceScope.CreateScope();
// Begin a database transaction
await using var transaction = await _context.Database.BeginTransactionAsync(); await using var transaction = await _context.Database.BeginTransactionAsync();
try try
{ {
// Get currently logged in employee
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
var today = DateTime.UtcNow.Date; var todaysDate = DateTime.UtcNow.Date;
// Fetch all needed entities concurrently using the single context
var projectServicesTask = Task.Run(async () => var projectServicesTask = Task.Run(async () =>
{ {
await using var context = await _dbContextFactory.CreateDbContextAsync(); await using var context = await _dbContextFactory.CreateDbContextAsync();
@ -470,7 +289,7 @@ namespace Marco.Pms.Services.Controllers
.Where(sp => model.ServiceIds.Contains(sp.ServiceId) && sp.ProjectId == model.ProjectId && sp.IsActive).ToListAsync(); .Where(sp => model.ServiceIds.Contains(sp.ServiceId) && sp.ProjectId == model.ProjectId && sp.IsActive).ToListAsync();
}); });
var projectOrgMappingsTask = Task.Run(async () => var projectOrganizationsTask = Task.Run(async () =>
{ {
await using var context = await _dbContextFactory.CreateDbContextAsync(); await using var context = await _dbContextFactory.CreateDbContextAsync();
return await context.ProjectOrgMappings return await context.ProjectOrgMappings
@ -484,7 +303,7 @@ namespace Marco.Pms.Services.Controllers
return await context.ServiceMasters.Where(s => model.ServiceIds.Contains(s.Id) && s.TenantId == tenantId).ToListAsync(); return await context.ServiceMasters.Where(s => model.ServiceIds.Contains(s.Id) && s.TenantId == tenantId).ToListAsync();
}); });
var orgTypeTask = Task.Run(async () => var organizationTypeTask = Task.Run(async () =>
{ {
await using var context = await _dbContextFactory.CreateDbContextAsync(); await using var context = await _dbContextFactory.CreateDbContextAsync();
return await context.OrgTypeMasters.FirstOrDefaultAsync(o => o.Id == model.OrganizationId); return await context.OrgTypeMasters.FirstOrDefaultAsync(o => o.Id == model.OrganizationId);
@ -496,7 +315,7 @@ namespace Marco.Pms.Services.Controllers
return await context.Organizations.FirstOrDefaultAsync(o => o.Id == model.OrganizationId); return await context.Organizations.FirstOrDefaultAsync(o => o.Id == model.OrganizationId);
}); });
var parentOrgTask = Task.Run(async () => var parentorganizationTask = 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.ParentOrganizationId); return await context.Organizations.FirstOrDefaultAsync(o => o.Id == model.ParentOrganizationId);
@ -521,82 +340,68 @@ namespace Marco.Pms.Services.Controllers
&& p.OrganizationTypeId == ServiceProvider); && p.OrganizationTypeId == ServiceProvider);
}); });
await Task.WhenAll(projectTask, organizationTask, parentOrgTask, serviceTask, orgTypeTask, projectServicesTask, projectOrgMappingsTask, isPMCTask, isServiceProviderTask); await Task.WhenAll(organizationTask, parentorganizationTask, projectTask, projectServicesTask, isPMCTask, isServiceProviderTask,
serviceTask, organizationTypeTask, projectOrganizationsTask);
var project = projectTask.Result;
var organization = organizationTask.Result;
var parentOrganization = parentOrgTask.Result;
var services = serviceTask.Result; var services = serviceTask.Result;
var organizationType = orgTypeTask.Result; var organizationType = organizationTypeTask.Result;
var organization = organizationTask.Result;
var projectServices = projectServicesTask.Result; var projectServices = projectServicesTask.Result;
var projectOrganizations = projectOrgMappingsTask.Result; var parentorganization = parentorganizationTask.Result;
var projectOrganizations = projectOrganizationsTask.Result;
var project = projectTask.Result;
var isPMC = isPMCTask.Result; var isPMC = isPMCTask.Result;
var isServiceProvider = isServiceProviderTask.Result; var isServiceProvider = isServiceProviderTask.Result;
// Validation checks
if (organization == null) if (organization == null)
{ {
_logger.LogWarning("Organization with ID {OrganizationId} not found.", model.OrganizationId);
return NotFound(ApiResponse<object>.ErrorResponse("Organization not found", "Organization not found in database", 404)); return NotFound(ApiResponse<object>.ErrorResponse("Organization not found", "Organization not found in database", 404));
} }
if (project == null) if (project == null)
{ {
_logger.LogWarning("Project with ID {ProjectId} not found.", model.ProjectId);
return NotFound(ApiResponse<object>.ErrorResponse("Project not found", "Project not found in database", 404)); return NotFound(ApiResponse<object>.ErrorResponse("Project not found", "Project not found in database", 404));
} }
if (services == null || !services.Any()) if (projectServices == null)
{ {
_logger.LogWarning("No services found for Service IDs {@ServiceIds}.", model.ServiceIds);
return NotFound(ApiResponse<object>.ErrorResponse("Project Service not found", "Project Service not found in database", 404)); return NotFound(ApiResponse<object>.ErrorResponse("Project Service not found", "Project Service not found in database", 404));
} }
// Check whether mapping exists between service provider organization and tenant
var serviceProviderTenantMapping = await _context.TenantOrgMappings var serviceProviderTenantMapping = await _context.TenantOrgMappings
.FirstOrDefaultAsync(spt => spt.OrganizationId == model.OrganizationId && spt.TenantId == project.TenantId && spt.IsActive); .FirstOrDefaultAsync(spt => spt.OrganizationId == model.OrganizationId && spt.TenantId == project.TenantId && spt.IsActive);
if (serviceProviderTenantMapping == null) if (serviceProviderTenantMapping == null)
{ {
var newMapping = new TenantOrgMapping var newServiceProviderTenantMapping = new TenantOrgMapping
{ {
OrganizationId = organization.Id, OrganizationId = organization.Id,
SPRID = organization.SPRID, SPRID = organization.SPRID,
AssignedDate = today, AssignedDate = DateTime.UtcNow,
IsActive = true, IsActive = true,
AssignedById = loggedInEmployee.Id, AssignedById = loggedInEmployee.Id,
TenantId = project.TenantId TenantId = project.TenantId
}; };
_context.TenantOrgMappings.Add(newMapping); _context.TenantOrgMappings.Add(newServiceProviderTenantMapping);
_logger.LogInfo("Created new TenantOrgMapping for OrganizationId {OrganizationId} and TenantId {TenantId}",
organization.Id, project.TenantId);
} }
// Access control validations List<ProjectOrgMapping> projectOrgMappings = new List<ProjectOrgMapping>();
List<ProjectServiceMapping> projectServiceMappings = new List<ProjectServiceMapping>();
if (isPMC && model.OrganizationTypeId != ServiceProvider && model.OrganizationTypeId != SubContractorProvider) if (isPMC && model.OrganizationTypeId != ServiceProvider && model.OrganizationTypeId != SubContractorProvider)
{ {
_logger.LogWarning("PMCs cannot assign organization type {OrganizationTypeId}. UserId: {UserId}",
model.OrganizationTypeId, loggedInEmployee.Id);
return StatusCode(403, ApiResponse<object>.ErrorResponse("Access Denied", "You don't have access to assign this type of organization", 403)); return StatusCode(403, ApiResponse<object>.ErrorResponse("Access Denied", "You don't have access to assign this type of organization", 403));
} }
if (isServiceProvider && model.OrganizationTypeId == ServiceProvider) if (isServiceProvider && model.OrganizationTypeId == ServiceProvider)
{ {
_logger.LogWarning("Service providers cannot assign organization type {OrganizationTypeId}. UserId: {UserId}",
model.OrganizationTypeId, loggedInEmployee.Id);
return StatusCode(403, ApiResponse<object>.ErrorResponse("Access Denied", "You don't have access to assign this type of organization", 403)); return StatusCode(403, ApiResponse<object>.ErrorResponse("Access Denied", "You don't have access to assign this type of organization", 403));
} }
var newProjectOrgMappings = new List<ProjectOrgMapping>();
var newProjectServiceMappings = new List<ProjectServiceMapping>();
// Loop through each service to create mappings
foreach (var serviceId in model.ServiceIds) foreach (var serviceId in model.ServiceIds)
{ {
var service = services.FirstOrDefault(s => s.Id == serviceId); var service = await _context.ServiceMasters.FirstOrDefaultAsync(s => s.Id == serviceId);
if (service == null) if (service == null)
{ {
_logger.LogWarning("Service with ID {ServiceId} not found.", serviceId);
return NotFound(ApiResponse<object>.ErrorResponse("Service not found", "Service not found in database", 404)); return NotFound(ApiResponse<object>.ErrorResponse("Service not found", "Service not found in database", 404));
} }
var projectService = projectServices.FirstOrDefault(ps => ps.ServiceId == serviceId); var projectService = projectServices.FirstOrDefault(ps => ps.ServiceId == serviceId);
if (projectService == null) if (projectService == null)
{ {
@ -606,285 +411,202 @@ namespace Marco.Pms.Services.Controllers
ProjectId = project.Id, ProjectId = project.Id,
ServiceId = serviceId, ServiceId = serviceId,
TenantId = project.TenantId, TenantId = project.TenantId,
PlannedStartDate = project.StartDate ?? today, PlannedStartDate = project.StartDate ?? DateTime.UtcNow,
PlannedEndDate = project.EndDate ?? today, PlannedEndDate = project.EndDate ?? DateTime.UtcNow,
ActualStartDate = today, ActualStartDate = DateTime.UtcNow,
IsActive = true IsActive = true
}; };
newProjectServiceMappings.Add(projectService); projectServiceMappings.Add(projectService);
} }
// Check if the organization is already assigned for this service
var existingAssignment = projectOrganizations.FirstOrDefault(po => po.ProjectService != null
&& po.ProjectService.ProjectId == project.Id
&& po.ProjectService.ServiceId == serviceId
&& po.OrganizationId == model.OrganizationId);
if (existingAssignment != null)
{
_logger.LogWarning("Organization {OrganizationId} is already assigned to project {ProjectId} for service {ServiceId}.",
model.OrganizationId, project.Id, serviceId);
return Conflict(ApiResponse<object>.ErrorResponse("Organization already assigned", "Organization is already assigned to this project and service", 409));
}
// Prepare new project-org mapping
var projectOrgMapping = new ProjectOrgMapping var projectOrgMapping = new ProjectOrgMapping
{ {
ProjectServiceId = projectService.Id, ProjectServiceId = projectService.Id,
OrganizationId = model.OrganizationId, OrganizationId = model.OrganizationId,
OrganizationTypeId = model.OrganizationTypeId, OrganizationTypeId = model.OrganizationTypeId,
ParentOrganizationId = model.ParentOrganizationId ?? loggedInEmployee.OrganizationId, ParentOrganizationId = model.ParentOrganizationId ?? loggedInEmployee.OrganizationId,
AssignedDate = today, AssignedDate = DateTime.UtcNow,
AssignedById = loggedInEmployee.Id, AssignedById = loggedInEmployee.Id,
TenantId = project.TenantId TenantId = project.TenantId
}; };
newProjectOrgMappings.Add(projectOrgMapping); var projectOrganization = projectOrganizations
.FirstOrDefault(po => po.ProjectService != null && po.ProjectService.ProjectId == project.Id && po.ProjectService.ServiceId == serviceId
&& po.OrganizationId == model.OrganizationId);
if (projectOrganization != null)
{
return StatusCode(409, ApiResponse<object>.ErrorResponse("Organization is already assigned to this project", "Organization is already assigned to this project", 409));
}
projectOrgMappings.Add(projectOrgMapping);
} }
// Save new project service mappings if any if (projectServiceMappings.Any())
if (newProjectServiceMappings.Any())
{ {
_context.ProjectServiceMappings.AddRange(newProjectServiceMappings); _context.ProjectServiceMappings.AddRange(projectServiceMappings);
await _context.SaveChangesAsync(); await _context.SaveChangesAsync();
_logger.LogInfo("Added {Count} new ProjectServiceMappings for ProjectId {ProjectId}.", newProjectServiceMappings.Count, project.Id);
} }
// Save new project organization mappings _context.ProjectOrgMappings.AddRange(projectOrgMappings);
_context.ProjectOrgMappings.AddRange(newProjectOrgMappings);
await _context.SaveChangesAsync();
// Commit transaction await _context.SaveChangesAsync();
await transaction.CommitAsync(); await transaction.CommitAsync();
_logger.LogInfo("Assigned organization {OrganizationId} to project {ProjectId} successfully.", model.OrganizationId, model.ProjectId);
// Prepare response view models
var organizationVm = _mapper.Map<BasicOrganizationVm>(organization); var organizationVm = _mapper.Map<BasicOrganizationVm>(organization);
var parentOrganizationVm = _mapper.Map<BasicOrganizationVm>(parentOrganization); var parentorganizationVm = _mapper.Map<BasicOrganizationVm>(parentorganization);
var projectVm = _mapper.Map<BasicProjectVM>(project); var projectvm = _mapper.Map<BasicProjectVM>(project);
var response = services.Select(s => new AssignOrganizationVm var response = services.Select(s => new AssignOrganizationVm
{ {
Project = projectVm, Project = projectvm,
OrganizationType = organizationType, OrganizationType = organizationType,
Organization = organizationVm, Organization = organizationVm,
ParentOrganization = parentOrganizationVm, ParentOrganization = parentorganizationVm,
Service = _mapper.Map<ServiceMasterVM>(s) Service = _mapper.Map<ServiceMasterVM>(s)
}).ToList(); }).ToList();
await AssignApplicationRoleToOrganization(organization.Id, project.TenantId);
return Ok(ApiResponse<object>.SuccessResponse(response, "Organization successfully assigned to the project", 200)); return Ok(ApiResponse<object>.SuccessResponse(response, "Organization successfully assigned to the project", 200));
} }
catch (DbUpdateException dbEx) catch (DbUpdateException dbEx)
{ {
await transaction.RollbackAsync(); await transaction.RollbackAsync();
_logger.LogError(dbEx, "Database exception occurred while assigning organization {OrganizationId} to project {ProjectId}",
model.OrganizationId, model.ProjectId); _logger.LogError(dbEx, "Database Exception has been occured, While assigning the organization to project");
return StatusCode(500, ApiResponse<object>.ErrorResponse("Internal error", "A database exception occurred", 500)); return StatusCode(500, ApiResponse<object>.ErrorResponse("Internal error", "An database exception has been occured", 500));
} }
catch (Exception ex) catch (Exception ex)
{ {
await transaction.RollbackAsync(); _logger.LogError(ex, "Exception has been occured, While assigned the organizatio to project");
_logger.LogError(ex, "Unhandled exception occurred while assigning organization {OrganizationId} to project {ProjectId}", return StatusCode(500, ApiResponse<object>.ErrorResponse("Internal error", "An internal exception has been occured", 500));
model.OrganizationId, model.ProjectId);
return StatusCode(500, ApiResponse<object>.ErrorResponse("Internal error", "An internal exception occurred", 500));
} }
} }
[HttpPost("assign/tenant/{organizationId}")] [HttpPost("assign/tenant/{organizationId}")]
public async Task<IActionResult> AssignOrganizationToTenantAsync(Guid organizationId) public async Task<IActionResult> AssignOrganizationToTenantAsync(Guid organizationId)
{ {
_logger.LogInfo("Started assigning organization {OrganizationId} to tenant {TenantId}", organizationId, tenantId);
// Create a DbContext instance for this method scope
await using var _context = await _dbContextFactory.CreateDbContextAsync(); await using var _context = await _dbContextFactory.CreateDbContextAsync();
using var scope = _serviceScope.CreateScope();
// Begin a database transaction
await using var transaction = await _context.Database.BeginTransactionAsync(); await using var transaction = await _context.Database.BeginTransactionAsync();
try try
{ {
// Get currently logged in employee for auditing purposes
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
// Fetch existing tenant-organization mapping if any var organizationTenantMappingTask = Task.Run(async () =>
var organizationTenantMapping = await _context.TenantOrgMappings {
.FirstOrDefaultAsync(spt => spt.OrganizationId == organizationId && spt.TenantId == tenantId && spt.IsActive); await using var context = await _dbContextFactory.CreateDbContextAsync();
return await context.TenantOrgMappings
.FirstOrDefaultAsync(spt => spt.OrganizationId == organizationId && spt.TenantId == tenantId && spt.IsActive);
});
// Fetch the organization details var organizationTask = Task.Run(async () =>
var organization = await _context.Organizations.FirstOrDefaultAsync(o => o.Id == organizationId); {
await using var context = await _dbContextFactory.CreateDbContextAsync();
return await context.Organizations
.FirstOrDefaultAsync(o => o.Id == organizationId);
});
await Task.WhenAll(organizationTenantMappingTask, organizationTask);
var organizationTenantMapping = organizationTenantMappingTask.Result;
var organization = organizationTask.Result;
// Validate organization existence
if (organization == null) if (organization == null)
{ {
_logger.LogWarning("Organization with ID {OrganizationId} not found.", organizationId);
return NotFound(ApiResponse<object>.ErrorResponse("Organization not found", "Organization not found in database", 404)); return NotFound(ApiResponse<object>.ErrorResponse("Organization not found", "Organization not found in database", 404));
} }
if (organizationTenantMapping != null) if (organizationTenantMapping == null)
{ {
_logger.LogInfo("Organization {OrganizationId} is already assigned to tenant {TenantId}. No action taken.", organizationId, tenantId); var newOrganizationTenantMapping = new TenantOrgMapping
// Commit transaction anyway to complete scope cleanly (optional) {
await transaction.RollbackAsync(); OrganizationId = organization.Id,
return StatusCode(409, ApiResponse<object>.ErrorResponse("Organization is already assigned to tenant", "Organization is already assigned to tenant", 409)); SPRID = organization.SPRID,
AssignedDate = DateTime.UtcNow,
IsActive = true,
AssignedById = loggedInEmployee.Id,
TenantId = tenantId
};
_context.TenantOrgMappings.Add(newOrganizationTenantMapping);
await _context.SaveChangesAsync();
await transaction.CommitAsync();
} }
// Create new tenant-organization mapping if none exists
var newMapping = new TenantOrgMapping
{
OrganizationId = organization.Id,
SPRID = organization.SPRID,
AssignedDate = DateTime.UtcNow,
IsActive = true,
AssignedById = loggedInEmployee.Id,
TenantId = tenantId
};
_context.TenantOrgMappings.Add(newMapping);
await _context.SaveChangesAsync();
await transaction.CommitAsync();
_logger.LogInfo("Assigned organization {OrganizationId} to tenant {TenantId} successfully.", organizationId, tenantId);
// Prepare response view model
var response = _mapper.Map<BasicOrganizationVm>(organization); var response = _mapper.Map<BasicOrganizationVm>(organization);
await AssignApplicationRoleToOrganization(organization.Id, tenantId);
return Ok(ApiResponse<object>.SuccessResponse(response, "Organization has been assigned to tenant", 200)); return Ok(ApiResponse<object>.SuccessResponse(response, "Organization has been assigned to tenant", 200));
} }
catch (DbUpdateException dbEx) catch (DbUpdateException dbEx)
{ {
await transaction.RollbackAsync(); await transaction.RollbackAsync();
_logger.LogError(dbEx, "Database exception occurred while assigning organization {OrganizationId} to tenant {TenantId}.", organizationId, tenantId);
return StatusCode(500, ApiResponse<object>.ErrorResponse("Internal error", "A database exception has occurred", 500)); _logger.LogError(dbEx, "Database Exception has been occured, While assigning the organization to project");
return StatusCode(500, ApiResponse<object>.ErrorResponse("Internal error", "An database exception has been occured", 500));
} }
catch (Exception ex) catch (Exception ex)
{ {
await transaction.RollbackAsync(); _logger.LogError(ex, "Exception has been occured, While assigned the organizatio to project");
_logger.LogError(ex, "Unhandled exception occurred while assigning organization {OrganizationId} to tenant {TenantId}.", organizationId, tenantId); return StatusCode(500, ApiResponse<object>.ErrorResponse("Internal error", "An internal exception has been occured", 500));
return StatusCode(500, ApiResponse<object>.ErrorResponse("Internal error", "An internal exception has occurred", 500));
} }
} }
#endregion #endregion
#region =================================================================== Put Functions =================================================================== #region =================================================================== Put Functions ===================================================================
[HttpPut("edit/{id}")] //[HttpPut("edit/{id}")]
public async Task<IActionResult> UpdateOrganiationAsync(Guid id, [FromBody] UpdateOrganizationDto model) //public async Task<IActionResult> UpdateServiceProviderAsync(Guid id, [FromBody] UpdateOrganizationDto model)
{ //{
await using var _context = await _dbContextFactory.CreateDbContextAsync(); // await using var _context = await _dbContextFactory.CreateDbContextAsync();
// using var scope = _serviceScope.CreateScope();
try // var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
{
// Get the current logged-in employee
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
_logger.LogDebug("Started updating organization OrganizationId: {OrganizationId} by EmployeeId: {EmployeeId}", // var clientTask = Task.Run(async () =>
id, loggedInEmployee.Id); // {
// await using var context = await _dbContextFactory.CreateDbContextAsync();
// return await context.Clients.FirstOrDefaultAsync(c => c.PrimaryEmployeeId == loggedInEmployee.Id && c.TenantId == tenantId);
// });
// var employeeTask = Task.Run(async () =>
// {
// await using var context = await _dbContextFactory.CreateDbContextAsync();
// return await context.Employees.AsNoTracking().FirstOrDefaultAsync(e => e.OrganizationId == id && e.IsPrimary);
// });
// Check if the user is a tenant-level employee and restrict editing to their own org // await Task.WhenAll(clientTask, employeeTask);
var isTenantEmployee = await _context.Tenants.AnyAsync(t => t.Id == tenantId && t.OrganizationId == loggedInEmployee.OrganizationId);
if (!isTenantEmployee && loggedInEmployee.OrganizationId != id)
{
_logger.LogWarning("Access denied. Tenant-level employee {EmployeeId} attempted to update another organization (OrganizationId: {OrganizationId})",
loggedInEmployee.Id, id);
return StatusCode(403, ApiResponse<object>.ErrorResponse("Access Denied", "User does not have permission to update the organization", 403));
}
// Fetch the active organization entity // var client = clientTask.Result;
var organization = await _context.Organizations.FirstOrDefaultAsync(o => o.Id == id && o.IsActive); // var employee = employeeTask.Result;
if (organization == null)
{
_logger.LogWarning("Organization with Id {OrganizationId} not found or inactive.", id);
return NotFound(ApiResponse<object>.ErrorResponse("Organization not found", "Organization not found", 404));
}
// Update basic organization fields // if (employee == null)
organization.Name = model.Name; // {
organization.ContactPerson = model.ContactPerson; // return NotFound(ApiResponse<object>.ErrorResponse("Primary employee for service provider not found", "Primary employee for service provider not found in database", 400));
organization.Address = model.Address; // }
organization.ContactNumber = model.ContactNumber;
organization.UpdatedById = loggedInEmployee.Id;
organization.UpdatedAt = DateTime.UtcNow;
// Fetch the primary active employee of the organization // if (!(loggedInEmployee.ApplicationUser?.IsRootUser ?? false) && !loggedInEmployee.IsPrimary && client == null && employee.Id == loggedInEmployee.Id)
var employee = await _context.Employees.FirstOrDefaultAsync(e => e.OrganizationId == id && e.IsPrimary && e.IsActive); // {
if (employee == null) // return StatusCode(403, ApiResponse<object>.ErrorResponse("Access Denied", "You do not have permission to create new service provider.", 403));
{ // }
_logger.LogWarning("Primary employee not found for OrganizationId: {OrganizationId}", id); // if (model.Id == id)
return NotFound(ApiResponse<object>.ErrorResponse("Primary employee not found", "Primary employee not found", 404)); // {
} // return BadRequest(ApiResponse<object>.ErrorResponse("Invalid Input", "Id from path parameters do not matches with id from model", 400));
// }
// var serviceProvider = await _context.ServiceProviders.FirstOrDefaultAsync(sp => sp.Id == model.Id);
// if (serviceProvider == null)
// {
// return NotFound(ApiResponse<object>.ErrorResponse("Service Provider not found", "Service Provider not found in database", 404));
// }
// _mapper.Map(model, serviceProvider);
// Split contact person's name into first and last names // _context.ServiceProviders.Update(serviceProvider);
var fullName = (model.ContactPerson ?? string.Empty).Split(' ', StringSplitOptions.RemoveEmptyEntries);
employee.FirstName = fullName.Length > 0 ? fullName[0] : string.Empty;
employee.LastName = fullName.Length > 1 ? fullName[^1] : string.Empty;
employee.CurrentAddress = model.Address;
employee.PermanentAddress = model.Address;
employee.PhoneNumber = model.ContactNumber;
// Update organization's service mappings if service IDs are provided // var fullName = model.ContactPerson.Split(" ");
if (model.ServiceIds?.Any() ?? false)
{
// Fetch existing service mappings (as no tracking for diff logic)
var orgServiceMappings = await _context.OrgServiceMappings
.AsNoTracking()
.Where(os => os.OrganizationId == id)
.ToListAsync();
var existedServiceIds = orgServiceMappings.Select(os => os.ServiceId).ToList(); // employee.FirstName = fullName[0];
// employee.LastName = fullName[fullName.Length - 1];
// employee.CurrentAddress = model.Address;
// employee.PermanentAddress = model.Address;
// employee.PhoneNumber = model.PhoneNumber;
// Determine new service mappings to add // _context.Employees.Update(employee);
var newServiceIds = model.ServiceIds.Except(existedServiceIds).ToList(); // await _context.SaveChangesAsync();
var orgServicesToDelete = orgServiceMappings
.Where(s => !model.ServiceIds.Contains(s.ServiceId))
.ToList();
// Add new service mappings // var response = _mapper.Map<OrganizationVM>(serviceProvider);
if (newServiceIds.Any()) // return Ok(ApiResponse<object>.SuccessResponse(response, "Successfully updated the service provider", 200));
{ //}
var newMappings = newServiceIds.Select(sid => new OrgServiceMapping
{
OrganizationId = id,
ServiceId = sid
});
await _context.OrgServiceMappings.AddRangeAsync(newMappings);
}
// Remove deleted service mappings
if (orgServicesToDelete.Any())
{
_context.OrgServiceMappings.RemoveRange(orgServicesToDelete);
}
}
await _context.SaveChangesAsync();
var response = _mapper.Map<OrganizationVM>(organization);
var createdByEmployee = await _context.Employees.Include(e => e.JobRole).FirstOrDefaultAsync(e => e.Id == organization.CreatedById);
response.CreatedBy = _mapper.Map<BasicEmployeeVM>(createdByEmployee);
response.UpdatedBy = _mapper.Map<BasicEmployeeVM>(loggedInEmployee);
_logger.LogInfo("Successfully updated organization OrganizationId: {OrganizationId} by EmployeeId: {EmployeeId}",
id, loggedInEmployee.Id);
return Ok(ApiResponse<object>.SuccessResponse(response, "Organization updated Successfully", 200));
}
catch (DbUpdateException dbEx)
{
_logger.LogError(dbEx, "Database exception occurred while updating OrganizationId: {OrganizationId}", id);
return StatusCode(500, ApiResponse<object>.ErrorResponse("Internal error", "A database exception occurred", 500));
}
catch (Exception ex)
{
_logger.LogError(ex, "Unhandled exception occurred while updating OrganizationId: {OrganizationId}", id);
return StatusCode(500, ApiResponse<object>.ErrorResponse("Internal error", "An internal exception occurred", 500));
}
}
#endregion #endregion
@ -945,98 +667,45 @@ namespace Marco.Pms.Services.Controllers
#endregion #endregion
#region =================================================================== Helper Functions =================================================================== #region =================================================================== Helper Functions ===================================================================
//private ServicesProviderFilter? TryDeserializeServicesProviderFilter(string? filter)
//{
// if (string.IsNullOrWhiteSpace(filter))
// {
// return null;
// }
private async Task AssignApplicationRoleToOrganization(Guid organizationId, Guid tenantId) // var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true };
{ // ServicesProviderFilter? documentFilter = null;
if (loggedOrganizationId == organizationId)
{
return;
}
await using var _context = await _dbContextFactory.CreateDbContextAsync();
using var scope = _serviceScope.CreateScope();
var rootEmployee = await _context.Employees // try
.Include(e => e.ApplicationUser) // {
.FirstOrDefaultAsync(e => e.ApplicationUser != null && e.ApplicationUser.IsRootUser.HasValue && e.ApplicationUser.IsRootUser.Value && e.OrganizationId == organizationId && e.IsPrimary); // // First, try to deserialize directly. This is the expected case (e.g., from a web client).
if (rootEmployee == null) // documentFilter = JsonSerializer.Deserialize<ServicesProviderFilter>(filter, options);
{ // }
return; // catch (JsonException ex)
} // {
string serviceProviderRoleName = "Service Provider Role"; // _logger.LogError(ex, "[{MethodName}] Failed to directly deserialize filter. Attempting to unescape and re-parse. Filter: {Filter}", nameof(TryDeserializeServicesProviderFilter), filter);
var serviceProviderRole = await _context.ApplicationRoles.FirstOrDefaultAsync(ar => ar.Role == serviceProviderRoleName && ar.TenantId == tenantId); // // If direct deserialization fails, it might be an escaped string (common with tools like Postman or some mobile clients).
if (serviceProviderRole == null) // try
{ // {
serviceProviderRole = new Model.Roles.ApplicationRole // // Unescape the string first, then deserialize the result.
{ // string unescapedJsonString = JsonSerializer.Deserialize<string>(filter, options) ?? "";
Id = Guid.NewGuid(), // if (!string.IsNullOrWhiteSpace(unescapedJsonString))
Role = serviceProviderRoleName, // {
Description = serviceProviderRoleName, // documentFilter = JsonSerializer.Deserialize<ServicesProviderFilter>(unescapedJsonString, options);
IsSystem = true, // }
TenantId = tenantId // }
}; // catch (JsonException ex1)
_context.ApplicationRoles.Add(serviceProviderRole); // {
// // If both attempts fail, log the final error and return null.
// _logger.LogError(ex1, "[{MethodName}] All attempts to deserialize the filter failed. Filter will be ignored. Filter: {Filter}", nameof(TryDeserializeServicesProviderFilter), filter);
// return null;
// }
// }
// return documentFilter;
//}
var rolePermissionMappigs = new List<RolePermissionMappings> {
new RolePermissionMappings
{
ApplicationRoleId = serviceProviderRole.Id,
FeaturePermissionId = PermissionsMaster.ViewProject
},
new RolePermissionMappings
{
ApplicationRoleId = serviceProviderRole.Id,
FeaturePermissionId = PermissionsMaster.ViewProjectInfra
},
new RolePermissionMappings
{
ApplicationRoleId = serviceProviderRole.Id,
FeaturePermissionId = PermissionsMaster.ViewTask
},
new RolePermissionMappings
{
ApplicationRoleId = serviceProviderRole.Id,
FeaturePermissionId = PermissionsMaster.ViewAllEmployees
},
new RolePermissionMappings
{
ApplicationRoleId = serviceProviderRole.Id,
FeaturePermissionId = PermissionsMaster.TeamAttendance
},
new RolePermissionMappings
{
ApplicationRoleId = serviceProviderRole.Id,
FeaturePermissionId = PermissionsMaster.AssignRoles
},
new RolePermissionMappings
{
ApplicationRoleId = serviceProviderRole.Id,
FeaturePermissionId = PermissionsMaster.ManageProjectInfra
},
new RolePermissionMappings
{
ApplicationRoleId = serviceProviderRole.Id,
FeaturePermissionId = PermissionsMaster.AssignAndReportProgress
},
new RolePermissionMappings
{
ApplicationRoleId = serviceProviderRole.Id,
FeaturePermissionId = PermissionsMaster.AddAndEditTask
}
};
_context.RolePermissionMappings.AddRange(rolePermissionMappigs);
}
_context.EmployeeRoleMappings.Add(new EmployeeRoleMapping
{
EmployeeId = rootEmployee.Id,
RoleId = serviceProviderRole.Id,
IsEnabled = true,
TenantId = tenantId
});
var _cache = scope.ServiceProvider.GetRequiredService<CacheUpdateHelper>();
await _cache.ClearAllPermissionIdsByEmployeeID(rootEmployee.Id, tenantId);
}
#endregion #endregion
} }
} }

View File

@ -319,14 +319,6 @@ namespace MarcoBMS.Services.Controllers
return StatusCode(response.StatusCode, response); return StatusCode(response.StatusCode, response);
} }
[HttpGet("get/task/team/{projectId}")]
public async Task<IActionResult> GetProjectTeamByServiceAndOrganization(Guid projectId, [FromQuery] Guid? serviceId, [FromQuery] Guid? organizationId)
{
Employee loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
var response = await _projectServices.GetProjectTeamByServiceAndOrganizationAsync(projectId, serviceId, organizationId, tenantId, loggedInEmployee);
return StatusCode(response.StatusCode, response);
}
#endregion #endregion
#region =================================================================== Project InfraStructure Get APIs =================================================================== #region =================================================================== Project InfraStructure Get APIs ===================================================================
@ -519,7 +511,6 @@ namespace MarcoBMS.Services.Controllers
return StatusCode(response.StatusCode, response); return StatusCode(response.StatusCode, response);
} }
[HttpPost("assign/service")] [HttpPost("assign/service")]
public async Task<IActionResult> AssignServiceToProject([FromBody] AssignServiceDto model) public async Task<IActionResult> AssignServiceToProject([FromBody] AssignServiceDto model)
{ {
@ -560,13 +551,6 @@ namespace MarcoBMS.Services.Controllers
var response = await _projectServices.GetAssignedOrganizationsToProjectAsync(projectId, tenantId, loggedInEmployee); var response = await _projectServices.GetAssignedOrganizationsToProjectAsync(projectId, tenantId, loggedInEmployee);
return StatusCode(response.StatusCode, response); return StatusCode(response.StatusCode, response);
} }
[HttpGet("get/assigned/organization/dropdown/{projectId}")]
public async Task<IActionResult> GetAssignedOrganizationsToProjectForDropdownAsync(Guid projectId)
{
Employee loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
var response = await _projectServices.GetAssignedOrganizationsToProjectForDropdownAsync(projectId, tenantId, loggedInEmployee);
return StatusCode(response.StatusCode, response);
}
#endregion #endregion
} }

View File

@ -1,5 +1,4 @@
using Marco.Pms.DataAccess.Data; using Marco.Pms.DataAccess.Data;
using Marco.Pms.Model.Dtos.Attendance;
using Marco.Pms.Model.Dtos.Mail; using Marco.Pms.Model.Dtos.Mail;
using Marco.Pms.Model.Mail; using Marco.Pms.Model.Mail;
using Marco.Pms.Model.MongoDBModels.Utility; using Marco.Pms.Model.MongoDBModels.Utility;
@ -11,6 +10,7 @@ using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using MongoDB.Driver;
using System.Data; using System.Data;
using System.Globalization; using System.Globalization;
using System.Net.Mail; using System.Net.Mail;
@ -22,25 +22,27 @@ namespace Marco.Pms.Services.Controllers
[Authorize] [Authorize]
public class ReportController : ControllerBase public class ReportController : ControllerBase
{ {
private readonly IDbContextFactory<ApplicationDbContext> _dbContextFactory;
private readonly IServiceScopeFactory _serviceScopeFactory;
private readonly ApplicationDbContext _context; private readonly ApplicationDbContext _context;
private readonly IEmailSender _emailSender;
private readonly ILoggingService _logger; private readonly ILoggingService _logger;
private readonly UserHelper _userHelper; private readonly UserHelper _userHelper;
private readonly IWebHostEnvironment _env;
private readonly ReportHelper _reportHelper;
private readonly IConfiguration _configuration; private readonly IConfiguration _configuration;
public ReportController(IDbContextFactory<ApplicationDbContext> dbContextFactory, private readonly CacheUpdateHelper _cache;
ApplicationDbContext context, private readonly IServiceScopeFactory _serviceScopeFactory;
ILoggingService logger, public ReportController(ApplicationDbContext context, IEmailSender emailSender, ILoggingService logger, UserHelper userHelper,
UserHelper userHelper, IWebHostEnvironment env, ReportHelper reportHelper, IConfiguration configuration, CacheUpdateHelper cache, IServiceScopeFactory serviceScopeFactory)
IConfiguration configuration,
IServiceScopeFactory serviceScopeFactory)
{ {
_dbContextFactory = dbContextFactory ?? throw new ArgumentNullException(nameof(dbContextFactory)); _context = context;
_serviceScopeFactory = serviceScopeFactory ?? throw new ArgumentNullException(nameof(serviceScopeFactory)); _emailSender = emailSender;
_context = context ?? throw new ArgumentNullException(nameof(context)); _logger = logger;
_logger = logger ?? throw new ArgumentNullException(nameof(logger)); _userHelper = userHelper;
_userHelper = userHelper ?? throw new ArgumentNullException(nameof(userHelper)); _env = env;
_configuration = configuration ?? throw new ArgumentNullException(nameof(configuration)); _reportHelper = reportHelper;
_configuration = configuration;
_cache = cache;
_serviceScopeFactory = serviceScopeFactory;
} }
/// <summary> /// <summary>
@ -433,12 +435,10 @@ namespace Marco.Pms.Services.Controllers
200)); 200));
} }
[HttpGet("report-mail")] [HttpGet("report-mail")]
public async Task<IActionResult> GetProjectStatisticsFromCache() public async Task<IActionResult> GetProjectStatisticsFromCache()
{ {
using var scope = _serviceScopeFactory.CreateScope();
var _cache = scope.ServiceProvider.GetRequiredService<CacheUpdateHelper>();
var mailList = await _cache.GetProjectReportMail(false); var mailList = await _cache.GetProjectReportMail(false);
if (mailList == null) if (mailList == null)
{ {
@ -447,92 +447,5 @@ namespace Marco.Pms.Services.Controllers
return Ok(ApiResponse<object>.SuccessResponse(mailList, "Fetched list of mail body successfully", 200)); return Ok(ApiResponse<object>.SuccessResponse(mailList, "Fetched list of mail body successfully", 200));
} }
[HttpGet("report-attendance")]
public async Task<IActionResult> GetAttendanceReportAsync([FromQuery] bool isCurrentMonth = false)
{
Guid tenantId = _userHelper.GetTenantId();
using var scope = _serviceScopeFactory.CreateScope();
DateTime today = DateTime.Today;
DateTime firstDayOfMonth = new DateTime(today.Year, today.Month, 1);
DateTime firstDayOfNextMonth = firstDayOfMonth.AddMonths(1);
if (!isCurrentMonth)
{
firstDayOfNextMonth = firstDayOfMonth;
firstDayOfMonth = firstDayOfMonth.AddMonths(-1);
}
// Generate list of all dates in the month
var allDates = Enumerable.Range(0, (firstDayOfNextMonth - firstDayOfMonth).Days)
.Select(offset => firstDayOfMonth.AddDays(offset))
.ToList();
var attendancesTask = Task.Run(async () =>
{
await using var context = await _dbContextFactory.CreateDbContextAsync();
return await context.Attendes
.Where(a => a.AttendanceDate >= firstDayOfMonth && a.AttendanceDate < firstDayOfNextMonth && a.Employee != null && a.TenantId == tenantId)
.GroupBy(a => a.ProjectID)
.ToListAsync();
});
var projectAllocationTask = Task.Run(async () =>
{
await using var context = await _dbContextFactory.CreateDbContextAsync();
return await context.ProjectAllocations
.Include(pa => pa.Employee)
.Where(pa => pa.TenantId == tenantId && pa.IsActive)
.ToListAsync();
});
await Task.WhenAll(attendancesTask, projectAllocationTask);
var attendances = attendancesTask.Result;
var projectAllocations = projectAllocationTask.Result;
var result = attendances.Select(g =>
{
var projectAllocation = projectAllocations.Where(pa => pa.ProjectId == g.Key && pa.Employee != null).ToList();
var projectAttendance = projectAllocation.Select(pa =>
{
var attendances = g.Where(a => a.EmployeeId == pa.EmployeeId).ToList();
var attendanceDate = attendances.Select(a => a.AttendanceDate.Date).ToList();
return new
{
FirstName = pa.Employee!.FirstName,
LastName = pa.Employee.LastName,
Attendances = allDates.Select(d =>
{
var attendance = attendances.FirstOrDefault(a => a.AttendanceDate.Date == d);
return new
{
AttendanceDate = d,
CheckIn = attendance?.InTime,
CheckOut = attendance?.OutTime,
Activity = attendance?.Activity,
IsApproved = attendance?.ApprovedById.HasValue,
};
}).ToList(),
CheckInCheckOutDone = attendances.Where(a => a.InTime.HasValue && a.OutTime.HasValue && a.Activity == ATTENDANCE_MARK_TYPE.REGULARIZE).Count(),
CheckInDone = attendances.Where(a => a.InTime.HasValue).Count(),
CheckOutPending = attendances.Where(a => a.InTime.HasValue && !a.OutTime.HasValue).Count(),
RejectedRegularize = attendances.Where(a => a.Activity == ATTENDANCE_MARK_TYPE.REGULARIZE_REJECT).Count(),
AbsentAttendance = allDates.Where(d => !attendanceDate.Contains(d) && d.DayOfWeek != DayOfWeek.Sunday).Count()
};
}).OrderBy(ar => ar.FirstName).ThenBy(ar => ar.LastName).ToList();
return new
{
ProjectName = _context.Projects.Where(p => p.Id == g.Key && p.TenantId == tenantId).Select(p => p.Name).FirstOrDefault(),
ProjectAttendance = projectAttendance
};
}).ToList();
var response = result.OrderBy(r => r.ProjectName).ToList();
return Ok(ApiResponse<object>.SuccessResponse(response, "Attendance Report fetched successfully", 200));
}
} }
} }

View File

@ -457,7 +457,7 @@ namespace MarcoBMS.Services.Controllers
t.WorkItem.WorkArea != null && t.WorkItem.WorkArea != null &&
t.WorkItem.WorkArea.Floor != null && t.WorkItem.WorkArea.Floor != null &&
t.WorkItem.WorkArea.Floor.Building != null && t.WorkItem.WorkArea.Floor.Building != null &&
t.WorkItem.WorkArea.Floor.Building.ProjectId == projectId && t.WorkItem.WorkArea.Floor.Building.ProjectId != projectId &&
t.TenantId == tenantId); t.TenantId == tenantId);
var taskFilter = TryDeserializeFilter(filter); var taskFilter = TryDeserializeFilter(filter);
@ -482,13 +482,6 @@ 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 &&
@ -752,97 +745,6 @@ 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

@ -549,11 +549,7 @@ namespace Marco.Pms.Services.Controllers
ApplicationUserId = applicationUser.Id, ApplicationUserId = applicationUser.Id,
JobRole = adminJobRole, // Link to the newly created role JobRole = adminJobRole, // Link to the newly created role
CurrentAddress = model.BillingAddress, CurrentAddress = model.BillingAddress,
IsActive = true, OrganizationId = organization.Id
IsSystem = false,
IsPrimary = true,
OrganizationId = organization.Id,
HasApplicationAccess = true
}; };
_context.Employees.Add(employeeUser); _context.Employees.Add(employeeUser);
@ -640,16 +636,13 @@ namespace Marco.Pms.Services.Controllers
_context.ProjectAllocations.Add(projectAllocation); _context.ProjectAllocations.Add(projectAllocation);
// Map organization services // Map organization services
if (model.ServiceIds?.Any() ?? false) var serviceOrgMappings = model.ServiceIds.Select(s => new OrgServiceMapping
{ {
var serviceOrgMappings = model.ServiceIds.Select(s => new OrgServiceMapping ServiceId = s,
{ OrganizationId = organization.Id
ServiceId = s, }).ToList();
OrganizationId = organization.Id
}).ToList();
_context.OrgServiceMappings.AddRange(serviceOrgMappings); _context.OrgServiceMappings.AddRange(serviceOrgMappings);
}
// 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();
@ -1013,6 +1006,10 @@ namespace Marco.Pms.Services.Controllers
try try
{ {
_ = Task.Run(async () =>
{
await ClearPermissionForTenant();
});
var features = await _featureDetailsHelper.GetFeatureDetails(subscriptionPlan.FeaturesId); var features = await _featureDetailsHelper.GetFeatureDetails(subscriptionPlan.FeaturesId);
if (features == null) if (features == null)
{ {
@ -1065,7 +1062,7 @@ namespace Marco.Pms.Services.Controllers
// Get root employee and role for this tenant // Get root employee and role for this tenant
var rootEmployee = await _context.Employees var rootEmployee = await _context.Employees
.Include(e => e.ApplicationUser) .Include(e => e.ApplicationUser)
.FirstOrDefaultAsync(e => e.ApplicationUser != null && (e.ApplicationUser.IsRootUser ?? false) && e.OrganizationId == tenant.OrganizationId); .FirstOrDefaultAsync(e => e.ApplicationUser != null && (e.ApplicationUser.IsRootUser ?? false) && e.TenantId == model.TenantId);
if (rootEmployee == null) if (rootEmployee == null)
{ {
@ -1123,9 +1120,6 @@ namespace Marco.Pms.Services.Controllers
_logger.LogInfo("Removed {Count} role permission mappings for role {RoleId}", deleteMappings.Count, roleId); _logger.LogInfo("Removed {Count} role permission mappings for role {RoleId}", deleteMappings.Count, roleId);
} }
var _cache = scope.ServiceProvider.GetRequiredService<CacheUpdateHelper>();
await _cache.ClearAllEmployeesFromCacheByTenantId(tenant.Id);
var _masteData = scope.ServiceProvider.GetRequiredService<MasterDataService>(); var _masteData = scope.ServiceProvider.GetRequiredService<MasterDataService>();
if (features.Modules?.ProjectManagement?.Enabled ?? false) if (features.Modules?.ProjectManagement?.Enabled ?? false)
@ -1324,6 +1318,10 @@ namespace Marco.Pms.Services.Controllers
_logger.LogInfo("Subscription plan changed: Tenant={TenantId}, NewPlan={PlanId}", _logger.LogInfo("Subscription plan changed: Tenant={TenantId}, NewPlan={PlanId}",
model.TenantId, model.PlanId); model.TenantId, model.PlanId);
_ = Task.Run(async () =>
{
await ClearPermissionForTenant();
});
// 8. Update tenant permissions based on subscription features. // 8. Update tenant permissions based on subscription features.
var features = await _featureDetailsHelper.GetFeatureDetails(subscriptionPlan.FeaturesId); var features = await _featureDetailsHelper.GetFeatureDetails(subscriptionPlan.FeaturesId);
@ -1358,7 +1356,7 @@ namespace Marco.Pms.Services.Controllers
// 8c. Find root employee & role for this tenant. // 8c. Find root employee & role for this tenant.
var rootEmployee = await context.Employees var rootEmployee = await context.Employees
.Include(e => e.ApplicationUser) .Include(e => e.ApplicationUser)
.FirstOrDefaultAsync(e => e.ApplicationUser != null && (e.ApplicationUser.IsRootUser ?? false) && e.OrganizationId == tenant.OrganizationId); .FirstOrDefaultAsync(e => e.ApplicationUser != null && (e.ApplicationUser.IsRootUser ?? false) && e.TenantId == model.TenantId);
if (rootEmployee == null) if (rootEmployee == null)
{ {
@ -1369,8 +1367,7 @@ namespace Marco.Pms.Services.Controllers
var rootRoleId = await context.EmployeeRoleMappings var rootRoleId = await context.EmployeeRoleMappings
.AsNoTracking() .AsNoTracking()
.Include(er => er.Role) .Where(er => er.EmployeeId == rootEmployee.Id && er.TenantId == model.TenantId)
.Where(er => er.EmployeeId == rootEmployee.Id && er.TenantId == model.TenantId && er.Role != null && er.Role.Role == "Super User")
.Select(er => er.RoleId) .Select(er => er.RoleId)
.FirstOrDefaultAsync(); .FirstOrDefaultAsync();
@ -1435,9 +1432,6 @@ namespace Marco.Pms.Services.Controllers
_logger.LogInfo("Permissions revoked: {Count} for Role={RoleId}", mappingsToRemove.Count, rootRoleId); _logger.LogInfo("Permissions revoked: {Count} for Role={RoleId}", mappingsToRemove.Count, rootRoleId);
} }
var _cache = scope.ServiceProvider.GetRequiredService<CacheUpdateHelper>();
await _cache.ClearAllEmployeesFromCacheByTenantId(tenant.Id);
var _masteData = scope.ServiceProvider.GetRequiredService<MasterDataService>(); var _masteData = scope.ServiceProvider.GetRequiredService<MasterDataService>();
if (features.Modules?.ProjectManagement?.Enabled ?? false) if (features.Modules?.ProjectManagement?.Enabled ?? false)
@ -1822,6 +1816,19 @@ namespace Marco.Pms.Services.Controllers
return ApiResponse<SubscriptionPlanVM>.SuccessResponse(VM, "Success", 200); return ApiResponse<SubscriptionPlanVM>.SuccessResponse(VM, "Success", 200);
} }
private async Task ClearPermissionForTenant()
{
await using var _context = await _dbContextFactory.CreateDbContextAsync();
using var scope = _serviceScopeFactory.CreateScope();
var _cache = scope.ServiceProvider.GetRequiredService<CacheUpdateHelper>();
var _cacheLogger = scope.ServiceProvider.GetRequiredService<ILoggingService>();
var employeeIds = await _context.Employees.Where(e => e.TenantId == tenantId).Select(e => e.Id).ToListAsync();
await _cache.ClearAllEmployeesFromCacheByEmployeeIds(employeeIds, tenantId);
_cacheLogger.LogInfo("{EmployeeCount} number of employee deleted", employeeIds.Count);
}
#endregion #endregion
} }
} }

View File

@ -84,10 +84,7 @@ namespace Marco.Pms.Services.Helpers
Id = o.Id.ToString(), Id = o.Id.ToString(),
Name = o.Name, Name = o.Name,
ContactPerson = o.ContactPerson, ContactPerson = o.ContactPerson,
Email = o.Email, Email = o.Email
Address = o.Address,
ContactNumber = o.ContactNumber,
SPRID = o.SPRID
}) // Projection }) // Projection
.FirstOrDefaultAsync(); .FirstOrDefaultAsync();
}); });
@ -103,10 +100,7 @@ namespace Marco.Pms.Services.Helpers
Id = o.Id.ToString(), Id = o.Id.ToString(),
Name = o.Name, Name = o.Name,
ContactPerson = o.ContactPerson, ContactPerson = o.ContactPerson,
Email = o.Email, Email = o.Email
Address = o.Address,
ContactNumber = o.ContactNumber,
SPRID = o.SPRID
}) // Projection }) // Projection
.FirstOrDefaultAsync(); .FirstOrDefaultAsync();
}); });
@ -306,10 +300,7 @@ namespace Marco.Pms.Services.Helpers
Id = o.Id.ToString(), Id = o.Id.ToString(),
Name = o.Name, Name = o.Name,
ContactPerson = o.ContactPerson, ContactPerson = o.ContactPerson,
Email = o.Email, Email = o.Email
Address = o.Address,
ContactNumber = o.ContactNumber,
SPRID = o.SPRID
}) // Projection }) // Projection
.ToListAsync(); .ToListAsync();
}); });
@ -956,28 +947,6 @@ namespace Marco.Pms.Services.Helpers
_logger.LogError(ex, "Error occured while deleting all employees from Cache"); _logger.LogError(ex, "Error occured while deleting all employees from Cache");
} }
} }
public async Task ClearAllEmployeesFromCacheByOnlyEmployeeId(Guid employeeId)
{
try
{
var response = await _employeeCache.ClearAllEmployeesFromCacheByOnlyEmployeeId(employeeId);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error occured while deleting all employees from Cache");
}
}
public async Task ClearAllEmployeesFromCacheByTenantId(Guid tenantId)
{
try
{
var response = await _employeeCache.ClearAllEmployeesFromCacheByTenantId(tenantId);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error occured while deleting all employees from Cache");
}
}
public async Task ClearAllEmployees() public async Task ClearAllEmployees()
{ {
try try

View File

@ -71,11 +71,11 @@ namespace Marco.Pms.Services.Helpers
.Where(a => a.ProjectID == projectId && a.InTime != null && a.InTime.Value.Date == reportDate) .Where(a => a.ProjectID == projectId && a.InTime != null && a.InTime.Value.Date == reportDate)
.ToListAsync(); .ToListAsync();
var checkedInEmployeeIds = attendances.Select(a => a.EmployeeId).Distinct().ToHashSet(); var checkedInEmployeeIds = attendances.Select(a => a.EmployeeID).Distinct().ToHashSet();
var checkoutPendingIds = attendances.Where(a => a.OutTime == null).Select(a => a.EmployeeId).Distinct().ToHashSet(); var checkoutPendingIds = attendances.Where(a => a.OutTime == null).Select(a => a.EmployeeID).Distinct().ToHashSet();
var regularizationIds = attendances var regularizationIds = attendances
.Where(a => a.Activity == ATTENDANCE_MARK_TYPE.REQUEST_REGULARIZE) .Where(a => a.Activity == ATTENDANCE_MARK_TYPE.REQUEST_REGULARIZE)
.Select(a => a.EmployeeId).Distinct().ToHashSet(); .Select(a => a.EmployeeID).Distinct().ToHashSet();
// Preload buildings, floors, areas // Preload buildings, floors, areas
List<BuildingMongoDBVM>? buildings = null; List<BuildingMongoDBVM>? buildings = null;
@ -190,7 +190,6 @@ namespace Marco.Pms.Services.Helpers
double totalCompletedWork = workItems.Sum(w => w.CompletedWork); double totalCompletedWork = workItems.Sum(w => w.CompletedWork);
var todayAssignedTasks = tasks.Where(t => t.AssignmentDate.Date == reportDate).ToList(); var todayAssignedTasks = tasks.Where(t => t.AssignmentDate.Date == reportDate).ToList();
var todaysCompletedTasks = tasks.Where(t => t.AssignmentDate.Date == reportDate && t.ReportedById != null).ToList();
var reportPending = tasks.Where(t => t.ReportedDate == null).ToList(); var reportPending = tasks.Where(t => t.ReportedDate == null).ToList();
double totalPlannedTask = todayAssignedTasks.Sum(t => t.PlannedTask); double totalPlannedTask = todayAssignedTasks.Sum(t => t.PlannedTask);
@ -247,7 +246,7 @@ namespace Marco.Pms.Services.Helpers
// Attendance details // Attendance details
var performedAttendance = attendances.Select(att => var performedAttendance = attendances.Select(att =>
{ {
var alloc = projectAllocations.FirstOrDefault(p => p.EmployeeId == att.EmployeeId); var alloc = projectAllocations.FirstOrDefault(p => p.EmployeeId == att.EmployeeID);
var role = jobRoles.FirstOrDefault(r => r.Id == alloc?.JobRoleId); var role = jobRoles.FirstOrDefault(r => r.Id == alloc?.JobRoleId);
string name = $"{alloc?.Employee?.FirstName ?? ""} {alloc?.Employee?.LastName ?? ""}"; string name = $"{alloc?.Employee?.FirstName ?? ""} {alloc?.Employee?.LastName ?? ""}";
@ -264,17 +263,14 @@ namespace Marco.Pms.Services.Helpers
// Fill report // Fill report
statisticReport.TodaysAttendances = checkedInEmployeeIds.Count; statisticReport.TodaysAttendances = checkedInEmployeeIds.Count;
statisticReport.TotalEmployees = assignedEmployeeIds.Count; statisticReport.TotalEmployees = assignedEmployeeIds.Count;
statisticReport.AttendancePercentage = assignedEmployeeIds.Count > 0 ? (checkedInEmployeeIds.Count / assignedEmployeeIds.Count) * 100 : 0;
statisticReport.RegularizationPending = regularizationIds.Count; statisticReport.RegularizationPending = regularizationIds.Count;
statisticReport.CheckoutPending = checkoutPendingIds.Count; statisticReport.CheckoutPending = checkoutPendingIds.Count;
statisticReport.TotalPlannedWork = totalPlannedWork; statisticReport.TotalPlannedWork = totalPlannedWork;
statisticReport.TotalCompletedWork = totalCompletedWork; statisticReport.TotalCompletedWork = totalCompletedWork;
statisticReport.CompletionStatus = totalPlannedWork > 0 ? (totalCompletedWork / totalPlannedWork) * 100 : 0;
statisticReport.TotalPlannedTask = totalPlannedTask; statisticReport.TotalPlannedTask = totalPlannedTask;
statisticReport.TotalCompletedTask = totalCompletedTask; statisticReport.TotalCompletedTask = totalCompletedTask;
statisticReport.AttendancePercentage = totalCompletedTask > 0 ? (totalCompletedTask / totalPlannedTask) * 100 : 0; statisticReport.CompletionStatus = totalPlannedWork > 0 ? totalCompletedWork / totalPlannedWork : 0;
statisticReport.TodaysAssignTasks = todayAssignedTasks.Count; statisticReport.TodaysAssignTasks = todayAssignedTasks.Count;
statisticReport.TodaysCompletedTasks = todaysCompletedTasks.Count;
statisticReport.ReportPending = reportPending.Count; statisticReport.ReportPending = reportPending.Count;
statisticReport.TeamOnSite = teamOnSite; statisticReport.TeamOnSite = teamOnSite;
statisticReport.PerformedTasks = performedTasks; statisticReport.PerformedTasks = performedTasks;
@ -330,11 +326,11 @@ namespace Marco.Pms.Services.Helpers
.Where(a => a.ProjectID == projectId && a.InTime != null && a.InTime.Value.Date == reportDate) .Where(a => a.ProjectID == projectId && a.InTime != null && a.InTime.Value.Date == reportDate)
.ToListAsync(); .ToListAsync();
var checkedInEmployeeIds = attendances.Select(a => a.EmployeeId).Distinct().ToHashSet(); var checkedInEmployeeIds = attendances.Select(a => a.EmployeeID).Distinct().ToHashSet();
var checkoutPendingIds = attendances.Where(a => a.OutTime == null).Select(a => a.EmployeeId).Distinct().ToHashSet(); var checkoutPendingIds = attendances.Where(a => a.OutTime == null).Select(a => a.EmployeeID).Distinct().ToHashSet();
var regularizationIds = attendances var regularizationIds = attendances
.Where(a => a.Activity == ATTENDANCE_MARK_TYPE.REQUEST_REGULARIZE) .Where(a => a.Activity == ATTENDANCE_MARK_TYPE.REQUEST_REGULARIZE)
.Select(a => a.EmployeeId).Distinct().ToHashSet(); .Select(a => a.EmployeeID).Distinct().ToHashSet();
// Preload buildings, floors, areas // Preload buildings, floors, areas
List<BuildingMongoDBVM>? buildings = null; List<BuildingMongoDBVM>? buildings = null;
@ -505,7 +501,7 @@ namespace Marco.Pms.Services.Helpers
// Attendance details // Attendance details
var performedAttendance = attendances.Select(att => var performedAttendance = attendances.Select(att =>
{ {
var alloc = projectAllocations.FirstOrDefault(p => p.EmployeeId == att.EmployeeId); var alloc = projectAllocations.FirstOrDefault(p => p.EmployeeId == att.EmployeeID);
var role = jobRoles.FirstOrDefault(r => r.Id == alloc?.JobRoleId); var role = jobRoles.FirstOrDefault(r => r.Id == alloc?.JobRoleId);
string name = $"{alloc?.Employee?.FirstName ?? ""} {alloc?.Employee?.LastName ?? ""}"; string name = $"{alloc?.Employee?.FirstName ?? ""} {alloc?.Employee?.LastName ?? ""}";

View File

@ -37,8 +37,7 @@ namespace MarcoBMS.Services.Helpers
// --- Step 1: Define the subquery using the main thread's context --- // --- Step 1: Define the subquery using the main thread's context ---
// This is safe because the query is not executed yet. // This is safe because the query is not executed yet.
var employeeRoleIdsQuery = _context.EmployeeRoleMappings var employeeRoleIdsQuery = _context.EmployeeRoleMappings
.AsNoTracking() .Where(erm => erm.EmployeeId == EmployeeId && erm.IsEnabled)
.Where(erm => erm.EmployeeId == EmployeeId && erm.IsEnabled && erm.TenantId == tenantId)
.Select(erm => erm.RoleId); .Select(erm => erm.RoleId);
// --- Step 2: Asynchronously update the cache using the DbContextFactory --- // --- Step 2: Asynchronously update the cache using the DbContextFactory ---
@ -51,8 +50,7 @@ namespace MarcoBMS.Services.Helpers
// Now, re-create and execute the query using this new, isolated context. // Now, re-create and execute the query using this new, isolated context.
var roleIds = await contextForCache.EmployeeRoleMappings var roleIds = await contextForCache.EmployeeRoleMappings
.AsNoTracking() .Where(erm => erm.EmployeeId == EmployeeId && erm.IsEnabled)
.Where(erm => erm.EmployeeId == EmployeeId && erm.IsEnabled && erm.TenantId == tenantId)
.Select(erm => erm.RoleId) .Select(erm => erm.RoleId)
.ToListAsync(); .ToListAsync();
@ -75,12 +73,9 @@ namespace MarcoBMS.Services.Helpers
var roleIds = await employeeRoleIdsQuery.ToListAsync(); var roleIds = await employeeRoleIdsQuery.ToListAsync();
var permissionIds = await _context.RolePermissionMappings var permissionIds = await _context.RolePermissionMappings
.AsNoTracking()
.Where(rp => roleIds.Contains(rp.ApplicationRoleId)).Select(rp => rp.FeaturePermissionId).ToListAsync(); .Where(rp => roleIds.Contains(rp.ApplicationRoleId)).Select(rp => rp.FeaturePermissionId).ToListAsync();
var permissions = await _context.FeaturePermissions var permissions = await _context.FeaturePermissions.Include(f => f.Feature)
.AsNoTracking()
.Include(f => f.Feature)
.Where(fp => permissionIds.Contains(fp.Id)) .Where(fp => permissionIds.Contains(fp.Id))
.Distinct() .Distinct()
.ToListAsync(); .ToListAsync();

View File

@ -36,7 +36,7 @@ namespace MarcoBMS.Services.Helpers
var tenantId = _httpContextAccessor.HttpContext?.User.FindFirst("TenantId")?.Value; var tenantId = _httpContextAccessor.HttpContext?.User.FindFirst("TenantId")?.Value;
if (tenantId != null) if (tenantId != null)
{ {
return await _context.Tenants.AsNoTracking().FirstOrDefaultAsync(t => t.Id == Guid.Parse(tenantId)); return await _context.Tenants.FirstOrDefaultAsync(t => t.Id == Guid.Parse(tenantId));
} }
return null; return null;
} }
@ -54,7 +54,7 @@ namespace MarcoBMS.Services.Helpers
{ {
var user = await GetCurrentUserAsync(); var user = await GetCurrentUserAsync();
if (user == null) return new Employee { }; if (user == null) return new Employee { };
var Employee = await _context.Employees.AsNoTracking().Include(a => a.ApplicationUser).Include(e => e.JobRole).FirstOrDefaultAsync(e => e.ApplicationUserId == user.Id && e.IsActive); var Employee = await _context.Employees.Include(e => e.JobRole).FirstOrDefaultAsync(e => e.ApplicationUserId == user.Id && e.IsActive);
return Employee ?? new Employee { }; return Employee ?? new Employee { };
} }

View File

@ -1,8 +1,10 @@
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]
public class MarcoHub : Hub public class MarcoHub : Hub
{ {
private readonly ILoggingService _logger; private readonly ILoggingService _logger;

View File

@ -47,13 +47,6 @@ namespace Marco.Pms.Services.MappingProfiles
CreateMap<CreateOrganizationDto, Organization>(); CreateMap<CreateOrganizationDto, Organization>();
CreateMap<Organization, OrganizationVM>(); CreateMap<Organization, OrganizationVM>();
CreateMap<Organization, BasicOrganizationVm>(); CreateMap<Organization, BasicOrganizationVm>();
CreateMap<Organization, OrganizationDetailsVM>();
CreateMap<OrganizationMongoDB, BasicOrganizationVm>()
.ForMember(
dest => dest.Id,
// Explicitly and safely convert string Id to Guid Id
opt => opt.MapFrom(src => new Guid(src.Id))
);
#endregion #endregion
@ -128,7 +121,6 @@ namespace Marco.Pms.Services.MappingProfiles
CreateMap<Project, ProjectListVM>(); CreateMap<Project, ProjectListVM>();
CreateMap<Project, ProjectDto>(); CreateMap<Project, ProjectDto>();
CreateMap<ProjectMongoDB, ProjectListVM>(); CreateMap<ProjectMongoDB, ProjectListVM>();
CreateMap<ProjectServiceMapping, ProjectServiceMappingVM>();
CreateMap<ProjectMongoDB, ProjectVM>() CreateMap<ProjectMongoDB, ProjectVM>()
.ForMember( .ForMember(
dest => dest.Id, dest => dest.Id,
@ -379,27 +371,6 @@ namespace Marco.Pms.Services.MappingProfiles
#endregion #endregion
#region ======================================================= Contact Category Master =======================================================
CreateMap<CreateContactCategoryDto, ContactCategoryMaster>();
CreateMap<UpdateContactCategoryDto, ContactCategoryMaster>();
CreateMap<ContactCategoryMaster, ContactCategoryVM>();
#endregion
#region ======================================================= Contact Tag Master =======================================================
CreateMap<CreateContactTagDto, ContactTagMaster>();
CreateMap<UpdateContactTagDto, ContactTagMaster>();
CreateMap<ContactTagMaster, ContactTagVM>();
#endregion
#region ======================================================= Expenses Status Master =======================================================
#endregion
#region ======================================================= Expenses Status Master =======================================================
#endregion
#region ======================================================= Expenses Status Master =======================================================
#endregion
#region ======================================================= Expenses Status Master =======================================================
#endregion
#region ======================================================= Expenses Status Master =======================================================
#endregion
#endregion #endregion
#region ======================================================= Document ======================================================= #region ======================================================= Document =======================================================

View File

@ -956,14 +956,10 @@ namespace Marco.Pms.Services.Service
try try
{ {
var contact = _mapper.Map<Contact>(createContact); var contact = _mapper.Map<Contact>(createContact);
if (string.IsNullOrWhiteSpace(createContact.Description)) if (string.IsNullOrWhiteSpace(createContact.Name))
{ {
contact.Description = string.Empty; contact.Description = string.Empty;
} }
if (string.IsNullOrWhiteSpace(createContact.Designation))
{
contact.Designation = string.Empty;
}
contact.CreatedAt = DateTime.UtcNow; contact.CreatedAt = DateTime.UtcNow;
contact.CreatedById = loggedInEmployeeId; contact.CreatedById = loggedInEmployeeId;
contact.TenantId = tenantId; contact.TenantId = tenantId;
@ -2035,11 +2031,9 @@ namespace Marco.Pms.Services.Service
try try
{ {
var (hasAdminPermission, hasManagerPermission, hasUserPermission) = await CheckPermissionsAsync(loggedInEmployee.Id); 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 bucketIds = await _context.ContactBucketMappings.AsNoTracking().Where(cb => cb.ContactId == noteDto.ContactId).Select(cb => cb.BucketId).ToListAsync(); if (!hasContactAccess)
var hasContactAccess = await _context.EmployeeBucketMappings.AsNoTracking().AnyAsync(eb => bucketIds.Contains(eb.BucketId) && eb.EmployeeId == loggedInEmployee.Id);
if (!hasAdminPermission && !hasContactAccess)
{ {
_logger.LogWarning("Employee {EmployeeId} does not have permission to delete contact {ContactId}", _logger.LogWarning("Employee {EmployeeId} does not have permission to delete contact {ContactId}",
loggedInEmployee.Id, noteDto.ContactId); loggedInEmployee.Id, noteDto.ContactId);
@ -2271,11 +2265,9 @@ 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 hasContactAccess = await _context.EmployeeBucketMappings.AnyAsync(eb => bucketIds.Contains(eb.BucketId) && eb.EmployeeId == loggedInEmployee.Id);
var bucketIds = await _context.ContactBucketMappings.AsNoTracking().Where(cb => cb.ContactId == note.ContactId).Select(cb => cb.BucketId).ToListAsync(); if (hasContactAccess)
var hasContactAccess = await _context.EmployeeBucketMappings.AsNoTracking().AnyAsync(eb => bucketIds.Contains(eb.BucketId) && eb.EmployeeId == loggedInEmployee.Id);
if (!hasAdminPermission && !hasContactAccess)
{ {
_logger.LogWarning("Employee {EmployeeId} does not have permission to delete contact {ContactId}", _logger.LogWarning("Employee {EmployeeId} does not have permission to delete contact {ContactId}",
loggedInEmployee.Id, note.ContactId); loggedInEmployee.Id, note.ContactId);

View File

@ -23,7 +23,6 @@ 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
@ -120,12 +119,8 @@ namespace Marco.Pms.Services.Service
// 2. --- Deserialize Filter and Apply --- // 2. --- Deserialize Filter and Apply ---
ExpensesFilter? expenseFilter = TryDeserializeFilter(filter); ExpensesFilter? expenseFilter = TryDeserializeFilter(filter);
//var (totalPages, totalCount, cacheList) = await _cache.GetExpenseListAsync(tenantId, loggedInEmployeeId, hasViewAllPermissionTask.Result, hasViewSelfPermissionTask.Result, var (totalPages, totalCount, cacheList) = await _cache.GetExpenseListAsync(tenantId, loggedInEmployeeId, hasViewAllPermissionTask.Result, hasViewSelfPermissionTask.Result,
// pageNumber, pageSize, expenseFilter, searchString); pageNumber, pageSize, expenseFilter, searchString);
List<ExpenseDetailsMongoDB>? cacheList = null;
var totalPages = 0;
var totalCount = 0;
// 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.
@ -273,8 +268,7 @@ namespace Marco.Pms.Services.Service
{ {
try try
{ {
//ExpenseDetailsMongoDB? expenseDetails = await _cache.GetExpenseDetailsById(id, tenantId); var expenseDetails = await _cache.GetExpenseDetailsById(id, tenantId);
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.AsNoTracking().FirstOrDefaultAsync(e => e.Id == id && e.TenantId == tenantId);
@ -319,13 +313,6 @@ 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);
@ -495,15 +482,6 @@ 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();
@ -523,7 +501,10 @@ namespace Marco.Pms.Services.Service
// Await all prerequisite checks at once. // Await all prerequisite checks at once.
await Task.WhenAll(hasUploadPermissionTask, hasProjectPermissionTask, projectTask, expenseTypeTask, paymentModeTask, statusMappingTask, paidByTask, expenseUIdTask); await Task.WhenAll(
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 || !await hasProjectPermissionTask)
@ -533,12 +514,11 @@ namespace Marco.Pms.Services.Service
} }
var validationErrors = new List<string>(); var validationErrors = new List<string>();
var project = projectTask.Result; var project = await projectTask;
var expenseType = expenseTypeTask.Result; var expenseType = await expenseTypeTask;
var paymentMode = paymentModeTask.Result; var paymentMode = await paymentModeTask;
var statusMapping = statusMappingTask.Result; var statusMapping = await statusMappingTask;
var paidBy = paidByTask.Result; 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");
@ -554,10 +534,9 @@ namespace Marco.Pms.Services.Service
_logger.LogWarning("Expense creation failed due to validation errors: {ValidationErrors}", errorMessage); _logger.LogWarning("Expense creation failed due to validation errors: {ValidationErrors}", errorMessage);
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;
@ -1100,13 +1079,6 @@ 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

File diff suppressed because it is too large Load Diff

View File

@ -79,7 +79,6 @@ namespace Marco.Pms.Services.Service
{ {
// Fetch permissions explicitly assigned to this employee in the project. // Fetch permissions explicitly assigned to this employee in the project.
var projectLevelPermissionIds = await _context.ProjectLevelPermissionMappings var projectLevelPermissionIds = await _context.ProjectLevelPermissionMappings
.AsNoTracking()
.Where(pl => pl.ProjectId == projectId.Value && pl.EmployeeId == employeeId) .Where(pl => pl.ProjectId == projectId.Value && pl.EmployeeId == employeeId)
.Select(pl => pl.PermissionId) .Select(pl => pl.PermissionId)
.ToListAsync(); .ToListAsync();
@ -98,7 +97,6 @@ namespace Marco.Pms.Services.Service
// Get all feature permissions under those modules where the user didn't have explicit project-level grants. // Get all feature permissions under those modules where the user didn't have explicit project-level grants.
var allOverriddenPermissions = await _context.FeaturePermissions var allOverriddenPermissions = await _context.FeaturePermissions
.AsNoTracking()
.Where(fp => projectLevelModuleIds.Contains(fp.FeatureId) && .Where(fp => projectLevelModuleIds.Contains(fp.FeatureId) &&
!projectLevelPermissionIds.Contains(fp.Id)) !projectLevelPermissionIds.Contains(fp.Id))
.Select(fp => fp.Id) .Select(fp => fp.Id)
@ -140,12 +138,12 @@ namespace Marco.Pms.Services.Service
var hasPermission = await HasPermission(PermissionsMaster.ManageProject, employeeId); var hasPermission = await HasPermission(PermissionsMaster.ManageProject, employeeId);
if (hasPermission) if (hasPermission)
{ {
var projects = await _context.Projects.AsNoTracking().Where(c => c.TenantId == LoggedInEmployee.TenantId).ToListAsync(); var projects = await _context.Projects.Where(c => c.TenantId == LoggedInEmployee.TenantId).ToListAsync();
projectIds = projects.Select(p => p.Id).ToList(); projectIds = projects.Select(p => p.Id).ToList();
} }
else else
{ {
var allocation = await _context.ProjectAllocations.AsNoTracking().Where(c => c.EmployeeId == employeeId && c.IsActive).ToListAsync(); var allocation = await _context.ProjectAllocations.Where(c => c.EmployeeId == employeeId && c.IsActive).ToListAsync();
if (!allocation.Any()) if (!allocation.Any())
{ {
return false; return false;

View File

@ -716,7 +716,7 @@ namespace Marco.Pms.Services.Service
.Include(pa => pa.Employee) .Include(pa => pa.Employee)
.ThenInclude(e => e!.Organization) .ThenInclude(e => e!.Organization)
.Include(pa => pa.Service) .Include(pa => pa.Service)
.Where(pa => pa.ProjectId == projectId && pa.TenantId == tenantId); .Where(pa => pa.ProjectId == projectId && pa.TenantId == tenantId && pa.Service != null);
// Conditionally apply the filter for active allocations. // Conditionally apply the filter for active allocations.
if (!includeInactive) if (!includeInactive)
@ -738,7 +738,6 @@ namespace Marco.Pms.Services.Service
} }
var allocations = await projectAllocationQuery var allocations = await projectAllocationQuery
.Where(pa => pa.Service != null)
.Select(pa => new .Select(pa => new
{ {
// Fields from ProjectAllocation // Fields from ProjectAllocation
@ -754,10 +753,8 @@ namespace Marco.Pms.Services.Service
LastName = pa.Employee.LastName, LastName = pa.Employee.LastName,
MiddleName = pa.Employee.MiddleName, MiddleName = pa.Employee.MiddleName,
OrganizationId = pa.Employee.OrganizationId,
OrganizationName = pa.Employee.Organization!.Name, OrganizationName = pa.Employee.Organization!.Name,
ServiceId = pa.ServiceId,
ServiceName = pa.Service!.Name, ServiceName = pa.Service!.Name,
// Simplified JobRoleId logic: Use the allocation's role if it exists, otherwise fall back to the employee's default role. // Simplified JobRoleId logic: Use the allocation's role if it exists, otherwise fall back to the employee's default role.
@ -1013,12 +1010,6 @@ namespace Marco.Pms.Services.Service
} }
} }
var selectedEmployee = await _context.Employees.FirstOrDefaultAsync(e => e.Id == employeeId);
if (selectedEmployee == null)
{
_logger.LogWarning("Employee not found while assigning the projects to employee");
return ApiResponse<List<ProjectAllocationVM>>.ErrorResponse("Employee not found", "Employee not found", 404);
}
// --- Step 2: Fetch all relevant existing data in ONE database call --- // --- Step 2: Fetch all relevant existing data in ONE database call ---
var projectIdsInDto = allocationsDto.Select(p => p.ProjectId).ToList(); var projectIdsInDto = allocationsDto.Select(p => p.ProjectId).ToList();
@ -1034,11 +1025,6 @@ namespace Marco.Pms.Services.Service
var processedAllocations = new List<ProjectAllocation>(); var processedAllocations = new List<ProjectAllocation>();
var serviceProjects = await _context.ProjectOrgMappings
.Include(ps => ps.ProjectService)
.Where(ps => ps.ProjectService != null && projectIdsInDto.Contains(ps.ProjectService.ProjectId) &&
ps.OrganizationId == selectedEmployee.OrganizationId && ps.TenantId == tenantId).ToListAsync();
// --- Step 3: Process all logic IN MEMORY, tracking changes --- // --- Step 3: Process all logic IN MEMORY, tracking changes ---
foreach (var dto in allocationsDto) foreach (var dto in allocationsDto)
{ {
@ -1060,13 +1046,11 @@ namespace Marco.Pms.Services.Service
{ {
if (existingAllocation == null) if (existingAllocation == null)
{ {
var serviceProject = serviceProjects.FirstOrDefault(ps => ps.ProjectService != null && ps.ProjectService.ProjectId == dto.ProjectId);
// Create a new allocation because an active one doesn't exist. // Create a new allocation because an active one doesn't exist.
var newAllocation = _mapper.Map<ProjectAllocation>(dto); var newAllocation = _mapper.Map<ProjectAllocation>(dto);
newAllocation.EmployeeId = employeeId; newAllocation.EmployeeId = employeeId;
newAllocation.TenantId = tenantId; newAllocation.TenantId = tenantId;
newAllocation.AllocationDate = DateTime.UtcNow; newAllocation.AllocationDate = DateTime.UtcNow;
newAllocation.ServiceId = dto.ServiceId ?? serviceProject?.ProjectService?.ServiceId;
newAllocation.IsActive = true; newAllocation.IsActive = true;
_context.ProjectAllocations.Add(newAllocation); _context.ProjectAllocations.Add(newAllocation);
processedAllocations.Add(newAllocation); processedAllocations.Add(newAllocation);
@ -1182,190 +1166,6 @@ namespace Marco.Pms.Services.Service
500); 500);
} }
} }
public async Task<ApiResponse<object>> GetProjectTeamByServiceAndOrganization(Guid projectId, Guid? serviceId, Guid? organizationId, Guid tenantId, Employee loggedInEmployee)
{
using var scope = _serviceScopeFactory.CreateScope();
var _permission = scope.ServiceProvider.GetRequiredService<PermissionServices>();
var projectTask = Task.Run(async () =>
{
await using var context = await _dbContextFactory.CreateDbContextAsync();
return await context.Projects.FirstOrDefaultAsync(p => p.Id == projectId && p.TenantId == tenantId);
});
var tenantTask = Task.Run(async () =>
{
await using var context = await _dbContextFactory.CreateDbContextAsync();
return await context.Tenants.FirstOrDefaultAsync(t => t.Id == tenantId);
});
await Task.WhenAll(projectTask, tenantTask);
var project = projectTask.Result;
var tenant = tenantTask.Result;
if (project == null || tenant == null)
{
_logger.LogWarning("Project {ProjectId} not found in database for tenant {TenantId}", projectId, tenantId);
return ApiResponse<object>.ErrorResponse("Project not found", "Project not found", 404);
}
// Check if the logged-in employee has permission for the requested project
var hasProjectPermission = await _permission.HasProjectPermission(loggedInEmployee, projectId);
if (!hasProjectPermission)
{
_logger.LogWarning("User {EmployeeId} attempts to get employees for project {ProjectId} without permission", loggedInEmployee.Id, projectId);
return ApiResponse<object>.ErrorResponse("Access denied", "User does not have access to view the employees for this project", 403);
}
var organizationQuery = _context.ProjectOrgMappings
.Include(po => po.ProjectService)
.Where(po => po.ProjectService != null && po.ProjectService.ProjectId == projectId);
if (loggedInEmployee.OrganizationId != project.PMCId && loggedInEmployee.OrganizationId != project.PromoterId && loggedInEmployee.OrganizationId != tenant.OrganizationId)
{
organizationQuery = organizationQuery.Where(po => po.ParentOrganizationId == loggedInEmployee.OrganizationId || po.OrganizationId == loggedInEmployee.OrganizationId);
}
var organizationIds = await organizationQuery.Select(po => po.OrganizationId).ToListAsync();
if (loggedInEmployee.OrganizationId == project.PMCId || loggedInEmployee.OrganizationId == project.PromoterId || loggedInEmployee.OrganizationId == tenant.OrganizationId)
{
organizationIds.Add(project.PMCId);
organizationIds.Add(project.PromoterId);
organizationIds.Add(tenant.OrganizationId);
}
var projectAllocationQuery = _context.ProjectAllocations
.Include(pa => pa.Employee)
.ThenInclude(e => e!.JobRole)
.Where(pa => pa.ProjectId == projectId && pa.Employee != null && organizationIds.Contains(pa.Employee.OrganizationId));
if (serviceId.HasValue)
{
projectAllocationQuery = projectAllocationQuery.Where(pa => pa.ServiceId == serviceId);
}
if (organizationId.HasValue)
{
projectAllocationQuery = projectAllocationQuery.Where(pa => pa.Employee != null && pa.Employee.OrganizationId == organizationId);
}
var projectAllocations = await projectAllocationQuery
.ToListAsync();
var result = projectAllocations.Select(pa => _mapper.Map<EmployeeVM>(pa.Employee)).Distinct().ToList();
return ApiResponse<object>.SuccessResponse(result, "Employee list fetched successfully", 200);
}
public async Task<ApiResponse<object>> GetProjectTeamByServiceAndOrganizationAsync(
Guid projectId, Guid? serviceId, Guid? organizationId, Guid tenantId, Employee loggedInEmployee)
{
_logger.LogDebug("Started fetching project team. ProjectId: {ProjectId}, ServiceId: {ServiceId}, OrganizationId: {OrganizationId}, TenantId: {TenantId}, EmployeeId: {EmployeeId}",
projectId, serviceId ?? Guid.Empty, organizationId ?? Guid.Empty, tenantId, loggedInEmployee.Id);
try
{
// Use a single DbContext instance
using var scope = _serviceScopeFactory.CreateScope();
var _permission = scope.ServiceProvider.GetRequiredService<PermissionServices>();
// Fetch project and tenant entities in parallel
var projectTask = Task.Run(async () =>
{
await using var context = await _dbContextFactory.CreateDbContextAsync();
return await context.Projects.FirstOrDefaultAsync(p => p.Id == projectId && p.TenantId == tenantId);
});
var tenantTask = Task.Run(async () =>
{
await using var context = await _dbContextFactory.CreateDbContextAsync();
return await context.Tenants.FirstOrDefaultAsync(t => t.Id == tenantId);
});
await Task.WhenAll(projectTask, tenantTask);
var project = await projectTask;
var tenant = await tenantTask;
if (project == null || tenant == null)
{
_logger.LogWarning("Project {ProjectId} or Tenant {TenantId} not found in the database.", projectId, tenantId);
return ApiResponse<object>.ErrorResponse("Project or tenant not found", "Project or tenant record not found", 404);
}
// Check permission to view project team
var hasProjectPermission = await _permission.HasProjectPermission(loggedInEmployee, projectId);
if (!hasProjectPermission)
{
_logger.LogWarning("Access denied: User {EmployeeId} tried to get team for Project {ProjectId}", loggedInEmployee.Id, projectId);
return ApiResponse<object>.ErrorResponse("Access denied", "User does not have permission to view this project team", 403);
}
// Query ProjectOrgMappings for associated organizations
var organizationQuery = _context.ProjectOrgMappings
.Include(po => po.ProjectService)
.Where(po => po.ProjectService != null && po.ProjectService.ProjectId == projectId);
// Restrict organizations for non-PMC/Promoter users
if (loggedInEmployee.OrganizationId != project.PMCId &&
loggedInEmployee.OrganizationId != project.PromoterId &&
loggedInEmployee.OrganizationId != tenant.OrganizationId)
{
organizationQuery = organizationQuery.Where(po =>
po.ParentOrganizationId == loggedInEmployee.OrganizationId ||
po.OrganizationId == loggedInEmployee.OrganizationId);
}
var organizationIds = await organizationQuery.Select(po => po.OrganizationId).Distinct().ToListAsync();
// Add PMC, Promoter, Tenant organizations for privileged users
if (loggedInEmployee.OrganizationId == project.PMCId ||
loggedInEmployee.OrganizationId == project.PromoterId ||
loggedInEmployee.OrganizationId == tenant.OrganizationId)
{
organizationIds.Add(project.PMCId);
organizationIds.Add(project.PromoterId);
organizationIds.Add(tenant.OrganizationId);
}
organizationIds = organizationIds.Distinct().ToList();
// Build ProjectAllocation query for employees by service/organization if specified
var allocationQuery = _context.ProjectAllocations
.Include(pa => pa.Employee)
.ThenInclude(e => e!.JobRole)
.Where(pa => pa.ProjectId == projectId
&& pa.Employee != null
&& organizationIds.Contains(pa.Employee.OrganizationId));
if (serviceId.HasValue)
{
allocationQuery = allocationQuery.Where(pa => pa.ServiceId == serviceId);
}
if (organizationId.HasValue)
{
allocationQuery = allocationQuery.Where(pa => pa.Employee != null && pa.Employee.OrganizationId == organizationId);
}
var projectAllocations = await allocationQuery.ToListAsync();
// Map to distinct EmployeeVM results
var employeeList = projectAllocations
.Select(pa => _mapper.Map<EmployeeVM>(pa.Employee))
.Distinct()
.ToList();
_logger.LogInfo("Fetched {EmployeeCount} employees for Project {ProjectId}.", employeeList.Count, projectId);
return ApiResponse<object>.SuccessResponse(employeeList, "Employee list fetched successfully", 200);
}
catch (Exception ex)
{
_logger.LogError(ex, "Exception occurred while fetching project team for ProjectId: {ProjectId}", projectId);
return ApiResponse<object>.ErrorResponse("Internal error", "An error occurred while fetching the project team", 500);
}
}
#endregion #endregion
@ -2365,568 +2165,283 @@ namespace Marco.Pms.Services.Service
#region =================================================================== Assign Service APIs =================================================================== #region =================================================================== Assign Service APIs ===================================================================
/// <summary>
/// Retrieves the list of services assigned to a specific project based on the logged-in employee's organization and permissions.
/// </summary>
/// <param name="projectId">The unique identifier of the project.</param>
/// <param name="tenantId">The tenant identifier for multi-tenant data isolation.</param>
/// <param name="loggedInEmployee">The employee making the request, whose permissions are checked.</param>
/// <returns>An ApiResponse containing the list of assigned services or an error response.</returns>
public async Task<ApiResponse<object>> GetAssignedServiceToProjectAsync(Guid projectId, Guid tenantId, Employee loggedInEmployee) public async Task<ApiResponse<object>> GetAssignedServiceToProjectAsync(Guid projectId, Guid tenantId, Employee loggedInEmployee)
{ {
try try
{ {
using var scope = _serviceScopeFactory.CreateScope(); using var scope = _serviceScopeFactory.CreateScope();
var permissionService = scope.ServiceProvider.GetRequiredService<PermissionServices>(); var _permission = scope.ServiceProvider.GetRequiredService<PermissionServices>();
// Fetch the project to ensure it exists in the given tenant scope
var project = await _context.Projects
.AsNoTracking() // No changes are made, so use NoTracking for performance
.FirstOrDefaultAsync(p => p.Id == projectId && p.TenantId == tenantId);
var project = await _context.Projects.FirstOrDefaultAsync(p => p.Id == projectId && p.TenantId == tenantId);
if (project == null) if (project == null)
{ {
_logger.LogWarning("Project not found. ProjectId: {ProjectId}, TenantId: {TenantId}", projectId, tenantId);
return ApiResponse<object>.ErrorResponse("Project not found", "Project not found in database", 404); return ApiResponse<object>.ErrorResponse("Project not found", "Project not found in database", 404);
} }
// Verify logged-in employee has permission on the project var hasPermission = await _permission.HasProjectPermission(loggedInEmployee, projectId);
var hasPermission = await permissionService.HasProjectPermission(loggedInEmployee, projectId);
if (!hasPermission) if (!hasPermission)
{ {
_logger.LogWarning("Access DENIED for user {UserId} attempting to access project {ProjectId}.", loggedInEmployee.Id, projectId); _logger.LogWarning("Access DENIED for user {UserId} attempting to update project {ProjectId}.", loggedInEmployee.Id, projectId);
return ApiResponse<object>.ErrorResponse("Access Denied.", "You do not have permission to access this project.", 403); return ApiResponse<object>.ErrorResponse("Access Denied.", "You do not have permission to modify this project.", 403);
} }
List<ServiceMaster> assignedServices; List<ServiceMaster> services = new List<ServiceMaster>();
// Check if the logged-in employee's organization matches both Promoter and PMC of the project
if (project.PromoterId == loggedInEmployee.OrganizationId && project.PMCId == loggedInEmployee.OrganizationId) if (project.PromoterId == loggedInEmployee.OrganizationId && project.PMCId == loggedInEmployee.OrganizationId)
{ {
// Get all active services assigned directly to the project within the tenant var projectServices = await _context.ProjectServiceMappings
assignedServices = await _context.ProjectServiceMappings
.AsNoTracking()
.Include(ps => ps.Service) .Include(ps => ps.Service)
.Where(ps => ps.ProjectId == projectId && ps.IsActive && ps.TenantId == tenantId && ps.Service != null) .Where(ps => ps.ProjectId == projectId && ps.Service != null && ps.TenantId == tenantId && ps.IsActive)
.Select(ps => ps.Service!)
.Distinct()
.ToListAsync(); .ToListAsync();
services = projectServices.Select(ps => ps.Service!).Distinct().ToList();
_logger.LogInfo("User {UserId} requested all services for project {ProjectId} as Promoter and PMC.", loggedInEmployee.Id, projectId);
} }
else else
{ {
// Get the active project services mapped to the employee's organization for this project var orgProjectMapping = await _context.ProjectOrgMappings
assignedServices = await _context.ProjectOrgMappings
.AsNoTracking()
.Include(po => po.ProjectService) .Include(po => po.ProjectService)
.ThenInclude(ps => ps!.Service) .ThenInclude(ps => ps!.Service)
.Where(po => .Where(po => po.OrganizationId == loggedInEmployee.OrganizationId && po.ProjectService != null
po.OrganizationId == loggedInEmployee.OrganizationId && && po.ProjectService.IsActive && po.ProjectService.ProjectId == projectId && po.ProjectService.Service != null)
po.ProjectService != null &&
po.ProjectService.IsActive &&
po.ProjectService.ProjectId == projectId &&
po.ProjectService.Service != null)
.Select(po => po.ProjectService!.Service!)
.Distinct()
.ToListAsync(); .ToListAsync();
_logger.LogInfo("User {UserId} requested services for project {ProjectId} via organization mapping.", loggedInEmployee.Id, projectId); services = orgProjectMapping.Select(po => po.ProjectService!.Service!).Distinct().ToList();
} }
var response = _mapper.Map<List<ServiceMasterVM>>(services);
// Map entities to view models return ApiResponse<object>.SuccessResponse(response, "Successfully fetched the services for this project", 200);
var serviceViewModels = _mapper.Map<List<ServiceMasterVM>>(assignedServices);
return ApiResponse<object>.SuccessResponse(serviceViewModels, "Successfully fetched the services for this project", 200);
} }
catch (DbUpdateException dbEx) catch (DbUpdateException dbEx)
{ {
_logger.LogError(dbEx, "Database exception occurred while fetching assigned services to project {ProjectId} for tenant {TenantId}.", projectId, tenantId); //await transaction.RollbackAsync();
return ApiResponse<object>.ErrorResponse("Internal error", "A database exception occurred", 500);
_logger.LogError(dbEx, "Database Exception has been occured, While deassigning the sevice to the project");
return ApiResponse<object>.ErrorResponse("Internal error", "An database exception has been occured", 500);
} }
catch (Exception ex) catch (Exception ex)
{ {
_logger.LogError(ex, "Unexpected exception occurred while fetching assigned services to project {ProjectId} for tenant {TenantId}.", projectId, tenantId); _logger.LogError(ex, "Exception has been occured, While deassigning the sevice to the project");
return ApiResponse<object>.ErrorResponse("Internal error", "An unexpected internal exception occurred", 500); return ApiResponse<object>.ErrorResponse("Internal error", "An internal exception has been occured", 500);
} }
} }
/// <summary>
/// Assigns one or multiple services to a project with specified planned and actual dates.
/// Checks for project existence and employee permissions before assignment.
/// </summary>
/// <param name="model">Data transfer object containing project ID, list of service IDs, and planned dates.</param>
/// <param name="tenantId">Tenant identifier for proper multi-tenant separation.</param>
/// <param name="loggedInEmployee">The employee requesting the service assignment, used for permission checks.</param>
/// <returns>ApiResponse with assigned services info or error details.</returns>
public async Task<ApiResponse<object>> AssignServiceToProjectAsync(AssignServiceDto model, Guid tenantId, Employee loggedInEmployee) public async Task<ApiResponse<object>> AssignServiceToProjectAsync(AssignServiceDto model, Guid tenantId, Employee loggedInEmployee)
{ {
// Begin a transaction to ensure atomicity of assignments
await using var transaction = await _context.Database.BeginTransactionAsync(); await using var transaction = await _context.Database.BeginTransactionAsync();
try try
{ {
using var scope = _serviceScopeFactory.CreateScope(); using var scope = _serviceScopeFactory.CreateScope();
var permissionService = scope.ServiceProvider.GetRequiredService<PermissionServices>(); var _permission = scope.ServiceProvider.GetRequiredService<PermissionServices>();
// Validate project exists within the tenant
var project = await _context.Projects
.AsNoTracking()
.FirstOrDefaultAsync(p => p.Id == model.ProjectId && p.TenantId == tenantId);
var project = await _context.Projects.FirstOrDefaultAsync(p => p.Id == model.ProjectId && p.TenantId == tenantId);
if (project == null) if (project == null)
{ {
_logger.LogWarning("Project not found. ProjectId: {ProjectId}, TenantId: {TenantId}", model.ProjectId, tenantId);
return ApiResponse<object>.ErrorResponse("Project not found", "Project not found in database", 404); return ApiResponse<object>.ErrorResponse("Project not found", "Project not found in database", 404);
} }
// Validate permission for logged-in employee to assign services to project var hasPermission = await _permission.HasProjectPermission(loggedInEmployee, model.ProjectId);
var hasPermission = await permissionService.HasProjectPermission(loggedInEmployee, model.ProjectId);
if (!hasPermission) if (!hasPermission)
{ {
_logger.LogWarning("Access DENIED for user {UserId} attempting to assign services to project {ProjectId}.", loggedInEmployee.Id, model.ProjectId); _logger.LogWarning("Access DENIED for user {UserId} attempting to update project {ProjectId}.", loggedInEmployee.Id, model.ProjectId);
return ApiResponse<object>.ErrorResponse("Access Denied", "You do not have permission to modify this project.", 403); return ApiResponse<object>.ErrorResponse("Access Denied.", "You do not have permission to modify this project.", 403);
} }
// Fetch existing active project service mappings for the requested service IDs, within the same tenant var todaysDate = DateTime.UtcNow.Date;
var existingProjectServices = await _context.ProjectServiceMappings
.Where(sp => model.ServiceIds.Contains(sp.ServiceId) && sp.ProjectId == model.ProjectId && sp.IsActive)
.ToListAsync();
// Fetch service details for the provided service IDs within the tenant scope var projectServicesTask = Task.Run(async () =>
var services = await _context.ServiceMasters {
.Where(s => model.ServiceIds.Contains(s.Id) && s.TenantId == tenantId) await using var context = await _dbContextFactory.CreateDbContextAsync();
.ToListAsync(); return await context.ProjectServiceMappings
.Where(sp => model.ServiceIds.Contains(sp.ServiceId) && sp.ProjectId == model.ProjectId && sp.IsActive).ToListAsync();
});
// Current UTC timestamp for actual start date var serviceTask = Task.Run(async () =>
var currentUtc = DateTime.UtcNow; {
await using var context = await _dbContextFactory.CreateDbContextAsync();
return await context.ServiceMasters.Where(s => model.ServiceIds.Contains(s.Id) && s.TenantId == tenantId).ToListAsync();
});
await Task.WhenAll(projectServicesTask, serviceTask);
var projectServices = projectServicesTask.Result;
var services = serviceTask.Result;
// Add new project service mappings if not already present
foreach (var serviceId in model.ServiceIds) foreach (var serviceId in model.ServiceIds)
{ {
if (!existingProjectServices.Any(ps => ps.ServiceId == serviceId))
var projectService = projectServices.FirstOrDefault(ps => ps.ServiceId == serviceId);
if (projectService == null)
{ {
var newMapping = new ProjectServiceMapping projectService = new ProjectServiceMapping
{ {
ProjectId = project.Id, ProjectId = project.Id,
ServiceId = serviceId, ServiceId = serviceId,
TenantId = tenantId, TenantId = project.TenantId,
PlannedStartDate = model.PlannedStartDate, PlannedStartDate = model.PlannedStartDate,
PlannedEndDate = model.PlannedEndDate, PlannedEndDate = model.PlannedEndDate,
ActualStartDate = currentUtc, ActualStartDate = DateTime.UtcNow,
IsActive = true IsActive = true
}; };
_context.ProjectServiceMappings.Add(newMapping); _context.ProjectServiceMappings.Add(projectService);
_logger.LogInfo("Assigned service {ServiceId} to project {ProjectId} by user {UserId}.", serviceId, model.ProjectId, loggedInEmployee.Id);
}
else
{
_logger.LogInfo("Service {ServiceId} is already assigned and active for project {ProjectId}.", serviceId, model.ProjectId);
} }
} }
await _context.SaveChangesAsync(); await _context.SaveChangesAsync();
await transaction.CommitAsync(); await transaction.CommitAsync();
// Prepare response combining project and service data mapped to view models
var response = services.Select(s => new ProjectServiceVM var response = services.Select(s => new ProjectServiceVM
{ {
Project = _mapper.Map<BasicProjectVM>(project), Project = _mapper.Map<BasicProjectVM>(project),
Service = _mapper.Map<ServiceMasterVM>(s), Service = _mapper.Map<ServiceMasterVM>(s),
PlannedStartDate = model.PlannedStartDate, PlannedStartDate = model.PlannedStartDate,
PlannedEndDate = model.PlannedEndDate, PlannedEndDate = model.PlannedEndDate,
ActualStartDate = currentUtc ActualStartDate = DateTime.UtcNow
}).ToList(); });
return ApiResponse<object>.SuccessResponse(response, "Services has been assigned to the project", 200);
return ApiResponse<object>.SuccessResponse(response, "Services have been assigned to the project successfully", 200);
} }
catch (DbUpdateException dbEx) catch (DbUpdateException dbEx)
{ {
await transaction.RollbackAsync(); await transaction.RollbackAsync();
_logger.LogError(dbEx, "Database exception while assigning services to project {ProjectId} for tenant {TenantId} by user {UserId}.", model.ProjectId, tenantId, loggedInEmployee.Id);
return ApiResponse<object>.ErrorResponse("Internal error", "A database exception has occurred", 500); _logger.LogError(dbEx, "Database Exception has been occured, While assigning the sevice to the project");
return ApiResponse<object>.ErrorResponse("Internal error", "An database exception has been occured", 500);
} }
catch (Exception ex) catch (Exception ex)
{ {
await transaction.RollbackAsync(); _logger.LogError(ex, "Exception has been occured, While assigning the sevice to the project");
_logger.LogError(ex, "Unexpected exception while assigning services to project {ProjectId} for tenant {TenantId} by user {UserId}.", model.ProjectId, tenantId, loggedInEmployee.Id); return ApiResponse<object>.ErrorResponse("Internal error", "An internal exception has been occured", 500);
return ApiResponse<object>.ErrorResponse("Internal error", "An unexpected internal exception has occurred", 500);
} }
} }
/// <summary>
/// Deassigns specified services from a project by marking them inactive and setting actual end date.
/// Validates project existence and employee permission before making updates.
/// </summary>
/// <param name="model">Contains ProjectId and list of ServiceIds to deassign.</param>
/// <param name="tenantId">Tenant context for multi-tenant data isolation.</param>
/// <param name="loggedInEmployee">Employee executing the operation, used for permission checks.</param>
/// <returns>ApiResponse indicating success or failure.</returns>
public async Task<ApiResponse<object>> DeassignServiceToProjectAsync(DeassignServiceDto model, Guid tenantId, Employee loggedInEmployee) public async Task<ApiResponse<object>> DeassignServiceToProjectAsync(DeassignServiceDto model, Guid tenantId, Employee loggedInEmployee)
{ {
await using var transaction = await _context.Database.BeginTransactionAsync(); await using var transaction = await _context.Database.BeginTransactionAsync();
try try
{ {
using var scope = _serviceScopeFactory.CreateScope(); using var scope = _serviceScopeFactory.CreateScope();
var permissionService = scope.ServiceProvider.GetRequiredService<PermissionServices>(); var _permission = scope.ServiceProvider.GetRequiredService<PermissionServices>();
// Validate that project exists for given tenant
var project = await _context.Projects
.AsNoTracking()
.FirstOrDefaultAsync(p => p.Id == model.ProjectId && p.TenantId == tenantId);
var project = await _context.Projects.FirstOrDefaultAsync(p => p.Id == model.ProjectId && p.TenantId == tenantId);
if (project == null) if (project == null)
{ {
_logger.LogWarning("Project not found. ProjectId: {ProjectId}, TenantId: {TenantId}", model.ProjectId, tenantId);
return ApiResponse<object>.ErrorResponse("Project not found", "Project not found in database", 404); return ApiResponse<object>.ErrorResponse("Project not found", "Project not found in database", 404);
} }
// Verify permission to update project var hasPermission = await _permission.HasProjectPermission(loggedInEmployee, model.ProjectId);
var hasPermission = await permissionService.HasProjectPermission(loggedInEmployee, model.ProjectId);
if (!hasPermission) if (!hasPermission)
{ {
_logger.LogWarning("Access DENIED for user {UserId} trying to deassign services from project {ProjectId}.", loggedInEmployee.Id, model.ProjectId); _logger.LogWarning("Access DENIED for user {UserId} attempting to update project {ProjectId}.", loggedInEmployee.Id, model.ProjectId);
return ApiResponse<object>.ErrorResponse("Access Denied", "You do not have permission to modify this project.", 403); return ApiResponse<object>.ErrorResponse("Access Denied.", "You do not have permission to modify this project.", 403);
} }
var todaysDate = DateTime.UtcNow.Date;
// Fetch active project service mappings matching provided service IDs var projectServicesTask = Task.Run(async () =>
var projectServices = await _context.ProjectServiceMappings {
.Where(ps => model.ServiceIds.Contains(ps.ServiceId) && ps.ProjectId == model.ProjectId && ps.IsActive) await using var context = await _dbContextFactory.CreateDbContextAsync();
.ToListAsync(); return await context.ProjectServiceMappings
.AsNoTracking()
.Where(sp => model.ServiceIds.Contains(sp.ServiceId) && sp.ProjectId == model.ProjectId && sp.IsActive).ToListAsync();
});
var serviceTask = Task.Run(async () =>
{
await using var context = await _dbContextFactory.CreateDbContextAsync();
return await context.ServiceMasters.Where(s => model.ServiceIds.Contains(s.Id) && s.TenantId == tenantId).ToListAsync();
});
await Task.WhenAll(projectServicesTask, serviceTask);
var projectServices = projectServicesTask.Result;
var services = serviceTask.Result;
if (!projectServices.Any()) if (!projectServices.Any())
{ {
_logger.LogWarning("No matching active project service mappings found for deassignment. ProjectId: {ProjectId}, ServiceIds: {ServiceIds}", return ApiResponse<object>.ErrorResponse("Project Service mapping not found", "Project Service mapping not found in database", 404);
model.ProjectId, string.Join(",", model.ServiceIds));
return ApiResponse<object>.ErrorResponse("Project Service mapping not found", "No active service mappings found in database", 404);
} }
var currentUtc = DateTime.UtcNow; projectServices = projectServices.Select(ps =>
// Mark mappings as inactive and set actual end date to now
foreach (var ps in projectServices)
{ {
ps.IsActive = false; ps.IsActive = false;
ps.ActualEndDate = currentUtc; return ps;
} }).ToList();
_context.ProjectServiceMappings.UpdateRange(projectServices); _context.ProjectServiceMappings.UpdateRange(projectServices);
await _context.SaveChangesAsync(); await _context.SaveChangesAsync();
await transaction.CommitAsync(); await transaction.CommitAsync();
_logger.LogInfo("User {UserId} deassigned {Count} services from project {ProjectId}.", loggedInEmployee.Id, projectServices.Count, model.ProjectId); return ApiResponse<object>.SuccessResponse(new { }, "Services has been deassigned to the project", 200);
return ApiResponse<object>.SuccessResponse(new { }, "Services have been deassigned from the project successfully", 200);
} }
catch (DbUpdateException dbEx) catch (DbUpdateException dbEx)
{ {
await transaction.RollbackAsync(); await transaction.RollbackAsync();
_logger.LogError(dbEx, "Database exception occurred while deassigning services from project {ProjectId} by user {UserId}.", model.ProjectId, loggedInEmployee.Id);
return ApiResponse<object>.ErrorResponse("Internal error", "A database exception has occurred", 500); _logger.LogError(dbEx, "Database Exception has been occured, While deassigning the sevice to the project");
return ApiResponse<object>.ErrorResponse("Internal error", "An database exception has been occured", 500);
} }
catch (Exception ex) catch (Exception ex)
{ {
await transaction.RollbackAsync(); _logger.LogError(ex, "Exception has been occured, While deassigning the sevice to the project");
_logger.LogError(ex, "Unexpected exception occurred while deassigning services from project {ProjectId} by user {UserId}.", model.ProjectId, loggedInEmployee.Id); return ApiResponse<object>.ErrorResponse("Internal error", "An internal exception has been occured", 500);
return ApiResponse<object>.ErrorResponse("Internal error", "An unexpected internal exception has occurred", 500);
} }
} }
#endregion #endregion
#region =================================================================== Assign Organization APIs =================================================================== #region =================================================================== Assign Organization APIs ===================================================================
public async Task<ApiResponse<object>> GetAssignedOrganizationsToProjectAsync(Guid projectId, Guid tenantId, Employee loggedInEmployee) public async Task<ApiResponse<object>> GetAssignedOrganizationsToProjectAsync(Guid projectId, Guid tenantId, Employee loggedInEmployee)
{ {
_logger.LogDebug("Started fetching assigned organizations for ProjectId: {ProjectId} and TenantId: {TenantId} by user {UserId}",
projectId, tenantId, loggedInEmployee.Id);
try try
{ {
// Create a scoped PermissionServices instance for permission checks
using var scope = _serviceScopeFactory.CreateScope(); using var scope = _serviceScopeFactory.CreateScope();
var permissionService = scope.ServiceProvider.GetRequiredService<PermissionServices>(); var _permission = scope.ServiceProvider.GetRequiredService<PermissionServices>();
// Retrieve the project by projectId and tenantId var project = await _context.Projects.FirstOrDefaultAsync(p => p.Id == projectId && p.TenantId == tenantId);
var projectTask = Task.Run(async () => if (project == null)
{ {
await using var context = await _dbContextFactory.CreateDbContextAsync(); return ApiResponse<object>.ErrorResponse("Project not found", "Project not found in database", 404);
return await context.Projects.AsNoTracking().Include(p => p.Promoter).Include(p => p.PMC).FirstOrDefaultAsync(p => p.Id == projectId && p.TenantId == tenantId);
});
var tenantTask = Task.Run(async () =>
{
await using var context = await _dbContextFactory.CreateDbContextAsync();
return await context.Tenants.AsNoTracking().Include(t => t.Organization).FirstOrDefaultAsync(t => t.Id == tenantId);
});
var projectServiceTask = Task.Run(async () =>
{
await using var context = await _dbContextFactory.CreateDbContextAsync();
return await context.ProjectServiceMappings
.AsNoTracking()
.Include(ps => ps!.Service)
.Where(ps => ps.ProjectId == projectId && ps.TenantId == tenantId).ToListAsync();
});
await Task.WhenAll(projectTask, tenantTask, projectServiceTask);
var project = projectTask.Result;
var tenant = tenantTask.Result;
var projectService = projectServiceTask.Result;
if (project == null || tenant == null)
{
_logger.LogWarning("Project {ProjectId} not found in database for tenant {TenantId}", projectId, tenantId);
return ApiResponse<object>.ErrorResponse("Project not found", "Project not found", 404);
} }
// Check if the logged in employee has permission to access the project var hasPermission = await _permission.HasProjectPermission(loggedInEmployee, projectId);
var hasPermission = await permissionService.HasProjectPermission(loggedInEmployee, projectId);
if (!hasPermission) if (!hasPermission)
{ {
_logger.LogWarning("Access denied for user {UserId} on project {ProjectId}", loggedInEmployee.Id, projectId); _logger.LogWarning("Access DENIED for user {UserId} attempting to update project {ProjectId}.", loggedInEmployee.Id, projectId);
return ApiResponse<object>.ErrorResponse("Access Denied", "You do not have permission to access this project.", 403); return ApiResponse<object>.ErrorResponse("Access Denied.", "You do not have permission to modify this project.", 403);
} }
// Fetch all project-organization mappings with related service and organization data var projectOrgMapping = await _context.ProjectOrgMappings
var projectOrgMappingsQuery = _context.ProjectOrgMappings
.AsNoTracking()
.Include(po => po.ProjectService) .Include(po => po.ProjectService)
.ThenInclude(ps => ps!.Service) .ThenInclude(ps => ps!.Service)
.Include(po => po.AssignedBy)
.Include(po => po.OrganizationType)
.Include(po => po.Organization) .Include(po => po.Organization)
.Where(po => po.ProjectService != null .Where(po => po.ProjectService != null && po.ProjectService.ProjectId == projectId && po.TenantId == tenantId)
&& po.ProjectService.ProjectId == projectId
&& po.TenantId == tenantId);
if (loggedInEmployee.OrganizationId != project.PMCId && loggedInEmployee.OrganizationId != project.PromoterId && loggedInEmployee.OrganizationId != tenant.OrganizationId)
{
projectOrgMappingsQuery = projectOrgMappingsQuery.Where(po => po.ParentOrganizationId == loggedInEmployee.OrganizationId || po.OrganizationId == loggedInEmployee.OrganizationId);
}
var projectOrgMappings = await projectOrgMappingsQuery
.Distinct() .Distinct()
.ToListAsync(); .ToListAsync();
// Filter and map the data to the desired view model var response = projectOrgMapping.Where(po => po.Organization != null).Select(po => new ProjectOrganizationVM
var response = projectOrgMappings
.Where(po => po.Organization != null && po.OrganizationType != null)
.Select(po => new ProjectOrganizationVM
{
Id = po.Organization!.Id,
Name = po.Organization.Name,
Email = po.Organization.Email,
ContactPerson = po.Organization.ContactPerson,
SPRID = po.Organization.SPRID,
logoImage = po.Organization.logoImage,
OrganizationType = po.OrganizationType!.Name,
AssignedBy = _mapper.Map<BasicEmployeeVM>(po.AssignedBy),
Service = _mapper.Map<ServiceMasterVM>(po.ProjectService!.Service),
AssignedDate = po.AssignedDate,
CompletionDate = po.CompletionDate
})
.ToList();
if (loggedInEmployee.OrganizationId == project.PMCId || loggedInEmployee.OrganizationId == project.PromoterId || loggedInEmployee.OrganizationId == tenant.OrganizationId)
{ {
var pmc = project.PMC; Id = po.Organization!.Id,
var promoter = project.Promoter; Name = po.Organization.Name,
var organization = tenant.Organization; Email = po.Organization.Email,
ContactPerson = po.Organization.ContactPerson,
SPRID = po.Organization.SPRID,
logoImage = po.Organization.logoImage,
AssignedBy = _mapper.Map<BasicEmployeeVM>(po.AssignedBy),
Service = _mapper.Map<ServiceMasterVM>(po.ProjectService!.Service),
AssignedDate = po.AssignedDate,
CompletionDate = po.CompletionDate
}).ToList();
var employee = await _context.Employees.AsNoTracking().Include(e => e.JobRole).FirstOrDefaultAsync(e => e.OrganizationId == loggedInEmployee.OrganizationId && e.IsPrimary); return ApiResponse<object>.SuccessResponse(response, "Successfully fetched the list of organization assigned to the project", 200);
var assignedBy = _mapper.Map<BasicEmployeeVM>(employee);
var assignedDate = project.StartDate.HasValue ? project.StartDate.Value : DateTime.UtcNow;
var completionDate = project.EndDate;
if (!response.Any(r => r.Id == project.PMCId) && pmc != null)
{
response.Add(new ProjectOrganizationVM
{
Id = pmc.Id,
Name = pmc.Name,
Email = pmc.Email,
ContactPerson = pmc.ContactPerson,
SPRID = pmc.SPRID,
logoImage = pmc.logoImage,
OrganizationType = "PMC",
AssignedBy = assignedBy,
AssignedDate = assignedDate,
CompletionDate = completionDate
});
}
if (!response.Any(r => r.Id == project.PromoterId) && promoter != null)
{
response.Add(new ProjectOrganizationVM
{
Id = promoter.Id,
Name = promoter.Name,
Email = promoter.Email,
ContactPerson = promoter.ContactPerson,
SPRID = promoter.SPRID,
logoImage = promoter.logoImage,
OrganizationType = "Promotor",
AssignedBy = assignedBy,
AssignedDate = assignedDate,
CompletionDate = completionDate
});
}
if (!response.Any(r => r.Id == tenant.OrganizationId) && organization != null)
{
response.Add(new ProjectOrganizationVM
{
Id = organization.Id,
Name = organization.Name,
Email = organization.Email,
ContactPerson = organization.ContactPerson,
SPRID = organization.SPRID,
logoImage = organization.logoImage,
OrganizationType = "Primary",
AssignedBy = assignedBy,
AssignedDate = assignedDate,
CompletionDate = completionDate
});
}
}
_logger.LogInfo("Fetched {Count} assigned organizations for ProjectId: {ProjectId}", response.Count, projectId);
return ApiResponse<object>.SuccessResponse(response, "Successfully fetched the list of organizations assigned to the project", 200);
} }
catch (DbUpdateException dbEx) catch (DbUpdateException dbEx)
{ {
_logger.LogError(dbEx, "Database exception while fetching assigned organizations for ProjectId: {ProjectId}", projectId); //await transaction.RollbackAsync();
return ApiResponse<object>.ErrorResponse("Internal error", "A database exception occurred", 500);
_logger.LogError(dbEx, "Database Exception has been occured, While deassigning the sevice to the project");
return ApiResponse<object>.ErrorResponse("Internal error", "An database exception has been occured", 500);
} }
catch (Exception ex) catch (Exception ex)
{ {
_logger.LogError(ex, "Unhandled exception while fetching assigned organizations for ProjectId: {ProjectId}", projectId); _logger.LogError(ex, "Exception has been occured, While deassigning the sevice to the project");
return ApiResponse<object>.ErrorResponse("Internal error", "An internal exception occurred", 500); return ApiResponse<object>.ErrorResponse("Internal error", "An internal exception has been occured", 500);
} }
} }
public async Task<ApiResponse<object>> GetAssignedOrganizationsToProjectForDropdownAsync(Guid projectId, Guid tenantId, Employee loggedInEmployee)
{
_logger.LogDebug("Started fetching assigned organizations for ProjectId: {ProjectId} and TenantId: {TenantId} by user {UserId}",
projectId, tenantId, loggedInEmployee.Id);
try
{
// Create a scoped PermissionServices instance for permission checks
using var scope = _serviceScopeFactory.CreateScope();
var permissionService = scope.ServiceProvider.GetRequiredService<PermissionServices>();
// Retrieve the project by projectId and tenantId
var projectTask = Task.Run(async () =>
{
await using var context = await _dbContextFactory.CreateDbContextAsync();
return await context.Projects.AsNoTracking().Include(p => p.Promoter).Include(p => p.PMC).FirstOrDefaultAsync(p => p.Id == projectId && p.TenantId == tenantId);
});
var tenantTask = Task.Run(async () =>
{
await using var context = await _dbContextFactory.CreateDbContextAsync();
return await context.Tenants.AsNoTracking().Include(t => t.Organization).FirstOrDefaultAsync(t => t.Id == tenantId);
});
var projectServiceTask = Task.Run(async () =>
{
await using var context = await _dbContextFactory.CreateDbContextAsync();
return await context.ProjectServiceMappings
.AsNoTracking()
.Include(ps => ps!.Service)
.Where(ps => ps.ProjectId == projectId && ps.TenantId == tenantId).ToListAsync();
});
await Task.WhenAll(projectTask, tenantTask, projectServiceTask);
var project = projectTask.Result;
var tenant = tenantTask.Result;
var projectService = projectServiceTask.Result;
if (project == null || tenant == null)
{
_logger.LogWarning("Project {ProjectId} not found in database for tenant {TenantId}", projectId, tenantId);
return ApiResponse<object>.ErrorResponse("Project not found", "Project not found", 404);
}
// Check if the logged in employee has permission to access the project
var hasPermission = await permissionService.HasProjectPermission(loggedInEmployee, projectId);
if (!hasPermission)
{
_logger.LogWarning("Access denied for user {UserId} on project {ProjectId}", loggedInEmployee.Id, projectId);
return ApiResponse<object>.ErrorResponse("Access Denied", "You do not have permission to access this project.", 403);
}
// Fetch all project-organization mappings with related service and organization data
var projectOrgMappingsQuery = _context.ProjectOrgMappings
.AsNoTracking()
.Include(po => po.ProjectService)
.ThenInclude(ps => ps!.Service)
.Include(po => po.AssignedBy)
.Include(po => po.OrganizationType)
.Include(po => po.Organization)
.Where(po => po.ProjectService != null
&& po.ProjectService.ProjectId == projectId
&& po.TenantId == tenantId);
if (loggedInEmployee.OrganizationId != project.PMCId && loggedInEmployee.OrganizationId != project.PromoterId && loggedInEmployee.OrganizationId != tenant.OrganizationId)
{
projectOrgMappingsQuery = projectOrgMappingsQuery.Where(po => po.ParentOrganizationId == loggedInEmployee.OrganizationId || po.OrganizationId == loggedInEmployee.OrganizationId);
}
var projectOrgMappings = await projectOrgMappingsQuery
.Distinct()
.ToListAsync();
var organizations = projectOrgMappings.Select(po => po.Organization!).ToList();
if (loggedInEmployee.OrganizationId == project.PMCId || loggedInEmployee.OrganizationId == project.PromoterId || loggedInEmployee.OrganizationId == tenant.OrganizationId)
{
var pmc = project.PMC;
var promoter = project.Promoter;
var organization = tenant.Organization;
if (!organizations.Any(r => r.Id == project.PMCId) && pmc != null)
{
organizations.Add(pmc);
}
if (!organizations.Any(r => r.Id == project.PromoterId) && promoter != null)
{
organizations.Add(promoter);
}
if (!organizations.Any(r => r.Id == tenant.OrganizationId) && organization != null)
{
organizations.Add(organization);
}
}
organizations = organizations.DistinctBy(o => o.Id).ToList();
// Filter and map the data to the desired view model
var response = organizations
.Select(o => new ProjectOrganizationVM
{
Id = o.Id,
Name = o.Name,
SPRID = 0
})
.ToList();
_logger.LogInfo("Fetched {Count} assigned organizations for ProjectId: {ProjectId}", response.Count, projectId);
return ApiResponse<object>.SuccessResponse(response, "Successfully fetched the list of organizations assigned to the project", 200);
}
catch (DbUpdateException dbEx)
{
_logger.LogError(dbEx, "Database exception while fetching assigned organizations for ProjectId: {ProjectId}", projectId);
return ApiResponse<object>.ErrorResponse("Internal error", "A database exception occurred", 500);
}
catch (Exception ex)
{
_logger.LogError(ex, "Unhandled exception while fetching assigned organizations for ProjectId: {ProjectId}", projectId);
return ApiResponse<object>.ErrorResponse("Internal error", "An internal exception occurred", 500);
}
}
#endregion #endregion
@ -2945,14 +2460,9 @@ namespace Marco.Pms.Services.Service
public async Task<List<ProjectAllocation>> GetTeamByProject(Guid TenantId, Guid ProjectId, Guid? OrganizationId, bool IncludeInactive) public async Task<List<ProjectAllocation>> GetTeamByProject(Guid TenantId, Guid ProjectId, Guid? OrganizationId, bool IncludeInactive)
{ {
var projectAllocationQuery = _context.ProjectAllocations var projectAllocationQuery = _context.ProjectAllocations
.Include(pa => pa.Project) .Include(pa => pa.Employee)
.Include(pa => pa.Employee) .ThenInclude(e => e!.Organization)
.ThenInclude(e => e!.Organization) .Where(pa => pa.TenantId == TenantId && pa.ProjectId == ProjectId);
.Include(pa => pa.Employee)
.ThenInclude(e => e!.JobRole)
.Where(pa => pa.TenantId == TenantId &&
pa.ProjectId == ProjectId && pa.Project != null &&
pa.Employee != null && pa.Employee.Organization != null && pa.Employee.JobRole != null);
if (!IncludeInactive) if (!IncludeInactive)
{ {
projectAllocationQuery = projectAllocationQuery.Where(pa => pa.IsActive); projectAllocationQuery = projectAllocationQuery.Where(pa => pa.IsActive);

View File

@ -23,7 +23,6 @@ namespace Marco.Pms.Services.Service.ServiceInterfaces
Task<ApiResponse<object>> GetProjectsByEmployeeAsync(Guid employeeId, Guid tenantId, Employee loggedInEmployee); Task<ApiResponse<object>> GetProjectsByEmployeeAsync(Guid employeeId, Guid tenantId, Employee loggedInEmployee);
Task<ApiResponse<List<ProjectAllocationVM>>> AssigneProjectsToEmployeeAsync(List<ProjectsAllocationDto> projectAllocationDtos, Guid employeeId, Guid tenantId, Employee loggedInEmployee); Task<ApiResponse<List<ProjectAllocationVM>>> AssigneProjectsToEmployeeAsync(List<ProjectsAllocationDto> projectAllocationDtos, Guid employeeId, Guid tenantId, Employee loggedInEmployee);
Task<ApiResponse<object>> GetProjectByEmployeeBasicAsync(Guid employeeId, Guid tenantId, Employee loggedInEmployee); Task<ApiResponse<object>> GetProjectByEmployeeBasicAsync(Guid employeeId, Guid tenantId, Employee loggedInEmployee);
Task<ApiResponse<object>> GetProjectTeamByServiceAndOrganizationAsync(Guid projectId, Guid? serviceId, Guid? organizationId, Guid tenantId, Employee loggedInEmployee);
Task<ApiResponse<object>> GetInfraDetailsAsync(Guid projectId, Guid? serviceId, Guid tenantId, Employee loggedInEmployee); Task<ApiResponse<object>> GetInfraDetailsAsync(Guid projectId, Guid? serviceId, Guid tenantId, Employee loggedInEmployee);
Task<ApiResponse<object>> GetWorkItemsAsync(Guid workAreaId, Guid? serviceId, Guid tenantId, Employee loggedInEmployee); Task<ApiResponse<object>> GetWorkItemsAsync(Guid workAreaId, Guid? serviceId, Guid tenantId, Employee loggedInEmployee);
@ -49,7 +48,6 @@ namespace Marco.Pms.Services.Service.ServiceInterfaces
Task<ApiResponse<object>> DeassignServiceToProjectAsync(DeassignServiceDto model, Guid tenantId, Employee loggedInEmployee); Task<ApiResponse<object>> DeassignServiceToProjectAsync(DeassignServiceDto model, Guid tenantId, Employee loggedInEmployee);
Task<ApiResponse<object>> GetAssignedOrganizationsToProjectAsync(Guid projectId, Guid tenantId, Employee loggedInEmployee); Task<ApiResponse<object>> GetAssignedOrganizationsToProjectAsync(Guid projectId, Guid tenantId, Employee loggedInEmployee);
Task<ApiResponse<object>> GetAssignedOrganizationsToProjectForDropdownAsync(Guid projectId, Guid tenantId, Employee loggedInEmployee);
} }
} }