Project_Branch_Management #155
@ -213,9 +213,15 @@ namespace Marco.Pms.DataAccess.Data
|
||||
public DbSet<ServiceProjectServiceMapping> ServiceProjectServiceMapping { get; set; }
|
||||
public DbSet<TeamRoleMaster> TeamRoleMasters { get; set; }
|
||||
public DbSet<ServiceProjectTag> ServiceProjectTags { get; set; }
|
||||
//public DbSet<TalkingPoint> TalkingPoints { get; set; }
|
||||
//public DbSet<TalkingPointAttachment> TalkingPointAttachments { get; set; }
|
||||
public DbSet<ServiceProjectTagMapping> ServiceProjectTagMappings { get; set; }
|
||||
public DbSet<ServiceProjectAllocation> ServiceProjectAllocations { get; set; }
|
||||
|
||||
#region ======================================================= Project Branch =======================================================
|
||||
public DbSet<ProjectBranch> ProjectBranches { get; set; }
|
||||
#endregion
|
||||
|
||||
#region ======================================================= Job =======================================================
|
||||
public DbSet<JobTicket> JobTickets { get; set; }
|
||||
public DbSet<JobStatus> JobStatus { get; set; }
|
||||
@ -1280,8 +1286,8 @@ namespace Marco.Pms.DataAccess.Data
|
||||
new JobStatus { Id = Guid.Parse("32d76a02-8f44-4aa0-9b66-c3716c45a918"), Name = "New", DisplayName = "New", Level = 1 },
|
||||
new JobStatus { Id = Guid.Parse("cfa1886d-055f-4ded-84c6-42a2a8a14a66"), Name = "Assigned", DisplayName = "Assigned", Level = 2 },
|
||||
new JobStatus { Id = Guid.Parse("5a6873a5-fed7-4745-a52f-8f61bf3bd72d"), Name = "In Progress", DisplayName = "In Progress", Level = 3 },
|
||||
new JobStatus { Id = Guid.Parse("aab71020-2fb8-44d9-9430-c9a7e9bf33b0"), Name = "Review", DisplayName = "Review", Level = 4 },
|
||||
new JobStatus { Id = Guid.Parse("ed10ab57-dbaa-4ca5-8ecd-56745dcbdbd7"), Name = "Done", DisplayName = "Done", Level = 5 },
|
||||
new JobStatus { Id = Guid.Parse("aab71020-2fb8-44d9-9430-c9a7e9bf33b0"), Name = "Work Done", DisplayName = "Work Done", Level = 4 },
|
||||
new JobStatus { Id = Guid.Parse("ed10ab57-dbaa-4ca5-8ecd-56745dcbdbd7"), Name = "Review Done", DisplayName = "Review Done", Level = 5 },
|
||||
new JobStatus { Id = Guid.Parse("3ddeefb5-ae3c-4e10-a922-35e0a452bb69"), Name = "Closed", DisplayName = "Closed", Level = 6 },
|
||||
new JobStatus { Id = Guid.Parse("75a0c8b8-9c6a-41af-80bf-b35bab722eb2"), Name = "On Hold", DisplayName = "On Hold", Level = 7 }
|
||||
);
|
||||
|
||||
8895
Marco.Pms.DataAccess/Migrations/20251119065548_Added_ProjectBranches_Table.Designer.cs
generated
Normal file
8895
Marco.Pms.DataAccess/Migrations/20251119065548_Added_ProjectBranches_Table.Designer.cs
generated
Normal file
File diff suppressed because one or more lines are too long
@ -0,0 +1,122 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Marco.Pms.DataAccess.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class Added_ProjectBranches_Table : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.UpdateData(
|
||||
table: "Projects",
|
||||
keyColumn: "ContactPerson",
|
||||
keyValue: null,
|
||||
column: "ContactPerson",
|
||||
value: "");
|
||||
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "ContactPerson",
|
||||
table: "Projects",
|
||||
type: "longtext",
|
||||
nullable: false,
|
||||
oldClrType: typeof(string),
|
||||
oldType: "longtext",
|
||||
oldNullable: true)
|
||||
.Annotation("MySql:CharSet", "utf8mb4")
|
||||
.OldAnnotation("MySql:CharSet", "utf8mb4");
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "ProjectBranches",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
|
||||
BranchName = table.Column<string>(type: "longtext", nullable: false)
|
||||
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||
ProjectId = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
|
||||
ContactInformation = table.Column<string>(type: "longtext", nullable: false)
|
||||
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||
Address = table.Column<string>(type: "longtext", nullable: false)
|
||||
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||
BranchType = table.Column<string>(type: "longtext", nullable: false)
|
||||
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||
GoogleMapUrl = table.Column<string>(type: "longtext", nullable: true)
|
||||
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||
IsActive = table.Column<bool>(type: "tinyint(1)", nullable: false),
|
||||
CreatedAt = table.Column<DateTime>(type: "datetime(6)", nullable: false),
|
||||
CreatedById = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
|
||||
UpdatedAt = table.Column<DateTime>(type: "datetime(6)", nullable: true),
|
||||
UpdatedById = table.Column<Guid>(type: "char(36)", nullable: true, collation: "ascii_general_ci"),
|
||||
TenantId = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci")
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_ProjectBranches", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_ProjectBranches_Employees_CreatedById",
|
||||
column: x => x.CreatedById,
|
||||
principalTable: "Employees",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "FK_ProjectBranches_Employees_UpdatedById",
|
||||
column: x => x.UpdatedById,
|
||||
principalTable: "Employees",
|
||||
principalColumn: "Id");
|
||||
table.ForeignKey(
|
||||
name: "FK_ProjectBranches_ServiceProjects_ProjectId",
|
||||
column: x => x.ProjectId,
|
||||
principalTable: "ServiceProjects",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "FK_ProjectBranches_Tenants_TenantId",
|
||||
column: x => x.TenantId,
|
||||
principalTable: "Tenants",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
})
|
||||
.Annotation("MySql:CharSet", "utf8mb4");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ProjectBranches_CreatedById",
|
||||
table: "ProjectBranches",
|
||||
column: "CreatedById");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ProjectBranches_ProjectId",
|
||||
table: "ProjectBranches",
|
||||
column: "ProjectId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ProjectBranches_TenantId",
|
||||
table: "ProjectBranches",
|
||||
column: "TenantId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ProjectBranches_UpdatedById",
|
||||
table: "ProjectBranches",
|
||||
column: "UpdatedById");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "ProjectBranches");
|
||||
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "ContactPerson",
|
||||
table: "Projects",
|
||||
type: "longtext",
|
||||
nullable: true,
|
||||
oldClrType: typeof(string),
|
||||
oldType: "longtext")
|
||||
.Annotation("MySql:CharSet", "utf8mb4")
|
||||
.OldAnnotation("MySql:CharSet", "utf8mb4");
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
@ -0,0 +1,78 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Marco.Pms.DataAccess.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class Added_ProjectBranch_As_ForignKey_In_JobTickets_Table : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<Guid>(
|
||||
name: "ProjectBranchId",
|
||||
table: "JobTickets",
|
||||
type: "char(36)",
|
||||
nullable: true,
|
||||
collation: "ascii_general_ci");
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "JobStatus",
|
||||
keyColumn: "Id",
|
||||
keyValue: new Guid("aab71020-2fb8-44d9-9430-c9a7e9bf33b0"),
|
||||
columns: new[] { "DisplayName", "Name" },
|
||||
values: new object[] { "Work Done", "Work Done" });
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "JobStatus",
|
||||
keyColumn: "Id",
|
||||
keyValue: new Guid("ed10ab57-dbaa-4ca5-8ecd-56745dcbdbd7"),
|
||||
columns: new[] { "DisplayName", "Name" },
|
||||
values: new object[] { "Review Done", "Review Done" });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_JobTickets_ProjectBranchId",
|
||||
table: "JobTickets",
|
||||
column: "ProjectBranchId");
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_JobTickets_ProjectBranches_ProjectBranchId",
|
||||
table: "JobTickets",
|
||||
column: "ProjectBranchId",
|
||||
principalTable: "ProjectBranches",
|
||||
principalColumn: "Id");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_JobTickets_ProjectBranches_ProjectBranchId",
|
||||
table: "JobTickets");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_JobTickets_ProjectBranchId",
|
||||
table: "JobTickets");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "ProjectBranchId",
|
||||
table: "JobTickets");
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "JobStatus",
|
||||
keyColumn: "Id",
|
||||
keyValue: new Guid("aab71020-2fb8-44d9-9430-c9a7e9bf33b0"),
|
||||
columns: new[] { "DisplayName", "Name" },
|
||||
values: new object[] { "Review", "Review" });
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "JobStatus",
|
||||
keyColumn: "Id",
|
||||
keyValue: new Guid("ed10ab57-dbaa-4ca5-8ecd-56745dcbdbd7"),
|
||||
columns: new[] { "DisplayName", "Name" },
|
||||
values: new object[] { "Done", "Done" });
|
||||
}
|
||||
}
|
||||
}
|
||||
8909
Marco.Pms.DataAccess/Migrations/20251120051203_Added_IsArchive_In_JobTicket_Table.Designer.cs
generated
Normal file
8909
Marco.Pms.DataAccess/Migrations/20251120051203_Added_IsArchive_In_JobTicket_Table.Designer.cs
generated
Normal file
File diff suppressed because one or more lines are too long
@ -0,0 +1,29 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Marco.Pms.DataAccess.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class Added_IsArchive_In_JobTicket_Table : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "IsArchive",
|
||||
table: "JobTickets",
|
||||
type: "tinyint(1)",
|
||||
nullable: false,
|
||||
defaultValue: false);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "IsArchive",
|
||||
table: "JobTickets");
|
||||
}
|
||||
}
|
||||
}
|
||||
8899
Marco.Pms.DataAccess/Migrations/20251120111120_Removed_Project_ForignKey_From_Expenses_Table.Designer.cs
generated
Normal file
8899
Marco.Pms.DataAccess/Migrations/20251120111120_Removed_Project_ForignKey_From_Expenses_Table.Designer.cs
generated
Normal file
File diff suppressed because one or more lines are too long
@ -0,0 +1,39 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Marco.Pms.DataAccess.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class Removed_Project_ForignKey_From_Expenses_Table : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_Expenses_Projects_ProjectId",
|
||||
table: "Expenses");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_Expenses_ProjectId",
|
||||
table: "Expenses");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Expenses_ProjectId",
|
||||
table: "Expenses",
|
||||
column: "ProjectId");
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_Expenses_Projects_ProjectId",
|
||||
table: "Expenses",
|
||||
column: "ProjectId",
|
||||
principalTable: "Projects",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
@ -0,0 +1,38 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Marco.Pms.DataAccess.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class Removed_Project_ForignKey_From_PaymentRequest_Table : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_PaymentRequests_Projects_ProjectId",
|
||||
table: "PaymentRequests");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_PaymentRequests_ProjectId",
|
||||
table: "PaymentRequests");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_PaymentRequests_ProjectId",
|
||||
table: "PaymentRequests",
|
||||
column: "ProjectId");
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_PaymentRequests_Projects_ProjectId",
|
||||
table: "PaymentRequests",
|
||||
column: "ProjectId",
|
||||
principalTable: "Projects",
|
||||
principalColumn: "Id");
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
@ -0,0 +1,38 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Marco.Pms.DataAccess.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class Removed_Project_ForignKey_From_RecurringPayment_Table : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_RecurringPayments_Projects_ProjectId",
|
||||
table: "RecurringPayments");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_RecurringPayments_ProjectId",
|
||||
table: "RecurringPayments");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_RecurringPayments_ProjectId",
|
||||
table: "RecurringPayments",
|
||||
column: "ProjectId");
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_RecurringPayments_Projects_ProjectId",
|
||||
table: "RecurringPayments",
|
||||
column: "ProjectId",
|
||||
principalTable: "Projects",
|
||||
principalColumn: "Id");
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
@ -0,0 +1,39 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Marco.Pms.DataAccess.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class Removed_Project_ForignKey_From_ProjectContactMapping_Table : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_ContactProjectMappings_Projects_ProjectId",
|
||||
table: "ContactProjectMappings");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_ContactProjectMappings_ProjectId",
|
||||
table: "ContactProjectMappings");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ContactProjectMappings_ProjectId",
|
||||
table: "ContactProjectMappings",
|
||||
column: "ProjectId");
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_ContactProjectMappings_Projects_ProjectId",
|
||||
table: "ContactProjectMappings",
|
||||
column: "ProjectId",
|
||||
principalTable: "Projects",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -898,8 +898,6 @@ namespace Marco.Pms.DataAccess.Migrations
|
||||
|
||||
b.HasIndex("ContactId");
|
||||
|
||||
b.HasIndex("ProjectId");
|
||||
|
||||
b.HasIndex("TenantId");
|
||||
|
||||
b.ToTable("ContactProjectMappings");
|
||||
@ -2329,8 +2327,6 @@ namespace Marco.Pms.DataAccess.Migrations
|
||||
|
||||
b.HasIndex("ProcessedById");
|
||||
|
||||
b.HasIndex("ProjectId");
|
||||
|
||||
b.HasIndex("ReviewedById");
|
||||
|
||||
b.HasIndex("StatusId");
|
||||
@ -2798,8 +2794,6 @@ namespace Marco.Pms.DataAccess.Migrations
|
||||
|
||||
b.HasIndex("PaidById");
|
||||
|
||||
b.HasIndex("ProjectId");
|
||||
|
||||
b.HasIndex("RecurringPaymentId");
|
||||
|
||||
b.HasIndex("TenantId");
|
||||
@ -2926,8 +2920,6 @@ namespace Marco.Pms.DataAccess.Migrations
|
||||
|
||||
b.HasIndex("ExpenseCategoryId");
|
||||
|
||||
b.HasIndex("ProjectId");
|
||||
|
||||
b.HasIndex("StatusId");
|
||||
|
||||
b.HasIndex("TenantId");
|
||||
@ -4799,6 +4791,7 @@ namespace Marco.Pms.DataAccess.Migrations
|
||||
.HasColumnType("char(36)");
|
||||
|
||||
b.Property<string>("ContactPerson")
|
||||
.IsRequired()
|
||||
.HasColumnType("longtext");
|
||||
|
||||
b.Property<DateTime?>("EndDate")
|
||||
@ -5260,16 +5253,16 @@ namespace Marco.Pms.DataAccess.Migrations
|
||||
new
|
||||
{
|
||||
Id = new Guid("aab71020-2fb8-44d9-9430-c9a7e9bf33b0"),
|
||||
DisplayName = "Review",
|
||||
DisplayName = "Work Done",
|
||||
Level = 4,
|
||||
Name = "Review"
|
||||
Name = "Work Done"
|
||||
},
|
||||
new
|
||||
{
|
||||
Id = new Guid("ed10ab57-dbaa-4ca5-8ecd-56745dcbdbd7"),
|
||||
DisplayName = "Done",
|
||||
DisplayName = "Review Done",
|
||||
Level = 5,
|
||||
Name = "Done"
|
||||
Name = "Review Done"
|
||||
},
|
||||
new
|
||||
{
|
||||
@ -5493,6 +5486,12 @@ namespace Marco.Pms.DataAccess.Migrations
|
||||
b.Property<bool>("IsActive")
|
||||
.HasColumnType("tinyint(1)");
|
||||
|
||||
b.Property<bool>("IsArchive")
|
||||
.HasColumnType("tinyint(1)");
|
||||
|
||||
b.Property<Guid?>("ProjectBranchId")
|
||||
.HasColumnType("char(36)");
|
||||
|
||||
b.Property<Guid>("ProjectId")
|
||||
.HasColumnType("char(36)");
|
||||
|
||||
@ -5526,6 +5525,8 @@ namespace Marco.Pms.DataAccess.Migrations
|
||||
|
||||
b.HasIndex("CreatedById");
|
||||
|
||||
b.HasIndex("ProjectBranchId");
|
||||
|
||||
b.HasIndex("ProjectId");
|
||||
|
||||
b.HasIndex("StatusId");
|
||||
@ -5537,6 +5538,65 @@ namespace Marco.Pms.DataAccess.Migrations
|
||||
b.ToTable("JobTickets");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Marco.Pms.Model.ServiceProject.ProjectBranch", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("char(36)");
|
||||
|
||||
b.Property<string>("Address")
|
||||
.IsRequired()
|
||||
.HasColumnType("longtext");
|
||||
|
||||
b.Property<string>("BranchName")
|
||||
.IsRequired()
|
||||
.HasColumnType("longtext");
|
||||
|
||||
b.Property<string>("BranchType")
|
||||
.IsRequired()
|
||||
.HasColumnType("longtext");
|
||||
|
||||
b.Property<string>("ContactInformation")
|
||||
.IsRequired()
|
||||
.HasColumnType("longtext");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.Property<Guid>("CreatedById")
|
||||
.HasColumnType("char(36)");
|
||||
|
||||
b.Property<string>("GoogleMapUrl")
|
||||
.HasColumnType("longtext");
|
||||
|
||||
b.Property<bool>("IsActive")
|
||||
.HasColumnType("tinyint(1)");
|
||||
|
||||
b.Property<Guid>("ProjectId")
|
||||
.HasColumnType("char(36)");
|
||||
|
||||
b.Property<Guid>("TenantId")
|
||||
.HasColumnType("char(36)");
|
||||
|
||||
b.Property<DateTime?>("UpdatedAt")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.Property<Guid?>("UpdatedById")
|
||||
.HasColumnType("char(36)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("CreatedById");
|
||||
|
||||
b.HasIndex("ProjectId");
|
||||
|
||||
b.HasIndex("TenantId");
|
||||
|
||||
b.HasIndex("UpdatedById");
|
||||
|
||||
b.ToTable("ProjectBranches");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Marco.Pms.Model.ServiceProject.ServiceProject", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
@ -6803,12 +6863,6 @@ namespace Marco.Pms.DataAccess.Migrations
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("Marco.Pms.Model.Projects.Project", "Project")
|
||||
.WithMany()
|
||||
.HasForeignKey("ProjectId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("Marco.Pms.Model.TenantModels.Tenant", "Tenant")
|
||||
.WithMany()
|
||||
.HasForeignKey("TenantId")
|
||||
@ -6817,8 +6871,6 @@ namespace Marco.Pms.DataAccess.Migrations
|
||||
|
||||
b.Navigation("Contact");
|
||||
|
||||
b.Navigation("Project");
|
||||
|
||||
b.Navigation("Tenant");
|
||||
});
|
||||
|
||||
@ -7310,12 +7362,6 @@ namespace Marco.Pms.DataAccess.Migrations
|
||||
.WithMany()
|
||||
.HasForeignKey("ProcessedById");
|
||||
|
||||
b.HasOne("Marco.Pms.Model.Projects.Project", "Project")
|
||||
.WithMany()
|
||||
.HasForeignKey("ProjectId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("Marco.Pms.Model.Employees.Employee", "ReviewedBy")
|
||||
.WithMany()
|
||||
.HasForeignKey("ReviewedById");
|
||||
@ -7348,8 +7394,6 @@ namespace Marco.Pms.DataAccess.Migrations
|
||||
|
||||
b.Navigation("ProcessedBy");
|
||||
|
||||
b.Navigation("Project");
|
||||
|
||||
b.Navigation("ReviewedBy");
|
||||
|
||||
b.Navigation("Status");
|
||||
@ -7480,10 +7524,6 @@ namespace Marco.Pms.DataAccess.Migrations
|
||||
.WithMany()
|
||||
.HasForeignKey("PaidById");
|
||||
|
||||
b.HasOne("Marco.Pms.Model.Projects.Project", "Project")
|
||||
.WithMany()
|
||||
.HasForeignKey("ProjectId");
|
||||
|
||||
b.HasOne("Marco.Pms.Model.Expenses.RecurringPayment", "RecurringPayment")
|
||||
.WithMany()
|
||||
.HasForeignKey("RecurringPaymentId");
|
||||
@ -7508,8 +7548,6 @@ namespace Marco.Pms.DataAccess.Migrations
|
||||
|
||||
b.Navigation("PaidBy");
|
||||
|
||||
b.Navigation("Project");
|
||||
|
||||
b.Navigation("RecurringPayment");
|
||||
|
||||
b.Navigation("Tenant");
|
||||
@ -7562,10 +7600,6 @@ namespace Marco.Pms.DataAccess.Migrations
|
||||
.WithMany()
|
||||
.HasForeignKey("ExpenseCategoryId");
|
||||
|
||||
b.HasOne("Marco.Pms.Model.Projects.Project", "Project")
|
||||
.WithMany()
|
||||
.HasForeignKey("ProjectId");
|
||||
|
||||
b.HasOne("Marco.Pms.Model.Expenses.Masters.RecurringPaymentStatus", "Status")
|
||||
.WithMany()
|
||||
.HasForeignKey("StatusId")
|
||||
@ -7588,8 +7622,6 @@ namespace Marco.Pms.DataAccess.Migrations
|
||||
|
||||
b.Navigation("ExpenseCategory");
|
||||
|
||||
b.Navigation("Project");
|
||||
|
||||
b.Navigation("Status");
|
||||
|
||||
b.Navigation("Tenant");
|
||||
@ -8418,6 +8450,10 @@ namespace Marco.Pms.DataAccess.Migrations
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("Marco.Pms.Model.ServiceProject.ProjectBranch", "ProjectBranch")
|
||||
.WithMany()
|
||||
.HasForeignKey("ProjectBranchId");
|
||||
|
||||
b.HasOne("Marco.Pms.Model.ServiceProject.ServiceProject", "Project")
|
||||
.WithMany()
|
||||
.HasForeignKey("ProjectId")
|
||||
@ -8444,6 +8480,8 @@ namespace Marco.Pms.DataAccess.Migrations
|
||||
|
||||
b.Navigation("Project");
|
||||
|
||||
b.Navigation("ProjectBranch");
|
||||
|
||||
b.Navigation("Status");
|
||||
|
||||
b.Navigation("Tenant");
|
||||
@ -8451,6 +8489,39 @@ namespace Marco.Pms.DataAccess.Migrations
|
||||
b.Navigation("UpdatedBy");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Marco.Pms.Model.ServiceProject.ProjectBranch", b =>
|
||||
{
|
||||
b.HasOne("Marco.Pms.Model.Employees.Employee", "CreatedBy")
|
||||
.WithMany()
|
||||
.HasForeignKey("CreatedById")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("Marco.Pms.Model.ServiceProject.ServiceProject", "Project")
|
||||
.WithMany()
|
||||
.HasForeignKey("ProjectId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("Marco.Pms.Model.TenantModels.Tenant", "Tenant")
|
||||
.WithMany()
|
||||
.HasForeignKey("TenantId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("Marco.Pms.Model.Employees.Employee", "UpdatedBy")
|
||||
.WithMany()
|
||||
.HasForeignKey("UpdatedById");
|
||||
|
||||
b.Navigation("CreatedBy");
|
||||
|
||||
b.Navigation("Project");
|
||||
|
||||
b.Navigation("Tenant");
|
||||
|
||||
b.Navigation("UpdatedBy");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Marco.Pms.Model.ServiceProject.ServiceProject", b =>
|
||||
{
|
||||
b.HasOne("Marco.Pms.Model.OrganizationModel.Organization", "Client")
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using Marco.Pms.Model.Projects;
|
||||
using Marco.Pms.Model.Utilities;
|
||||
using Marco.Pms.Model.Utilities;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
|
||||
namespace Marco.Pms.Model.Directory
|
||||
{
|
||||
@ -9,9 +8,6 @@ namespace Marco.Pms.Model.Directory
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public Guid ProjectId { get; set; }
|
||||
[ValidateNever]
|
||||
[ForeignKey("ProjectId")]
|
||||
public Project? Project { get; set; }
|
||||
public Guid ContactId { get; set; }
|
||||
[ValidateNever]
|
||||
[ForeignKey("ContactId")]
|
||||
|
||||
@ -8,6 +8,7 @@ namespace Marco.Pms.Model.Dtos.ServiceProject
|
||||
public required string Title { get; set; }
|
||||
public required string Description { get; set; }
|
||||
public required Guid ProjectId { get; set; }
|
||||
public Guid? ProjectBranchId { get; set; }
|
||||
public List<BasicEmployeeDto>? Assignees { get; set; }
|
||||
public required DateTime StartDate { get; set; }
|
||||
public required DateTime DueDate { get; set; }
|
||||
|
||||
13
Marco.Pms.Model/Dtos/ServiceProject/ProjectBranchDto.cs
Normal file
13
Marco.Pms.Model/Dtos/ServiceProject/ProjectBranchDto.cs
Normal file
@ -0,0 +1,13 @@
|
||||
namespace Marco.Pms.Model.Dtos.ServiceProject
|
||||
{
|
||||
public class ProjectBranchDto
|
||||
{
|
||||
public Guid? Id { get; set; }
|
||||
public required string BranchName { get; set; }
|
||||
public required Guid ProjectId { get; set; }
|
||||
public required string ContactInformation { get; set; }
|
||||
public required string Address { get; set; }
|
||||
public required string BranchType { get; set; } // HQ, ATMs, Bank Branches, Overcounter desk
|
||||
public string? GoogleMapUrl { get; set; }
|
||||
}
|
||||
}
|
||||
12
Marco.Pms.Model/Dtos/ServiceProject/TalkingPointDto.cs
Normal file
12
Marco.Pms.Model/Dtos/ServiceProject/TalkingPointDto.cs
Normal file
@ -0,0 +1,12 @@
|
||||
using Marco.Pms.Model.Utilities;
|
||||
|
||||
namespace Marco.Pms.Model.Dtos.ServiceProject
|
||||
{
|
||||
public class TalkingPointDto
|
||||
{
|
||||
public Guid? Id { get; set; }
|
||||
public required Guid ServiceProjectId { get; set; }
|
||||
public required string Comment { get; set; }
|
||||
public List<FileUploadModel>? Attachments { get; set; }
|
||||
}
|
||||
}
|
||||
@ -7,11 +7,11 @@ namespace Marco.Pms.Model.Dtos.ServiceProject
|
||||
{
|
||||
public string? Title { get; set; }
|
||||
public string? Description { get; set; }
|
||||
public Guid ProjectId { get; set; }
|
||||
public Guid StatusId { get; set; }
|
||||
public List<BasicEmployeeDto>? Assignees { get; set; }
|
||||
public DateTime StartDate { get; set; }
|
||||
public DateTime DueDate { get; set; }
|
||||
public List<TagDto>? Tags { get; set; }
|
||||
public bool IsArchive { get; set; } = false;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
using Marco.Pms.Model.Employees;
|
||||
using Marco.Pms.Model.Expenses.Masters;
|
||||
using Marco.Pms.Model.Master;
|
||||
using Marco.Pms.Model.Projects;
|
||||
using Marco.Pms.Model.Utilities;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
@ -14,10 +13,6 @@ namespace Marco.Pms.Model.Expenses
|
||||
public string UIDPrefix { get; set; } = default!;
|
||||
public int UIDPostfix { get; set; }
|
||||
public Guid ProjectId { get; set; }
|
||||
|
||||
[ValidateNever]
|
||||
[ForeignKey("ProjectId")]
|
||||
public Project? Project { get; set; }
|
||||
public Guid ExpensesTypeId { get; set; }
|
||||
|
||||
//[ValidateNever]
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
using Marco.Pms.Model.Employees;
|
||||
using Marco.Pms.Model.Expenses.Masters;
|
||||
using Marco.Pms.Model.Master;
|
||||
using Marco.Pms.Model.Projects;
|
||||
using Marco.Pms.Model.Utilities;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
@ -28,10 +27,6 @@ namespace Marco.Pms.Model.Expenses
|
||||
public double? TDSPercentage { get; set; }
|
||||
public DateTime DueDate { get; set; }
|
||||
public Guid? ProjectId { get; set; }
|
||||
|
||||
[ValidateNever]
|
||||
[ForeignKey("ProjectId")]
|
||||
public Project? Project { get; set; }
|
||||
public Guid? RecurringPaymentId { get; set; }
|
||||
|
||||
[ValidateNever]
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
using Marco.Pms.Model.Employees;
|
||||
using Marco.Pms.Model.Expenses.Masters;
|
||||
using Marco.Pms.Model.Master;
|
||||
using Marco.Pms.Model.Projects;
|
||||
using Marco.Pms.Model.TenantModels;
|
||||
using Marco.Pms.Model.Utilities;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
|
||||
@ -29,10 +28,6 @@ namespace Marco.Pms.Model.Expenses
|
||||
public DateTime? LatestPRGeneratedAt { get; set; }
|
||||
public DateTime? NextStrikeDate { get; set; }
|
||||
public Guid? ProjectId { get; set; }
|
||||
|
||||
[ValidateNever]
|
||||
[ForeignKey("ProjectId")]
|
||||
public Project? Project { get; set; }
|
||||
public int PaymentBufferDays { get; set; }
|
||||
public Guid? ExpenseCategoryId { get; set; }
|
||||
|
||||
|
||||
11
Marco.Pms.Model/Filters/AdvanceFilter.cs
Normal file
11
Marco.Pms.Model/Filters/AdvanceFilter.cs
Normal file
@ -0,0 +1,11 @@
|
||||
namespace Marco.Pms.Model.Filters
|
||||
{
|
||||
public class AdvanceFilter
|
||||
{
|
||||
// The dynamic filters from your JSON
|
||||
public List<SortItem>? SortFilters { get; set; }
|
||||
public List<SearchItem>? SearchFilters { get; set; }
|
||||
public List<AdvanceItem>? AdvanceFilters { get; set; }
|
||||
public string GroupByColumn { get; set; } = string.Empty;
|
||||
}
|
||||
}
|
||||
9
Marco.Pms.Model/Filters/AdvanceItem.cs
Normal file
9
Marco.Pms.Model/Filters/AdvanceItem.cs
Normal file
@ -0,0 +1,9 @@
|
||||
namespace Marco.Pms.Model.Filters
|
||||
{
|
||||
public class AdvanceItem
|
||||
{
|
||||
public string Column { get; set; } = string.Empty;
|
||||
public string Opration { get; set; } = string.Empty; // "greater than", "equal to", etc.
|
||||
public string Value { get; set; } = string.Empty;
|
||||
}
|
||||
}
|
||||
8
Marco.Pms.Model/Filters/SearchItem.cs
Normal file
8
Marco.Pms.Model/Filters/SearchItem.cs
Normal file
@ -0,0 +1,8 @@
|
||||
namespace Marco.Pms.Model.Filters
|
||||
{
|
||||
public class SearchItem
|
||||
{
|
||||
public string Column { get; set; } = string.Empty;
|
||||
public string Value { get; set; } = string.Empty;
|
||||
}
|
||||
}
|
||||
8
Marco.Pms.Model/Filters/SortItem.cs
Normal file
8
Marco.Pms.Model/Filters/SortItem.cs
Normal file
@ -0,0 +1,8 @@
|
||||
namespace Marco.Pms.Model.Filters
|
||||
{
|
||||
public class SortItem
|
||||
{
|
||||
public string Column { get; set; } = string.Empty;
|
||||
public bool SortDescending { get; set; }
|
||||
}
|
||||
}
|
||||
@ -14,7 +14,7 @@ namespace Marco.Pms.Model.Projects
|
||||
public Guid Id { get; set; }
|
||||
[Required]
|
||||
[DisplayName("Project Name")]
|
||||
public string? Name { get; set; }
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public string? ShortName { get; set; }
|
||||
|
||||
[DisplayName("Project Address")]
|
||||
@ -22,7 +22,7 @@ namespace Marco.Pms.Model.Projects
|
||||
|
||||
[DisplayName("Contact Person")]
|
||||
|
||||
public string? ContactPerson { get; set; }
|
||||
public string ContactPerson { get; set; } = string.Empty;
|
||||
|
||||
public DateTime? StartDate { get; set; }
|
||||
public DateTime? EndDate { get; set; }
|
||||
|
||||
@ -17,6 +17,11 @@ namespace Marco.Pms.Model.ServiceProject
|
||||
[ValidateNever]
|
||||
[ForeignKey("ProjectId")]
|
||||
public ServiceProject? Project { get; set; }
|
||||
public Guid? ProjectBranchId { get; set; }
|
||||
|
||||
[ValidateNever]
|
||||
[ForeignKey("ProjectBranchId")]
|
||||
public ProjectBranch? ProjectBranch { get; set; }
|
||||
public Guid StatusId { get; set; }
|
||||
|
||||
[ValidateNever]
|
||||
@ -25,6 +30,7 @@ namespace Marco.Pms.Model.ServiceProject
|
||||
public DateTime StartDate { get; set; }
|
||||
public DateTime DueDate { get; set; }
|
||||
public bool IsActive { get; set; } = true;
|
||||
public bool IsArchive { get; set; } = false;
|
||||
public DateTime CreatedAt { get; set; }
|
||||
public Guid CreatedById { get; set; }
|
||||
|
||||
|
||||
35
Marco.Pms.Model/ServiceProject/ProjectBranch.cs
Normal file
35
Marco.Pms.Model/ServiceProject/ProjectBranch.cs
Normal file
@ -0,0 +1,35 @@
|
||||
using Marco.Pms.Model.Employees;
|
||||
using Marco.Pms.Model.Utilities;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
|
||||
namespace Marco.Pms.Model.ServiceProject
|
||||
{
|
||||
public class ProjectBranch : TenantRelation
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public string BranchName { get; set; } = string.Empty;
|
||||
public Guid ProjectId { get; set; }
|
||||
|
||||
[ValidateNever]
|
||||
[ForeignKey("ProjectId")]
|
||||
public ServiceProject? Project { get; set; }
|
||||
public string ContactInformation { get; set; } = string.Empty; // Json string
|
||||
public string Address { get; set; } = string.Empty;
|
||||
public string BranchType { get; set; } = string.Empty; // HQ, ATMs, Bank Branches, Overcounter desk
|
||||
public string? GoogleMapUrl { get; set; }
|
||||
public bool IsActive { get; set; } = true;
|
||||
public DateTime CreatedAt { get; set; }
|
||||
public Guid CreatedById { get; set; }
|
||||
|
||||
[ValidateNever]
|
||||
[ForeignKey("CreatedById")]
|
||||
public Employee? CreatedBy { get; set; }
|
||||
public DateTime? UpdatedAt { get; set; }
|
||||
public Guid? UpdatedById { get; set; }
|
||||
|
||||
[ValidateNever]
|
||||
[ForeignKey("UpdatedById")]
|
||||
public Employee? UpdatedBy { get; set; }
|
||||
}
|
||||
}
|
||||
31
Marco.Pms.Model/ServiceProject/TalkingPoint.cs
Normal file
31
Marco.Pms.Model/ServiceProject/TalkingPoint.cs
Normal file
@ -0,0 +1,31 @@
|
||||
using Marco.Pms.Model.Employees;
|
||||
using Marco.Pms.Model.Utilities;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
|
||||
namespace Marco.Pms.Model.ServiceProject
|
||||
{
|
||||
public class TalkingPoint : TenantRelation
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public Guid ServiceProjectId { get; set; }
|
||||
|
||||
[ValidateNever]
|
||||
[ForeignKey("ServiceProjectId")]
|
||||
public ServiceProject? ServiceProject { get; set; }
|
||||
public string Comment { get; set; } = string.Empty;
|
||||
public bool IsActive { get; set; } = true;
|
||||
public DateTime CreatedAt { get; set; }
|
||||
public Guid CreatedById { get; set; }
|
||||
|
||||
[ValidateNever]
|
||||
[ForeignKey("CreatedById")]
|
||||
public Employee? CreatedBy { get; set; }
|
||||
public DateTime? UpdatedAt { get; set; }
|
||||
public Guid? UpdatedById { get; set; }
|
||||
|
||||
[ValidateNever]
|
||||
[ForeignKey("UpdatedById")]
|
||||
public Employee? UpdatedBy { get; set; }
|
||||
}
|
||||
}
|
||||
22
Marco.Pms.Model/ServiceProject/TalkingPointAttachment.cs
Normal file
22
Marco.Pms.Model/ServiceProject/TalkingPointAttachment.cs
Normal file
@ -0,0 +1,22 @@
|
||||
using Marco.Pms.Model.DocumentManager;
|
||||
using Marco.Pms.Model.Utilities;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
|
||||
namespace Marco.Pms.Model.ServiceProject
|
||||
{
|
||||
public class TalkingPointAttachment : TenantRelation
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public Guid DocumentId { get; set; }
|
||||
|
||||
[ValidateNever]
|
||||
[ForeignKey("DocumentId")]
|
||||
public Document? Document { get; set; }
|
||||
public Guid? TalkingPointId { get; set; }
|
||||
|
||||
[ValidateNever]
|
||||
[ForeignKey("TalkingPointId")]
|
||||
public TalkingPoint? TalkingPoint { get; set; }
|
||||
}
|
||||
}
|
||||
@ -10,7 +10,7 @@ namespace Marco.Pms.Model.ViewModels.Expenses
|
||||
public class ExpenseDetailsVM
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public ProjectInfoVM? Project { get; set; }
|
||||
public BasicProjectVM? Project { get; set; }
|
||||
public ExpenseCategoryMasterVM? ExpenseCategory { get; set; }
|
||||
public PaymentModeMatserVM? PaymentMode { get; set; }
|
||||
public BasicEmployeeVM? PaidBy { get; set; }
|
||||
|
||||
@ -9,7 +9,7 @@ namespace Marco.Pms.Model.ViewModels.Expanses
|
||||
public class ExpenseList
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public ProjectInfoVM? Project { get; set; }
|
||||
public BasicProjectVM? Project { get; set; }
|
||||
public ExpenseCategoryMasterVM? ExpenseCategory { get; set; }
|
||||
public PaymentModeMatserVM? PaymentMode { get; set; }
|
||||
public BasicEmployeeVM? PaidBy { get; set; }
|
||||
|
||||
@ -7,5 +7,6 @@
|
||||
public string? Description { get; set; }
|
||||
public string? JobTicketUId { get; set; }
|
||||
public string? StatusName { get; set; }
|
||||
public bool IsArchive { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,9 @@
|
||||
namespace Marco.Pms.Model.ViewModels.ServiceProject
|
||||
{
|
||||
public class BasicProjectBranchVM
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public string? BranchName { get; set; }
|
||||
public string? BranchType { get; set; } // HQ, ATMs, Bank Branches, Overcounter desk
|
||||
}
|
||||
}
|
||||
@ -6,8 +6,5 @@
|
||||
public string? Name { get; set; }
|
||||
public string? ShortName { get; set; }
|
||||
public DateTime AssignedDate { get; set; }
|
||||
public string? ContactName { get; set; }
|
||||
public string? ContactPhone { get; set; }
|
||||
public string? ContactEmail { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@ -11,11 +11,13 @@ namespace Marco.Pms.Model.ViewModels.ServiceProject
|
||||
public string? Description { get; set; }
|
||||
public string? JobTicketUId { get; set; }
|
||||
public BasicServiceProjectVM? Project { get; set; }
|
||||
public BasicProjectBranchVM? ProjectBranch { get; set; }
|
||||
public List<BasicEmployeeVM>? Assignees { get; set; }
|
||||
public JobStatus? Status { get; set; }
|
||||
public DateTime StartDate { get; set; }
|
||||
public DateTime DueDate { get; set; }
|
||||
public bool IsActive { get; set; }
|
||||
public bool IsArchive { get; set; }
|
||||
public Guid? AttendanceId { get; set; }
|
||||
public TAGGING_MARK_TYPE? TaggingAction { get; set; }
|
||||
public TAGGING_MARK_TYPE? NextTaggingAction { get; set; }
|
||||
|
||||
@ -16,6 +16,7 @@ namespace Marco.Pms.Model.ViewModels.ServiceProject
|
||||
public DateTime StartDate { get; set; }
|
||||
public DateTime DueDate { get; set; }
|
||||
public bool IsActive { get; set; }
|
||||
public bool IsArchive { get; set; }
|
||||
public DateTime CreatedAt { get; set; }
|
||||
public BasicEmployeeVM? CreatedBy { get; set; }
|
||||
public List<TagVM>? Tags { get; set; }
|
||||
|
||||
@ -0,0 +1,20 @@
|
||||
using Marco.Pms.Model.ViewModels.Activities;
|
||||
|
||||
namespace Marco.Pms.Model.ViewModels.ServiceProject
|
||||
{
|
||||
public class ProjectBranchDetailsVM
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public string? BranchName { get; set; }
|
||||
public BasicServiceProjectVM? Project { get; set; }
|
||||
public string? ContactInformation { get; set; } // Json string
|
||||
public string? Address { get; set; }
|
||||
public string? BranchType { get; set; } // HQ, ATMs, Bank Branches, Overcounter desk
|
||||
public string? GoogleMapUrl { get; set; }
|
||||
public bool IsActive { get; set; } = true;
|
||||
public DateTime CreatedAt { get; set; }
|
||||
public BasicEmployeeVM? CreatedBy { get; set; }
|
||||
public DateTime? UpdatedAt { get; set; }
|
||||
public BasicEmployeeVM? UpdatedBy { get; set; }
|
||||
}
|
||||
}
|
||||
16
Marco.Pms.Model/ViewModels/ServiceProject/ProjectBranchVM.cs
Normal file
16
Marco.Pms.Model/ViewModels/ServiceProject/ProjectBranchVM.cs
Normal file
@ -0,0 +1,16 @@
|
||||
using Marco.Pms.Model.ViewModels.Activities;
|
||||
|
||||
namespace Marco.Pms.Model.ViewModels.ServiceProject
|
||||
{
|
||||
public class ProjectBranchVM
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public string? BranchName { get; set; }
|
||||
public BasicServiceProjectVM? Project { get; set; }
|
||||
public string? ContactInformation { get; set; } // Json string
|
||||
public string? Address { get; set; }
|
||||
public string? BranchType { get; set; } // HQ, ATMs, Bank Branches, Overcounter desk
|
||||
public DateTime CreatedAt { get; set; }
|
||||
public BasicEmployeeVM? CreatedBy { get; set; }
|
||||
}
|
||||
}
|
||||
18
Marco.Pms.Model/ViewModels/ServiceProject/TalkingPointVM.cs
Normal file
18
Marco.Pms.Model/ViewModels/ServiceProject/TalkingPointVM.cs
Normal file
@ -0,0 +1,18 @@
|
||||
using Marco.Pms.Model.ViewModels.Activities;
|
||||
using Marco.Pms.Model.ViewModels.DocumentManager;
|
||||
|
||||
namespace Marco.Pms.Model.ViewModels.ServiceProject
|
||||
{
|
||||
public class TalkingPointVM
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public BasicServiceProjectVM? ServiceProject { get; set; }
|
||||
public string? Comment { get; set; }
|
||||
public bool IsActive { get; set; }
|
||||
public DateTime CreatedAt { get; set; }
|
||||
public BasicEmployeeVM? CreatedBy { get; set; }
|
||||
public DateTime? UpdatedAt { get; set; }
|
||||
public BasicEmployeeVM? UpdatedBy { get; set; }
|
||||
public List<DocumentVM>? Attachments { get; set; }
|
||||
}
|
||||
}
|
||||
@ -474,7 +474,7 @@ namespace MarcoBMS.Services.Controllers
|
||||
var _employeeHelper = scope.ServiceProvider.GetRequiredService<EmployeeHelper>();
|
||||
var _firebase = scope.ServiceProvider.GetRequiredService<IFirebaseService>();
|
||||
|
||||
var currentEmployee = await _userHelper.GetCurrentEmployeeAsync();
|
||||
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
|
||||
using var transaction = await _context.Database.BeginTransactionAsync();
|
||||
|
||||
try
|
||||
@ -517,7 +517,7 @@ namespace MarcoBMS.Services.Controllers
|
||||
{
|
||||
attendance.OutTime = finalDateTime;
|
||||
attendance.Activity = ATTENDANCE_MARK_TYPE.REQUEST_REGULARIZE;
|
||||
attendance.RequestedById = currentEmployee.Id;
|
||||
attendance.RequestedById = loggedInEmployee.Id;
|
||||
attendance.RequestedAt = DateTime.UtcNow;
|
||||
}
|
||||
else
|
||||
@ -531,7 +531,7 @@ namespace MarcoBMS.Services.Controllers
|
||||
{
|
||||
attendance.IsApproved = true;
|
||||
attendance.Activity = ATTENDANCE_MARK_TYPE.REGULARIZE;
|
||||
attendance.ApprovedById = currentEmployee.Id;
|
||||
attendance.ApprovedById = loggedInEmployee.Id;
|
||||
attendance.ApprovedAt = DateTime.UtcNow;
|
||||
// do nothing
|
||||
}
|
||||
@ -539,7 +539,7 @@ namespace MarcoBMS.Services.Controllers
|
||||
{
|
||||
attendance.IsApproved = false;
|
||||
attendance.Activity = ATTENDANCE_MARK_TYPE.REGULARIZE_REJECT;
|
||||
attendance.ApprovedById = currentEmployee.Id;
|
||||
attendance.ApprovedById = loggedInEmployee.Id;
|
||||
attendance.ApprovedAt = DateTime.UtcNow;
|
||||
// do nothing
|
||||
}
|
||||
@ -584,7 +584,7 @@ namespace MarcoBMS.Services.Controllers
|
||||
Longitude = recordAttendanceDot.Longitude,
|
||||
|
||||
TenantId = tenantId,
|
||||
UpdatedBy = currentEmployee.Id,
|
||||
UpdatedBy = loggedInEmployee.Id,
|
||||
UpdatedOn = recordAttendanceDot.Date
|
||||
};
|
||||
//if (recordAttendanceDot.Image != null && recordAttendanceDot.Image.Count > 0)
|
||||
@ -619,7 +619,7 @@ namespace MarcoBMS.Services.Controllers
|
||||
{
|
||||
sendActivity = 1;
|
||||
}
|
||||
var notification = new { LoggedInUserId = currentEmployee.Id, Keyword = "Attendance", Activity = sendActivity, ProjectId = attendance.ProjectID, Response = vm };
|
||||
var notification = new { LoggedInUserId = loggedInEmployee.Id, Keyword = "Attendance", Activity = sendActivity, ProjectId = attendance.ProjectID, Response = vm };
|
||||
await _signalR.Clients.All.SendAsync("NotificationEventHandler", notification);
|
||||
_logger.LogInfo("Attendance for employee {FirstName} {LastName} has been marked", employee.FirstName ?? string.Empty, employee.LastName ?? string.Empty);
|
||||
|
||||
@ -628,10 +628,11 @@ namespace MarcoBMS.Services.Controllers
|
||||
// --- Push Notification Section ---
|
||||
// This section attempts to send a test push notification to the user's device.
|
||||
// It's designed to fail gracefully and handle invalid Firebase Cloud Messaging (FCM) tokens.
|
||||
|
||||
var context = HttpContext;
|
||||
string origin = context.Request.Headers["Origin"].FirstOrDefault() ?? "";
|
||||
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, origin, loggedInEmployee.Id, tenantId);
|
||||
|
||||
});
|
||||
|
||||
@ -848,10 +849,12 @@ namespace MarcoBMS.Services.Controllers
|
||||
// --- Push Notification Section ---
|
||||
// This section attempts to send a test push notification to the user's device.
|
||||
// It's designed to fail gracefully and handle invalid Firebase Cloud Messaging (FCM) tokens.
|
||||
var context = HttpContext;
|
||||
string origin = context.Request.Headers["Origin"].FirstOrDefault() ?? "";
|
||||
|
||||
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, origin, loggedInEmployee.Id, tenantId);
|
||||
|
||||
});
|
||||
|
||||
|
||||
@ -4,6 +4,7 @@ using Marco.Pms.Helpers.Utility;
|
||||
using Marco.Pms.Model.Collection;
|
||||
using Marco.Pms.Model.Dtos.Collection;
|
||||
using Marco.Pms.Model.Entitlements;
|
||||
using Marco.Pms.Model.Filters;
|
||||
using Marco.Pms.Model.MongoDBModels.Utility;
|
||||
using Marco.Pms.Model.OrganizationModel;
|
||||
using Marco.Pms.Model.Projects;
|
||||
@ -13,6 +14,7 @@ using Marco.Pms.Model.ViewModels.Activities;
|
||||
using Marco.Pms.Model.ViewModels.Collection;
|
||||
using Marco.Pms.Model.ViewModels.Organization;
|
||||
using Marco.Pms.Model.ViewModels.Projects;
|
||||
using Marco.Pms.Services.Extensions;
|
||||
using Marco.Pms.Services.Service;
|
||||
using MarcoBMS.Services.Helpers;
|
||||
using MarcoBMS.Services.Service;
|
||||
@ -20,7 +22,9 @@ using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using MongoDB.Driver;
|
||||
using System.Text.Json;
|
||||
using Document = Marco.Pms.Model.DocumentManager.Document;
|
||||
using Invoice = Marco.Pms.Model.Collection.Invoice;
|
||||
|
||||
namespace Marco.Pms.Services.Controllers
|
||||
{
|
||||
@ -59,8 +63,8 @@ namespace Marco.Pms.Services.Controllers
|
||||
/// </summary>
|
||||
|
||||
[HttpGet("invoice/list")]
|
||||
public async Task<IActionResult> GetInvoiceListAsync([FromQuery] Guid? projectId, [FromQuery] string? searchString, [FromQuery] DateTime? fromDate, [FromQuery] DateTime? toDate,
|
||||
[FromQuery] int pageSize = 20, [FromQuery] int pageNumber = 1, [FromQuery] bool isActive = true, [FromQuery] bool isPending = false)
|
||||
public async Task<IActionResult> GetInvoiceListAsync([FromQuery] Guid? projectId, [FromQuery] string? searchString, [FromQuery] string? filter, [FromQuery] DateTime? fromDate,
|
||||
[FromQuery] DateTime? toDate, [FromQuery] int pageSize = 20, [FromQuery] int pageNumber = 1, [FromQuery] bool isActive = true, [FromQuery] bool isPending = false)
|
||||
{
|
||||
try
|
||||
{
|
||||
@ -94,16 +98,52 @@ namespace Marco.Pms.Services.Controllers
|
||||
|
||||
_logger.LogInfo("Permissions validated for EmployeeId {EmployeeId}.", employee.Id);
|
||||
|
||||
var advanceFilter = TryDeserializeFilter(filter);
|
||||
|
||||
await using var _context = await _dbContextFactory.CreateDbContextAsync();
|
||||
await using var context = await _dbContextFactory.CreateDbContextAsync();
|
||||
|
||||
// Fetch related project data asynchronously and in parallel
|
||||
var infraProjectsQuery = _context.Projects
|
||||
.Where(p => p.TenantId == tenantId);
|
||||
|
||||
var serviceProjectsQuery = context.ServiceProjects
|
||||
.Where(sp => sp.TenantId == tenantId);
|
||||
|
||||
if (advanceFilter?.SearchFilters != null && advanceFilter.SearchFilters.Any())
|
||||
{
|
||||
var projectSearchFilter = advanceFilter.SearchFilters
|
||||
.Where(f => f.Column == "ProjectName")
|
||||
.Select(f => new SearchItem { Column = "Name", Value = f.Value })
|
||||
.ToList();
|
||||
if (projectSearchFilter.Any())
|
||||
{
|
||||
infraProjectsQuery = infraProjectsQuery.ApplySearchFilters(projectSearchFilter);
|
||||
serviceProjectsQuery = serviceProjectsQuery.ApplySearchFilters(projectSearchFilter);
|
||||
}
|
||||
}
|
||||
|
||||
var infraProjectsTask = infraProjectsQuery
|
||||
.Select(p => _mapper.Map<BasicProjectVM>(p))
|
||||
.ToListAsync();
|
||||
var serviceProjectsTask = serviceProjectsQuery
|
||||
.Select(sp => _mapper.Map<BasicProjectVM>(sp))
|
||||
.ToListAsync();
|
||||
|
||||
await Task.WhenAll(infraProjectsTask, serviceProjectsTask);
|
||||
|
||||
var projects = infraProjectsTask.Result;
|
||||
projects.AddRange(serviceProjectsTask.Result);
|
||||
|
||||
var projIds = projects.Select(p => p.Id).Distinct().ToList();
|
||||
|
||||
// Build invoice query efficiently - always use AsNoTracking for reads
|
||||
var query = _context.Invoices
|
||||
.AsNoTracking()
|
||||
.Include(i => i.BilledTo)
|
||||
.Include(i => i.CreatedBy).ThenInclude(e => e!.JobRole)
|
||||
.Include(i => i.UpdatedBy).ThenInclude(e => e!.JobRole)
|
||||
.Where(i => i.IsActive == isActive && i.TenantId == tenantId);
|
||||
.Where(i => projIds.Contains(i.ProjectId) && i.IsActive == isActive && i.TenantId == tenantId);
|
||||
|
||||
// Filter by date, ensuring date boundaries are correct
|
||||
if (fromDate.HasValue && toDate.HasValue)
|
||||
@ -126,11 +166,22 @@ namespace Marco.Pms.Services.Controllers
|
||||
_logger.LogDebug("Project filter applied: {ProjectId}", projectId.Value);
|
||||
}
|
||||
|
||||
query = query.ApplyCustomFilters(advanceFilter, "InvoiceDate");
|
||||
|
||||
if (advanceFilter?.SearchFilters != null)
|
||||
{
|
||||
var invoiceSearchFilter = advanceFilter.SearchFilters.Where(f => f.Column != "ProjectName").ToList();
|
||||
if (invoiceSearchFilter.Any())
|
||||
{
|
||||
query = query.ApplySearchFilters(invoiceSearchFilter);
|
||||
}
|
||||
}
|
||||
|
||||
var totalItems = await query.CountAsync();
|
||||
var totalPages = (int)Math.Ceiling((double)totalItems / pageSize);
|
||||
_logger.LogInfo("Total invoices found: {TotalItems}", totalItems);
|
||||
|
||||
var pagedInvoices = await query
|
||||
.OrderByDescending(i => i.InvoiceDate)
|
||||
.Skip((pageNumber - 1) * pageSize)
|
||||
.Take(pageSize)
|
||||
.ToListAsync();
|
||||
@ -155,20 +206,6 @@ namespace Marco.Pms.Services.Controllers
|
||||
|
||||
_logger.LogDebug("Received payment data for {Count} invoices.", paymentGroups.Count);
|
||||
|
||||
// Fetch related project data asynchronously and in parallel
|
||||
var projIds = pagedInvoices.Select(i => i.ProjectId).Distinct().ToList();
|
||||
var infraProjectsTask = _context.Projects
|
||||
.Where(p => projIds.Contains(p.Id) && p.TenantId == tenantId)
|
||||
.ToListAsync();
|
||||
var serviceProjectsTask = context.ServiceProjects
|
||||
.Where(sp => projIds.Contains(sp.Id) && sp.TenantId == tenantId)
|
||||
.ToListAsync();
|
||||
|
||||
await Task.WhenAll(infraProjectsTask, serviceProjectsTask);
|
||||
|
||||
var infraProjects = infraProjectsTask.Result;
|
||||
var serviceProjects = serviceProjectsTask.Result;
|
||||
|
||||
// Build results and compute balances in memory for tight control
|
||||
var results = new List<InvoiceListVM>();
|
||||
|
||||
@ -185,16 +222,13 @@ namespace Marco.Pms.Services.Controllers
|
||||
var vm = _mapper.Map<InvoiceListVM>(invoice);
|
||||
|
||||
// Project mapping logic - minimize nested object allocations
|
||||
vm.Project = serviceProjects.Where(sp => sp.Id == invoice.ProjectId).Select(p => _mapper.Map<BasicProjectVM>(p)).FirstOrDefault()
|
||||
?? infraProjects.Where(ip => ip.Id == invoice.ProjectId).Select(sp => _mapper.Map<BasicProjectVM>(sp)).FirstOrDefault();
|
||||
vm.Project = projects.Where(sp => sp.Id == invoice.ProjectId).FirstOrDefault();
|
||||
|
||||
|
||||
vm.BalanceAmount = balance;
|
||||
results.Add(vm);
|
||||
}
|
||||
|
||||
var totalPages = (int)Math.Ceiling((double)totalItems / pageSize);
|
||||
|
||||
_logger.LogInfo("Returning {Count} invoices (page {PageNumber} of {TotalPages}).", results.Count, pageNumber, totalPages);
|
||||
|
||||
return Ok(ApiResponse<object>.SuccessResponse(
|
||||
@ -1155,6 +1189,45 @@ namespace Marco.Pms.Services.Controllers
|
||||
|
||||
#region =================================================================== Helper Functions ===================================================================
|
||||
|
||||
private AdvanceFilter? TryDeserializeFilter(string? filter)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(filter))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true };
|
||||
AdvanceFilter? advanceFilter = null;
|
||||
|
||||
try
|
||||
{
|
||||
// First, try to deserialize directly. This is the expected case (e.g., from a web client).
|
||||
advanceFilter = JsonSerializer.Deserialize<AdvanceFilter>(filter, options);
|
||||
}
|
||||
catch (JsonException ex)
|
||||
{
|
||||
_logger.LogError(ex, "[{MethodName}] Failed to directly deserialize filter. Attempting to unescape and re-parse. Filter: {Filter}", nameof(TryDeserializeFilter), filter);
|
||||
|
||||
// If direct deserialization fails, it might be an escaped string (common with tools like Postman or some mobile clients).
|
||||
try
|
||||
{
|
||||
// Unescape the string first, then deserialize the result.
|
||||
string unescapedJsonString = JsonSerializer.Deserialize<string>(filter, options) ?? "";
|
||||
if (!string.IsNullOrWhiteSpace(unescapedJsonString))
|
||||
{
|
||||
advanceFilter = JsonSerializer.Deserialize<AdvanceFilter>(unescapedJsonString, options);
|
||||
}
|
||||
}
|
||||
catch (JsonException ex1)
|
||||
{
|
||||
// If both attempts fail, log the final error and return null.
|
||||
_logger.LogError(ex1, "[{MethodName}] All attempts to deserialize the filter failed. Filter will be ignored. Filter: {Filter}", nameof(TryDeserializeFilter), filter);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return advanceFilter;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Async permission check helper with scoped DI lifetime
|
||||
/// </summary>
|
||||
|
||||
@ -206,6 +206,14 @@ namespace Marco.Pms.Services.Controllers
|
||||
return Ok(ApiResponse<object>.SuccessResponse(dashboardVM, "Project counts fetched successfully.", 200));
|
||||
}
|
||||
|
||||
[HttpGet("project-completion-status")]
|
||||
public async Task<IActionResult> GetAllProjectsAsync()
|
||||
{
|
||||
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
|
||||
var response = await _projectServices.GetAllProjectsAsync(string.Empty, 0, 0, loggedInEmployee, tenantId);
|
||||
return StatusCode(response.StatusCode, response);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves a dashboard summary of total employees and today's attendance.
|
||||
/// If a projectId is provided, it returns totals for that project; otherwise, for all accessible active projects.
|
||||
|
||||
@ -133,30 +133,13 @@ namespace Marco.Pms.Services.Controllers
|
||||
}
|
||||
|
||||
[HttpGet("get/project/report/{projectId}")]
|
||||
public async Task<IActionResult> GetProjectReport(Guid projectId)
|
||||
{
|
||||
using var scope = _serviceScopeFactory.CreateScope();
|
||||
var _reportHelper = scope.ServiceProvider.GetRequiredService<ReportHelper>();
|
||||
var _logger = scope.ServiceProvider.GetRequiredService<ILoggingService>();
|
||||
|
||||
var resonse = await _reportHelper.GetDailyProjectReportWithOutTenant(projectId);
|
||||
|
||||
if (resonse == null)
|
||||
{
|
||||
_logger.LogWarning("Project report not found");
|
||||
return NotFound(ApiResponse<object>.ErrorResponse("Project report not found", "Project report not found", 404));
|
||||
}
|
||||
_logger.LogInfo("Report for the project fetched successfully");
|
||||
return Ok(ApiResponse<object>.SuccessResponse(resonse, "Report for the project fetched successfully", 200));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves a daily project report by its unique identifier.
|
||||
/// </summary>
|
||||
/// <param name="projectId">The GUID of the project for which to generate the report.</param>
|
||||
/// <returns>An IActionResult containing the project report or an appropriate error response.</returns>
|
||||
[HttpGet("{projectId}/report")]
|
||||
public async Task<IActionResult> GetProjectReportAsync(Guid projectId)
|
||||
public async Task<IActionResult> GetProjectReportAsync(Guid projectId, [FromQuery] DateTime? date)
|
||||
{
|
||||
using var scope = _serviceScopeFactory.CreateScope();
|
||||
var _reportHelper = scope.ServiceProvider.GetRequiredService<ReportHelper>();
|
||||
@ -167,8 +150,17 @@ namespace Marco.Pms.Services.Controllers
|
||||
|
||||
try
|
||||
{
|
||||
DateTime reportDate;
|
||||
if (date.HasValue)
|
||||
{
|
||||
reportDate = date.Value;
|
||||
}
|
||||
else
|
||||
{
|
||||
reportDate = DateTime.UtcNow.AddDays(-1).Date;
|
||||
}
|
||||
// Call the helper service, which is now available as a class member.
|
||||
var response = await _reportHelper.GetDailyProjectReportWithOutTenant(projectId);
|
||||
var response = await _reportHelper.GetDailyProjectReportWithOutTenant(projectId, reportDate);
|
||||
|
||||
// Check if the report data was found.
|
||||
if (response == null)
|
||||
|
||||
@ -96,6 +96,75 @@ namespace Marco.Pms.Services.Controllers
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region =================================================================== Project Branch Functions ===================================================================
|
||||
|
||||
[HttpGet("branch/list/{projectId}")]
|
||||
public async Task<IActionResult> GetProjectBranchListByProject(Guid projectId, [FromQuery] string? searchString, [FromQuery] bool isActive = true,
|
||||
[FromQuery] int pageNumber = 1, [FromQuery] int pageSize = 20)
|
||||
{
|
||||
Employee loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
|
||||
var response = await _serviceProject.GetProjectBranchListByProjectAsync(projectId, isActive, searchString, pageNumber, pageSize, loggedInEmployee, tenantId);
|
||||
return StatusCode(response.StatusCode, response);
|
||||
}
|
||||
|
||||
[HttpGet("branch/details/{id}")]
|
||||
public async Task<IActionResult> GetProjectBranchDetails(Guid id)
|
||||
{
|
||||
Employee loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
|
||||
var response = await _serviceProject.GetProjectBranchDetailsAsync(id, loggedInEmployee, tenantId);
|
||||
|
||||
return StatusCode(response.StatusCode, response);
|
||||
}
|
||||
|
||||
[HttpGet("branch-type/list")]
|
||||
public async Task<IActionResult> GetBranchTypeList([FromQuery] string? searchString)
|
||||
{
|
||||
Employee loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
|
||||
var response = await _serviceProject.GetBranchTypeListAsync(searchString, loggedInEmployee, tenantId);
|
||||
|
||||
return StatusCode(response.StatusCode, response);
|
||||
}
|
||||
|
||||
[HttpPost("branch/create")]
|
||||
public async Task<IActionResult> CreateProjectBranch([FromBody] ProjectBranchDto model)
|
||||
{
|
||||
Employee loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
|
||||
var response = await _serviceProject.CreateProjectBranchAsync(model, loggedInEmployee, tenantId);
|
||||
if (response.Success)
|
||||
{
|
||||
var notification = new { LoggedInUserId = loggedInEmployee.Id, Keyword = "Project_Branch", Response = response.Data };
|
||||
await _signalR.SendNotificationAsync(notification);
|
||||
}
|
||||
return StatusCode(response.StatusCode, response);
|
||||
}
|
||||
|
||||
[HttpPut("branch/edit/{id}")]
|
||||
public async Task<IActionResult> UpdateProjectBranch(Guid id, ProjectBranchDto model)
|
||||
{
|
||||
Employee loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
|
||||
var response = await _serviceProject.UpdateProjectBranchAsync(id, model, loggedInEmployee, tenantId);
|
||||
if (response.Success)
|
||||
{
|
||||
var notification = new { LoggedInUserId = loggedInEmployee.Id, Keyword = "Project_Branch", Response = response.Data };
|
||||
await _signalR.SendNotificationAsync(notification);
|
||||
}
|
||||
return StatusCode(response.StatusCode, response);
|
||||
}
|
||||
|
||||
[HttpDelete("branch/delete/{id}")]
|
||||
public async Task<IActionResult> DeleteProjectBranch(Guid id, [FromQuery] bool isActive = false)
|
||||
{
|
||||
Employee loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
|
||||
var response = await _serviceProject.DeleteProjectBranchAsync(id, isActive, loggedInEmployee, tenantId);
|
||||
if (response.Success)
|
||||
{
|
||||
var notification = new { LoggedInUserId = loggedInEmployee.Id, Keyword = "Project_Branch", Response = response.Data };
|
||||
await _signalR.SendNotificationAsync(notification);
|
||||
}
|
||||
return StatusCode(response.StatusCode, response);
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region =================================================================== Service Project Allocation Functions ===================================================================
|
||||
|
||||
[HttpGet("get/allocation/list")]
|
||||
@ -127,10 +196,11 @@ namespace Marco.Pms.Services.Controllers
|
||||
#region =================================================================== Job Tickets Functions ===================================================================
|
||||
|
||||
[HttpGet("job/list")]
|
||||
public async Task<IActionResult> GetJobTicketsList([FromQuery] Guid? projectId, [FromQuery] int pageNumber = 1, [FromQuery] int pageSize = 20, [FromQuery] bool isActive = true)
|
||||
public async Task<IActionResult> GetJobTicketsList([FromQuery] Guid? projectId, [FromQuery] int pageNumber = 1, [FromQuery] int pageSize = 20, [FromQuery] bool isActive = true,
|
||||
[FromQuery] bool isArchive = false)
|
||||
{
|
||||
Employee loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
|
||||
var response = await _serviceProject.GetJobTicketsListAsync(projectId, pageNumber, pageSize, isActive, loggedInEmployee, tenantId);
|
||||
var response = await _serviceProject.GetJobTicketsListAsync(projectId, pageNumber, pageSize, isActive, isArchive, loggedInEmployee, tenantId);
|
||||
|
||||
return StatusCode(response.StatusCode, response);
|
||||
}
|
||||
|
||||
96
Marco.Pms.Services/Extensions/QueryableExtensions.cs
Normal file
96
Marco.Pms.Services/Extensions/QueryableExtensions.cs
Normal file
@ -0,0 +1,96 @@
|
||||
using Marco.Pms.Model.Filters;
|
||||
using System.Linq.Dynamic.Core;
|
||||
|
||||
namespace Marco.Pms.Services.Extensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Enterprise-grade extension methods for applying dynamic filters and sorting to IQueryable sources.
|
||||
/// </summary>
|
||||
public static class QueryableExtensions
|
||||
{
|
||||
public static IQueryable<T> ApplyCustomFilters<T>(this IQueryable<T> query, AdvanceFilter? filter, string defaultSortColumn)
|
||||
{
|
||||
// 1. Apply Advanced Filters (Arithmetic/Logic)
|
||||
if (filter?.AdvanceFilters != null && filter.AdvanceFilters.Any())
|
||||
{
|
||||
foreach (var advanceFilter in filter.AdvanceFilters)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(advanceFilter.Column)) continue;
|
||||
|
||||
string op = advanceFilter.Opration.ToLower().Trim();
|
||||
string predicate = "";
|
||||
|
||||
// Map your custom strings to Dynamic LINQ operators
|
||||
switch (op)
|
||||
{
|
||||
case "greater than": predicate = $"{advanceFilter.Column} > @0"; break;
|
||||
case "less than": predicate = $"{advanceFilter.Column} < @0"; break;
|
||||
case "equal to": predicate = $"{advanceFilter.Column} == @0"; break;
|
||||
case "not equal": predicate = $"{advanceFilter.Column} != @0"; break;
|
||||
case "greater or equal": predicate = $"{advanceFilter.Column} >= @0"; break;
|
||||
case "smaller or equal": predicate = $"{advanceFilter.Column} <= @0"; break;
|
||||
default: continue;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(predicate))
|
||||
{
|
||||
// Dynamic LINQ handles type conversion (string "100" to int 100) automatically
|
||||
query = query.Where(predicate, advanceFilter.Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Apply Sorting
|
||||
if (filter?.SortFilters != null && filter.SortFilters.Any())
|
||||
{
|
||||
// Build a comma-separated sort string: "Column1 desc, Column2 asc"
|
||||
var sortExpressions = new List<string>();
|
||||
foreach (var sort in filter.SortFilters)
|
||||
{
|
||||
string direction = sort.SortDescending ? "desc" : "asc";
|
||||
sortExpressions.Add($"{sort.Column} {direction}");
|
||||
}
|
||||
|
||||
query = query.OrderBy(string.Join(", ", sortExpressions));
|
||||
}
|
||||
else
|
||||
{
|
||||
// Default sorting
|
||||
query = query.OrderBy($"{defaultSortColumn} desc");
|
||||
}
|
||||
|
||||
|
||||
// 3.Apply GroupBy
|
||||
if (!string.IsNullOrEmpty(filter?.GroupByColumn))
|
||||
{
|
||||
query.GroupBy(filter.GroupByColumn, "it")
|
||||
.Select("new (Key, it as Items)"); // Reshape to { Key: "Value", Items: [...] }
|
||||
}
|
||||
return query;
|
||||
}
|
||||
|
||||
public static IQueryable<T> ApplySearchFilters<T>(this IQueryable<T> query, List<SearchItem> searchFilters)
|
||||
{
|
||||
// 1. Apply Search Filters (Contains/Text search)
|
||||
if (searchFilters.Any())
|
||||
{
|
||||
foreach (var search in searchFilters)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(search.Column) || string.IsNullOrWhiteSpace(search.Value)) continue;
|
||||
|
||||
// Generates: x.Column.Contains("Value")
|
||||
// Case insensitive logic can be handled here if needed
|
||||
query = query.Where($"{search.Column}.Contains(@0)", search.Value);
|
||||
}
|
||||
}
|
||||
return query;
|
||||
}
|
||||
|
||||
public static IQueryable<T> ApplyGroupByFilters<T>(this IQueryable<T> query, string groupByColumn)
|
||||
{
|
||||
query.GroupBy(groupByColumn, "it")
|
||||
.Select("new (Key, it as Items)"); // Reshape to { Key: "Value", Items: [...] }
|
||||
return query;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1162,35 +1162,23 @@ namespace Marco.Pms.Services.Helpers
|
||||
var processedByIds = model.Select(m => m.ProcessedById).ToList();
|
||||
var paidByIds = model.Select(m => m.PaidById).ToList();
|
||||
|
||||
var projectTask = Task.Run(async () =>
|
||||
var infraProjectTask = Task.Run(async () =>
|
||||
{
|
||||
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
|
||||
return await dbContext.Projects.AsNoTracking().Where(p => projectIds.Contains(p.Id) && p.TenantId == tenantId).ToListAsync();
|
||||
return await dbContext.Projects
|
||||
.AsNoTracking()
|
||||
.Where(p => projectIds.Contains(p.Id) && p.TenantId == tenantId)
|
||||
.Select(p => _mapper.Map<ProjectBasicMongoDB>(p))
|
||||
.ToListAsync();
|
||||
});
|
||||
var paidByTask = Task.Run(async () =>
|
||||
var serviceProjectTask = Task.Run(async () =>
|
||||
{
|
||||
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
|
||||
return await dbContext.Employees.Include(e => e.JobRole).AsNoTracking().Where(e => paidByIds.Contains(e.Id) && e.TenantId == tenantId).ToListAsync();
|
||||
});
|
||||
var createdByTask = Task.Run(async () =>
|
||||
{
|
||||
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
|
||||
return await dbContext.Employees.Include(e => e.JobRole).AsNoTracking().Where(e => createdByIds.Contains(e.Id) && e.TenantId == tenantId).ToListAsync();
|
||||
});
|
||||
var reviewedByTask = Task.Run(async () =>
|
||||
{
|
||||
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
|
||||
return await dbContext.Employees.Include(e => e.JobRole).AsNoTracking().Where(e => reviewedByIds.Contains(e.Id) && e.TenantId == tenantId).ToListAsync();
|
||||
});
|
||||
var approvedByTask = Task.Run(async () =>
|
||||
{
|
||||
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
|
||||
return await dbContext.Employees.Include(e => e.JobRole).AsNoTracking().Where(e => approvedByIds.Contains(e.Id) && e.TenantId == tenantId).ToListAsync();
|
||||
});
|
||||
var processedByTask = Task.Run(async () =>
|
||||
{
|
||||
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
|
||||
return await dbContext.Employees.Include(e => e.JobRole).AsNoTracking().Where(e => processedByIds.Contains(e.Id) && e.TenantId == tenantId).ToListAsync();
|
||||
return await dbContext.ServiceProjects
|
||||
.AsNoTracking()
|
||||
.Where(sp => projectIds.Contains(sp.Id) && sp.TenantId == tenantId)
|
||||
.Select(sp => _mapper.Map<ProjectBasicMongoDB>(sp))
|
||||
.ToListAsync();
|
||||
});
|
||||
var expenseCategoryTask = Task.Run(async () =>
|
||||
{
|
||||
@ -1202,6 +1190,15 @@ namespace Marco.Pms.Services.Helpers
|
||||
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
|
||||
return await dbContext.PaymentModeMatser.AsNoTracking().Where(pm => paymentModeIds.Contains(pm.Id)).ToListAsync();
|
||||
});
|
||||
|
||||
await Task.WhenAll(infraProjectTask, serviceProjectTask, expenseCategoryTask, paymentModeTask);
|
||||
|
||||
var projects = infraProjectTask.Result;
|
||||
projects.AddRange(serviceProjectTask.Result);
|
||||
|
||||
var expenseCategories = expenseCategoryTask.Result;
|
||||
var paymentModes = paymentModeTask.Result;
|
||||
|
||||
var statusMappingTask = Task.Run(async () =>
|
||||
{
|
||||
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
|
||||
@ -1218,6 +1215,43 @@ namespace Marco.Pms.Services.Helpers
|
||||
NextStatus = g.Select(s => s.NextStatus).OrderBy(s => s!.Name).ToList()
|
||||
}).ToListAsync();
|
||||
});
|
||||
var paidByTask = Task.Run(async () =>
|
||||
{
|
||||
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
|
||||
return await dbContext.Employees.Include(e => e.JobRole).AsNoTracking().Where(e => paidByIds.Contains(e.Id) && e.TenantId == tenantId).ToListAsync();
|
||||
});
|
||||
var createdByTask = Task.Run(async () =>
|
||||
{
|
||||
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
|
||||
return await dbContext.Employees.Include(e => e.JobRole).AsNoTracking().Where(e => createdByIds.Contains(e.Id) && e.TenantId == tenantId).ToListAsync();
|
||||
});
|
||||
|
||||
await Task.WhenAll(statusMappingTask, paidByTask, createdByTask);
|
||||
var statusMappings = statusMappingTask.Result;
|
||||
var paidBys = paidByTask.Result;
|
||||
var createdBys = createdByTask.Result;
|
||||
|
||||
var reviewedByTask = Task.Run(async () =>
|
||||
{
|
||||
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
|
||||
return await dbContext.Employees.Include(e => e.JobRole).AsNoTracking().Where(e => reviewedByIds.Contains(e.Id) && e.TenantId == tenantId).ToListAsync();
|
||||
});
|
||||
var approvedByTask = Task.Run(async () =>
|
||||
{
|
||||
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
|
||||
return await dbContext.Employees.Include(e => e.JobRole).AsNoTracking().Where(e => approvedByIds.Contains(e.Id) && e.TenantId == tenantId).ToListAsync();
|
||||
});
|
||||
var processedByTask = Task.Run(async () =>
|
||||
{
|
||||
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
|
||||
return await dbContext.Employees.Include(e => e.JobRole).AsNoTracking().Where(e => processedByIds.Contains(e.Id) && e.TenantId == tenantId).ToListAsync();
|
||||
});
|
||||
|
||||
await Task.WhenAll(reviewedByTask, approvedByTask, processedByTask);
|
||||
var reviewedBys = reviewedByTask.Result;
|
||||
var approvedBys = approvedByTask.Result;
|
||||
var processedBy = processedByTask.Result;
|
||||
|
||||
var statusTask = Task.Run(async () =>
|
||||
{
|
||||
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
|
||||
@ -1249,26 +1283,17 @@ namespace Marco.Pms.Services.Helpers
|
||||
.ToListAsync();
|
||||
});
|
||||
|
||||
// Await all prerequisite checks at once.
|
||||
await Task.WhenAll(projectTask, expenseCategoryTask, paymentModeTask, statusMappingTask, paidByTask, createdByTask, reviewedByTask, approvedByTask,
|
||||
processedByTask, statusTask, billAttachmentsTask);
|
||||
|
||||
var projects = projectTask.Result;
|
||||
var expenseCategories = expenseCategoryTask.Result;
|
||||
var paymentModes = paymentModeTask.Result;
|
||||
var statusMappings = statusMappingTask.Result;
|
||||
var paidBys = paidByTask.Result;
|
||||
var createdBys = createdByTask.Result;
|
||||
var reviewedBys = reviewedByTask.Result;
|
||||
var approvedBys = approvedByTask.Result;
|
||||
var processedBy = processedByTask.Result;
|
||||
|
||||
// Await all prerequisite checks at once.
|
||||
await Task.WhenAll(statusTask, billAttachmentsTask);
|
||||
var billAttachments = billAttachmentsTask.Result;
|
||||
|
||||
expenseList = model.Select(m =>
|
||||
{
|
||||
var response = _mapper.Map<ExpenseDetailsMongoDB>(m);
|
||||
|
||||
response.Project = projects.Where(p => p.Id == m.ProjectId).Select(p => _mapper.Map<ProjectBasicMongoDB>(p)).FirstOrDefault() ?? new ProjectBasicMongoDB();
|
||||
response.Project = projects.Where(p => Guid.Parse(p.Id) == m.ProjectId).FirstOrDefault() ?? new ProjectBasicMongoDB();
|
||||
response.PaidBy = paidBys.Where(p => p.Id == m.PaidById).Select(p => _mapper.Map<BasicEmployeeMongoDB>(p)).FirstOrDefault() ?? new BasicEmployeeMongoDB();
|
||||
response.CreatedBy = createdBys.Where(e => e.Id == m.CreatedById).Select(e => _mapper.Map<BasicEmployeeMongoDB>(e)).FirstOrDefault() ?? new BasicEmployeeMongoDB();
|
||||
response.ReviewedBy = reviewedBys.Where(e => e.Id == m.CreatedById).Select(e => _mapper.Map<BasicEmployeeMongoDB>(e)).FirstOrDefault();
|
||||
@ -1292,35 +1317,23 @@ namespace Marco.Pms.Services.Helpers
|
||||
}
|
||||
private async Task<ExpenseDetailsMongoDB> GetAllExpnesRelatedTablesForSingle(Expenses model, Guid tenantId)
|
||||
{
|
||||
var projectTask = Task.Run(async () =>
|
||||
var infraProjectTask = Task.Run(async () =>
|
||||
{
|
||||
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
|
||||
return await dbContext.Projects.AsNoTracking().FirstOrDefaultAsync(p => p.Id == model.ProjectId && p.TenantId == tenantId);
|
||||
return await dbContext.Projects
|
||||
.AsNoTracking()
|
||||
.Where(p => p.Id == model.ProjectId && p.TenantId == tenantId)
|
||||
.Select(p => _mapper.Map<ProjectBasicMongoDB>(p))
|
||||
.FirstOrDefaultAsync();
|
||||
});
|
||||
var paidByTask = Task.Run(async () =>
|
||||
var serviceProjectTask = Task.Run(async () =>
|
||||
{
|
||||
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
|
||||
return await dbContext.Employees.Include(e => e.JobRole).AsNoTracking().FirstOrDefaultAsync(e => e.Id == model.PaidById && e.TenantId == tenantId);
|
||||
});
|
||||
var createdByTask = Task.Run(async () =>
|
||||
{
|
||||
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
|
||||
return await dbContext.Employees.Include(e => e.JobRole).AsNoTracking().FirstOrDefaultAsync(e => e.Id == model.CreatedById && e.TenantId == tenantId);
|
||||
});
|
||||
var reviewedByTask = Task.Run(async () =>
|
||||
{
|
||||
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
|
||||
return await dbContext.Employees.Include(e => e.JobRole).AsNoTracking().FirstOrDefaultAsync(e => e.Id == model.ReviewedById && e.TenantId == tenantId);
|
||||
});
|
||||
var approvedByTask = Task.Run(async () =>
|
||||
{
|
||||
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
|
||||
return await dbContext.Employees.Include(e => e.JobRole).AsNoTracking().FirstOrDefaultAsync(e => e.Id == model.ApprovedById && e.TenantId == tenantId);
|
||||
});
|
||||
var processedByTask = Task.Run(async () =>
|
||||
{
|
||||
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
|
||||
return await dbContext.Employees.Include(e => e.JobRole).AsNoTracking().FirstOrDefaultAsync(e => e.Id == model.ProcessedById && e.TenantId == tenantId);
|
||||
return await dbContext.ServiceProjects
|
||||
.AsNoTracking()
|
||||
.Where(sp => sp.Id == model.ProjectId && sp.TenantId == tenantId)
|
||||
.Select(sp => _mapper.Map<ProjectBasicMongoDB>(sp))
|
||||
.FirstOrDefaultAsync();
|
||||
});
|
||||
var expenseCategoryTask = Task.Run(async () =>
|
||||
{
|
||||
@ -1332,6 +1345,12 @@ namespace Marco.Pms.Services.Helpers
|
||||
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
|
||||
return await dbContext.PaymentModeMatser.AsNoTracking().FirstOrDefaultAsync(pm => pm.Id == model.PaymentModeId);
|
||||
});
|
||||
|
||||
await Task.WhenAll(infraProjectTask, serviceProjectTask, expenseCategoryTask, paymentModeTask);
|
||||
var project = infraProjectTask.Result ?? serviceProjectTask.Result ?? new ProjectBasicMongoDB();
|
||||
var expenseCategory = expenseCategoryTask.Result;
|
||||
var paymentMode = paymentModeTask.Result;
|
||||
|
||||
var statusMappingTask = Task.Run(async () =>
|
||||
{
|
||||
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
|
||||
@ -1348,6 +1367,43 @@ namespace Marco.Pms.Services.Helpers
|
||||
NextStatus = g.Select(s => s.NextStatus).OrderBy(s => s!.Name).ToList()
|
||||
}).FirstOrDefaultAsync();
|
||||
});
|
||||
var paidByTask = Task.Run(async () =>
|
||||
{
|
||||
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
|
||||
return await dbContext.Employees.Include(e => e.JobRole).AsNoTracking().FirstOrDefaultAsync(e => e.Id == model.PaidById && e.TenantId == tenantId);
|
||||
});
|
||||
var createdByTask = Task.Run(async () =>
|
||||
{
|
||||
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
|
||||
return await dbContext.Employees.Include(e => e.JobRole).AsNoTracking().FirstOrDefaultAsync(e => e.Id == model.CreatedById && e.TenantId == tenantId);
|
||||
});
|
||||
|
||||
await Task.WhenAll(statusMappingTask, paidByTask, createdByTask);
|
||||
var statusMapping = statusMappingTask.Result;
|
||||
var paidBy = paidByTask.Result;
|
||||
var createdBy = createdByTask.Result;
|
||||
|
||||
var reviewedByTask = Task.Run(async () =>
|
||||
{
|
||||
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
|
||||
return await dbContext.Employees.Include(e => e.JobRole).AsNoTracking().FirstOrDefaultAsync(e => e.Id == model.ReviewedById && e.TenantId == tenantId);
|
||||
});
|
||||
var approvedByTask = Task.Run(async () =>
|
||||
{
|
||||
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
|
||||
return await dbContext.Employees.Include(e => e.JobRole).AsNoTracking().FirstOrDefaultAsync(e => e.Id == model.ApprovedById && e.TenantId == tenantId);
|
||||
});
|
||||
var processedByTask = Task.Run(async () =>
|
||||
{
|
||||
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
|
||||
return await dbContext.Employees.Include(e => e.JobRole).AsNoTracking().FirstOrDefaultAsync(e => e.Id == model.ProcessedById && e.TenantId == tenantId);
|
||||
});
|
||||
|
||||
await Task.WhenAll(reviewedByTask, approvedByTask, processedByTask);
|
||||
var reviewedBy = reviewedByTask.Result;
|
||||
var approvedBy = approvedByTask.Result;
|
||||
var processedBy = processedByTask.Result;
|
||||
|
||||
var statusTask = Task.Run(async () =>
|
||||
{
|
||||
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
|
||||
@ -1378,25 +1434,13 @@ namespace Marco.Pms.Services.Helpers
|
||||
.FirstOrDefaultAsync();
|
||||
});
|
||||
|
||||
// Await all prerequisite checks at once.
|
||||
await Task.WhenAll(projectTask, expenseCategoryTask, paymentModeTask, statusMappingTask, paidByTask, createdByTask, reviewedByTask, approvedByTask,
|
||||
processedByTask, statusTask, billAttachmentsTask);
|
||||
|
||||
var project = projectTask.Result;
|
||||
var expenseCategory = expenseCategoryTask.Result;
|
||||
var paymentMode = paymentModeTask.Result;
|
||||
var statusMapping = statusMappingTask.Result;
|
||||
var paidBy = paidByTask.Result;
|
||||
var createdBy = createdByTask.Result;
|
||||
var reviewedBy = reviewedByTask.Result;
|
||||
var approvedBy = approvedByTask.Result;
|
||||
var processedBy = processedByTask.Result;
|
||||
await Task.WhenAll(statusTask, billAttachmentsTask);
|
||||
var billAttachment = billAttachmentsTask.Result;
|
||||
|
||||
|
||||
var response = _mapper.Map<ExpenseDetailsMongoDB>(model);
|
||||
|
||||
response.Project = _mapper.Map<ProjectBasicMongoDB>(project);
|
||||
response.Project = project;
|
||||
response.PaidBy = _mapper.Map<BasicEmployeeMongoDB>(paidBy);
|
||||
response.CreatedBy = _mapper.Map<BasicEmployeeMongoDB>(createdBy);
|
||||
response.ReviewedBy = _mapper.Map<BasicEmployeeMongoDB>(reviewedBy);
|
||||
|
||||
@ -25,10 +25,9 @@ namespace Marco.Pms.Services.Helpers
|
||||
_cache = cache;
|
||||
}
|
||||
|
||||
public async Task<ProjectStatisticReport?> GetDailyProjectReportWithOutTenant(Guid projectId)
|
||||
public async Task<ProjectStatisticReport?> GetDailyProjectReportWithOutTenant(Guid projectId, DateTime reportDate)
|
||||
{
|
||||
// await _cache.GetBuildingAndFloorByWorkAreaId();
|
||||
DateTime reportDate = DateTime.UtcNow.AddDays(-1).Date;
|
||||
var project = await _cache.GetProjectDetailsWithBuildings(projectId);
|
||||
if (project == null)
|
||||
{
|
||||
|
||||
@ -162,6 +162,10 @@ namespace Marco.Pms.Services.MappingProfiles
|
||||
dest => dest.ProjectStatusId,
|
||||
opt => opt.MapFrom(src => Guid.Empty)
|
||||
);
|
||||
CreateMap<ProjectBasicMongoDB, BasicProjectVM>()
|
||||
.ForMember(
|
||||
dest => dest.Id,
|
||||
opt => opt.MapFrom(src => new Guid(src.Id)));
|
||||
|
||||
CreateMap<ProjectMongoDB, Project>()
|
||||
.ForMember(
|
||||
@ -197,9 +201,17 @@ namespace Marco.Pms.Services.MappingProfiles
|
||||
CreateMap<ServiceProject, BasicProjectVM>();
|
||||
CreateMap<ServiceProject, ServiceProjectVM>();
|
||||
CreateMap<ServiceProject, BasicServiceProjectVM>();
|
||||
CreateMap<ServiceProject, ProjectBasicMongoDB>();
|
||||
CreateMap<ServiceProject, ServiceProjectDetailsVM>();
|
||||
CreateMap<ServiceProjectAllocation, ServiceProjectAllocationVM>();
|
||||
|
||||
#region ======================================================= Project Branch =======================================================
|
||||
CreateMap<ProjectBranchDto, ProjectBranch>();
|
||||
CreateMap<ProjectBranch, ProjectBranchVM>();
|
||||
CreateMap<ProjectBranch, ProjectBranchDetailsVM>();
|
||||
CreateMap<ProjectBranch, BasicProjectBranchVM>();
|
||||
#endregion
|
||||
|
||||
//#region ======================================================= Talking Points =======================================================
|
||||
//CreateMap<TalkingPointDto, TalkingPoint>();
|
||||
//CreateMap<TalkingPoint, TalkingPointVM>();
|
||||
|
||||
@ -42,6 +42,7 @@
|
||||
<PackageReference Include="Serilog.Sinks.File" Version="6.0.0" />
|
||||
<PackageReference Include="Serilog.Sinks.MongoDB" Version="7.0.0" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" />
|
||||
<PackageReference Include="System.Linq.Dynamic.Core" Version="1.7.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@ -474,6 +474,12 @@ namespace Marco.Pms.Services.Service
|
||||
|
||||
return await _dbContext.ContactsEmails.Where(e => contactIds.Contains(e.ContactId)).ToListAsync();
|
||||
});
|
||||
await Task.WhenAll(contactTask, phoneTask, emailTask);
|
||||
|
||||
var contacts = contactTask.Result;
|
||||
var phones = phoneTask.Result;
|
||||
var emails = emailTask.Result;
|
||||
|
||||
var tagTask = Task.Run(async () =>
|
||||
{
|
||||
await using var _dbContext = await _dbContextFactory.CreateDbContextAsync();
|
||||
@ -493,11 +499,8 @@ namespace Marco.Pms.Services.Service
|
||||
return await _dbContext.ContactBucketMappings.Where(cb => contactIds.Contains(cb.ContactId)).ToListAsync();
|
||||
});
|
||||
|
||||
await Task.WhenAll(contactTask);
|
||||
await Task.WhenAll(tagTask, contactProjectTask, contactBucketTask);
|
||||
|
||||
var contacts = contactTask.Result;
|
||||
var phones = phoneTask.Result;
|
||||
var emails = emailTask.Result;
|
||||
var tags = tagTask.Result;
|
||||
var contactProjects = contactProjectTask.Result;
|
||||
var contactBuckets = contactBucketTask.Result;
|
||||
@ -587,6 +590,13 @@ namespace Marco.Pms.Services.Service
|
||||
_logger.LogWarning("Employee with ID {LoggedInEmployeeId} tries to update contact with ID {ContactId} is not found in database", loggedInEmployeeId);
|
||||
return ApiResponse<object>.ErrorResponse("Contact not found", "Contact not found", 404);
|
||||
}
|
||||
|
||||
var projectIds = await dbContext.ContactProjectMappings
|
||||
.AsNoTracking()
|
||||
.Where(cp => cp.ContactId == contact.Id && cp.TenantId == tenantId)
|
||||
.Select(cp => cp.ProjectId)
|
||||
.ToListAsync();
|
||||
|
||||
ContactProfileVM contactVM = _mapper.Map<ContactProfileVM>(contact);
|
||||
|
||||
var phonesTask = Task.Run(async () =>
|
||||
@ -609,21 +619,26 @@ namespace Marco.Pms.Services.Service
|
||||
.ToListAsync();
|
||||
});
|
||||
|
||||
var contactProjectsTask = Task.Run(async () =>
|
||||
var infraProjectTask = Task.Run(async () =>
|
||||
{
|
||||
await using var taskDbContext = await _dbContextFactory.CreateDbContextAsync();
|
||||
return await taskDbContext.ContactProjectMappings
|
||||
.AsNoTracking()
|
||||
.Include(cp => cp.Project)
|
||||
.Where(cp => cp.ContactId == contact.Id && cp.Project != null && cp.Project.TenantId == tenantId)
|
||||
.Select(cp => new BasicProjectVM
|
||||
{
|
||||
Id = cp.Project!.Id,
|
||||
Name = cp.Project.Name
|
||||
})
|
||||
.ToListAsync();
|
||||
await using var context = await _dbContextFactory.CreateDbContextAsync();
|
||||
return await context.Projects.Where(p => projectIds.Contains(p.Id) && p.TenantId == tenantId).Select(p => _mapper.Map<BasicProjectVM>(p)).ToListAsync();
|
||||
});
|
||||
|
||||
var serviceProjectTask = Task.Run(async () =>
|
||||
{
|
||||
await using var context = await _dbContextFactory.CreateDbContextAsync();
|
||||
return await context.ServiceProjects.Where(sp => projectIds.Contains(sp.Id) && sp.TenantId == tenantId).Select(sp => _mapper.Map<BasicProjectVM>(sp)).ToListAsync();
|
||||
});
|
||||
|
||||
await Task.WhenAll(phonesTask, emailsTask, infraProjectTask, serviceProjectTask);
|
||||
|
||||
contactVM.ContactPhones = phonesTask.Result;
|
||||
contactVM.ContactEmails = emailsTask.Result;
|
||||
var projects = infraProjectTask.Result;
|
||||
projects.AddRange(serviceProjectTask.Result);
|
||||
contactVM.Projects = projects;
|
||||
|
||||
var contactBucketsTask = Task.Run(async () =>
|
||||
{
|
||||
await using var taskDbContext = await _dbContextFactory.CreateDbContextAsync();
|
||||
@ -679,12 +694,10 @@ namespace Marco.Pms.Services.Service
|
||||
.ToListAsync();
|
||||
});
|
||||
|
||||
await Task.WhenAll(phonesTask, emailsTask, contactProjectsTask, contactBucketsTask, contactTagsTask, contactNotesTask);
|
||||
contactVM.ContactPhones = phonesTask.Result;
|
||||
contactVM.ContactEmails = emailsTask.Result;
|
||||
await Task.WhenAll(contactBucketsTask, contactTagsTask, contactNotesTask);
|
||||
|
||||
contactVM.Tags = contactTagsTask.Result;
|
||||
contactVM.Buckets = contactBucketsTask.Result;
|
||||
contactVM.Projects = contactProjectsTask.Result;
|
||||
contactVM.Notes = contactNotesTask.Result;
|
||||
_logger.LogInfo("Employee ID {EmployeeId} fetched profile of contact {ContactId}", loggedInEmployeeId, contact.Id);
|
||||
return ApiResponse<object>.SuccessResponse(contactVM, "Contact profile fetched successfully");
|
||||
@ -3289,11 +3302,18 @@ namespace Marco.Pms.Services.Service
|
||||
{
|
||||
if (!(dto.ProjectIds?.Any() ?? false)) return new List<ContactProjectMapping>();
|
||||
|
||||
var validProjectIds = await _context.Projects
|
||||
var infraProjectIds = await _context.Projects
|
||||
.Where(p => dto.ProjectIds.Contains(p.Id) && p.TenantId == tenantId)
|
||||
.Select(p => p.Id)
|
||||
.ToListAsync();
|
||||
|
||||
var serviceProjectIds = await _context.ServiceProjects
|
||||
.Where(sp => dto.ProjectIds.Contains(sp.Id) && sp.IsActive && sp.TenantId == tenantId)
|
||||
.Select(sp => sp.Id)
|
||||
.ToListAsync();
|
||||
|
||||
var validProjectIds = infraProjectIds.Concat(serviceProjectIds).Distinct().Where(p => p != Guid.Empty).ToList();
|
||||
|
||||
var mappings = validProjectIds.Select(projectId => new ContactProjectMapping
|
||||
{
|
||||
ProjectId = projectId,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -16,6 +16,7 @@ namespace Marco.Pms.Services.Service
|
||||
{
|
||||
private readonly IDbContextFactory<ApplicationDbContext> _dbContextFactory;
|
||||
private readonly IServiceScopeFactory _serviceScopeFactory;
|
||||
private readonly ILoggingService _logger;
|
||||
|
||||
private static readonly Guid Review = Guid.Parse("6537018f-f4e9-4cb3-a210-6c3b2da999d7");
|
||||
private static readonly Guid RejectedByReviewer = Guid.Parse("965eda62-7907-4963-b4a1-657fb0b2724b");
|
||||
@ -25,10 +26,12 @@ namespace Marco.Pms.Services.Service
|
||||
private static readonly Guid Processed = Guid.Parse("61578360-3a49-4c34-8604-7b35a3787b95");
|
||||
|
||||
public FirebaseService(IDbContextFactory<ApplicationDbContext> dbContextFactory,
|
||||
IServiceScopeFactory serviceScopeFactory)
|
||||
IServiceScopeFactory serviceScopeFactory,
|
||||
ILoggingService logger)
|
||||
{
|
||||
_dbContextFactory = dbContextFactory ?? throw new ArgumentNullException(nameof(dbContextFactory));
|
||||
_serviceScopeFactory = serviceScopeFactory ?? throw new ArgumentNullException(nameof(serviceScopeFactory));
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
|
||||
// Auth Controller
|
||||
@ -124,171 +127,174 @@ namespace Marco.Pms.Services.Service
|
||||
await SendMessageToMultipleDevicesOnlyDataAsync(registrationTokensForData, data);
|
||||
}
|
||||
|
||||
// Attendance Controller
|
||||
public async Task SendAttendanceMessageAsync(Guid projectId, string name, ATTENDANCE_MARK_TYPE markType, Guid employeeId, Guid tenantId)
|
||||
#region =================================================================== Attendance Functions ===================================================================
|
||||
|
||||
/// <summary>
|
||||
/// Sends attendance-related notifications for the specified project and employee attendance action.
|
||||
/// </summary>
|
||||
/// <param name="projectId">The Id of the project where attendance is marked.</param>
|
||||
/// <param name="name">Name of the employee marking attendance.</param>
|
||||
/// <param name="markType">Type of attendance mark action.</param>
|
||||
/// <param name="employeeId">Employee for whom attendance is marked.</param>
|
||||
/// <param name="origin">Origin of the request (optional), used to filter notifications.</param>
|
||||
/// <param name="loggedInEmployeeId">Employee Id of the caller (to exclude self-notifications).</param>
|
||||
/// <param name="tenantId">Tenant identifier for multi-tenant setup.</param>
|
||||
/// <returns>Task representing the asynchronous operation.</returns>
|
||||
public async Task SendAttendanceMessageAsync(Guid projectId, string name, ATTENDANCE_MARK_TYPE markType, Guid employeeId, string origin, Guid loggedInEmployeeId, Guid tenantId)
|
||||
{
|
||||
await using var _context = await _dbContextFactory.CreateDbContextAsync();
|
||||
var projectTask = Task.Run(async () =>
|
||||
try
|
||||
{
|
||||
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
|
||||
return await dbContext.ProjectAllocations
|
||||
.Include(pa => pa.Project)
|
||||
.Where(pa => pa.ProjectId == projectId && pa.IsActive && pa.Project != null)
|
||||
.GroupBy(pa => pa.ProjectId)
|
||||
.Select(g => new
|
||||
|
||||
// Fetch project details and assigned employees grouped by ProjectId
|
||||
var project = await dbContext.ProjectAllocations
|
||||
.Include(pa => pa.Project)
|
||||
.Where(pa => pa.ProjectId == projectId && pa.IsActive && pa.Project != null)
|
||||
.GroupBy(pa => pa.ProjectId)
|
||||
.Select(g => new
|
||||
{
|
||||
ProjectName = g.Select(pa => pa.Project!.Name).FirstOrDefault(),
|
||||
EmployeeIds = g.Select(pa => pa.EmployeeId).Distinct().ToList()
|
||||
})
|
||||
.FirstOrDefaultAsync();
|
||||
|
||||
if (project == null)
|
||||
{
|
||||
ProjectName = g.Select(pa => pa.Project!.Name).FirstOrDefault(),
|
||||
EmployeeIds = g.Select(pa => pa.EmployeeId).Distinct().ToList()
|
||||
}).FirstOrDefaultAsync();
|
||||
});
|
||||
_logger.LogWarning("No active project allocations found for ProjectId: {ProjectId}", projectId);
|
||||
return; // or throw if critical
|
||||
}
|
||||
|
||||
var teamAttendanceRoleTask = Task.Run(async () =>
|
||||
{
|
||||
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
|
||||
return await dbContext.RolePermissionMappings
|
||||
.Where(rp => rp.FeaturePermissionId == PermissionsMaster.TeamAttendance)
|
||||
.Select(rp => rp.ApplicationRoleId).ToListAsync();
|
||||
});
|
||||
var regularizeAttendanceRoleTask = Task.Run(async () =>
|
||||
{
|
||||
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
|
||||
return await dbContext.RolePermissionMappings
|
||||
.Where(rp => rp.FeaturePermissionId == PermissionsMaster.RegularizeAttendance)
|
||||
.Select(rp => rp.ApplicationRoleId).ToListAsync();
|
||||
});
|
||||
var manageProjectsRoleTask = Task.Run(async () =>
|
||||
{
|
||||
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
|
||||
return await dbContext.RolePermissionMappings
|
||||
.Where(rp => rp.FeaturePermissionId == PermissionsMaster.ManageProject)
|
||||
.Select(rp => rp.ApplicationRoleId).ToListAsync();
|
||||
});
|
||||
var projectAssignedEmployeeIds = project.EmployeeIds;
|
||||
|
||||
await Task.WhenAll(projectTask, teamAttendanceRoleTask, manageProjectsRoleTask, regularizeAttendanceRoleTask);
|
||||
// Load required role IDs in parallel for efficiency
|
||||
var teamAttendanceRoleIdsTask = dbContext.RolePermissionMappings.Where(rp => rp.FeaturePermissionId == PermissionsMaster.TeamAttendance).Select(rp => rp.ApplicationRoleId).ToListAsync();
|
||||
var regularizeAttendanceRoleIdsTask = dbContext.RolePermissionMappings.Where(rp => rp.FeaturePermissionId == PermissionsMaster.RegularizeAttendance).Select(rp => rp.ApplicationRoleId).ToListAsync();
|
||||
var manageProjectsRoleIdsTask = dbContext.RolePermissionMappings.Where(rp => rp.FeaturePermissionId == PermissionsMaster.ManageProject).Select(rp => rp.ApplicationRoleId).ToListAsync();
|
||||
|
||||
var teamAttendanceRoleIds = teamAttendanceRoleTask.Result;
|
||||
var regularizeAttendanceRoleIds = regularizeAttendanceRoleTask.Result;
|
||||
var manageProjectsRoleIds = manageProjectsRoleTask.Result;
|
||||
var project = projectTask.Result;
|
||||
await Task.WhenAll(teamAttendanceRoleIdsTask, regularizeAttendanceRoleIdsTask, manageProjectsRoleIdsTask);
|
||||
|
||||
List<Guid> projectAssignedEmployeeIds = project?.EmployeeIds ?? new List<Guid>();
|
||||
var teamAttendanceRoleIds = teamAttendanceRoleIdsTask.Result;
|
||||
var regularizeAttendanceRoleIds = regularizeAttendanceRoleIdsTask.Result;
|
||||
var manageProjectsRoleIds = manageProjectsRoleIdsTask.Result;
|
||||
|
||||
var employeeIdsTask = Task.Run(async () =>
|
||||
{
|
||||
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
|
||||
return await dbContext.EmployeeRoleMappings
|
||||
.Where(er => (projectAssignedEmployeeIds.Contains(er.EmployeeId) || manageProjectsRoleIds.Contains(er.RoleId)) && teamAttendanceRoleIds.Contains(er.RoleId))
|
||||
.Select(er => er.EmployeeId)
|
||||
.ToListAsync();
|
||||
});
|
||||
var teamEmployeeIdsTask = Task.Run(async () =>
|
||||
{
|
||||
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
|
||||
return await dbContext.EmployeeRoleMappings
|
||||
.Where(er => projectAssignedEmployeeIds.Contains(er.EmployeeId) || manageProjectsRoleIds.Contains(er.RoleId))
|
||||
.Select(er => er.EmployeeId)
|
||||
.ToListAsync();
|
||||
});
|
||||
// Fetch employees eligible for attendance notifications
|
||||
var employeeIds = await dbContext.EmployeeRoleMappings
|
||||
.Where(er => (projectAssignedEmployeeIds.Contains(er.EmployeeId) || manageProjectsRoleIds.Contains(er.RoleId))
|
||||
&& teamAttendanceRoleIds.Contains(er.RoleId))
|
||||
.Select(er => er.EmployeeId)
|
||||
.Distinct()
|
||||
.ToListAsync();
|
||||
|
||||
await Task.WhenAll(employeeIdsTask, teamEmployeeIdsTask);
|
||||
// Fetch team employees (for data-only notifications)
|
||||
var teamEmployeeIds = await dbContext.EmployeeRoleMappings
|
||||
.Where(er => projectAssignedEmployeeIds.Contains(er.EmployeeId) || manageProjectsRoleIds.Contains(er.RoleId))
|
||||
.Select(er => er.EmployeeId)
|
||||
.Distinct()
|
||||
.ToListAsync();
|
||||
|
||||
var employeeIds = employeeIdsTask.Result;
|
||||
var teamEmployeeIds = teamEmployeeIdsTask.Result;
|
||||
List<Guid> messageNotificationIds = new();
|
||||
|
||||
var mesaageNotificationIds = new List<Guid>();
|
||||
|
||||
Notification notificationFirebase;
|
||||
switch (markType)
|
||||
{
|
||||
case ATTENDANCE_MARK_TYPE.CHECK_IN:
|
||||
notificationFirebase = new Notification
|
||||
// Construct notification content based on attendance mark type
|
||||
Notification notificationFirebase = markType switch
|
||||
{
|
||||
ATTENDANCE_MARK_TYPE.CHECK_IN => new Notification
|
||||
{
|
||||
Title = "Attendance Update",
|
||||
Body = $" {name} has checked in for project {project?.ProjectName ?? ""}."
|
||||
};
|
||||
mesaageNotificationIds.AddRange(employeeIds);
|
||||
break;
|
||||
case ATTENDANCE_MARK_TYPE.CHECK_OUT:
|
||||
notificationFirebase = new Notification
|
||||
Body = $"{name} has checked in for project {project.ProjectName}."
|
||||
},
|
||||
ATTENDANCE_MARK_TYPE.CHECK_OUT => new Notification
|
||||
{
|
||||
Title = "Attendance Update",
|
||||
Body = $" {name} has checked out for project {project?.ProjectName ?? ""}."
|
||||
};
|
||||
mesaageNotificationIds.AddRange(employeeIds);
|
||||
break;
|
||||
case ATTENDANCE_MARK_TYPE.REQUEST_REGULARIZE:
|
||||
notificationFirebase = new Notification
|
||||
Body = $"{name} has checked out for project {project.ProjectName}."
|
||||
},
|
||||
ATTENDANCE_MARK_TYPE.REQUEST_REGULARIZE => new Notification
|
||||
{
|
||||
Title = "Regularization Request",
|
||||
Body = $" {name} has submitted a regularization request for project {project?.ProjectName ?? ""}."
|
||||
};
|
||||
mesaageNotificationIds = await _context.EmployeeRoleMappings
|
||||
.Where(er => (projectAssignedEmployeeIds.Contains(er.EmployeeId) || manageProjectsRoleIds.Contains(er.RoleId)) && regularizeAttendanceRoleIds.Contains(er.RoleId))
|
||||
.Select(er => er.EmployeeId)
|
||||
.ToListAsync();
|
||||
break;
|
||||
case ATTENDANCE_MARK_TYPE.REGULARIZE:
|
||||
notificationFirebase = new Notification
|
||||
Body = $"{name} has submitted a regularization request for project {project.ProjectName}."
|
||||
},
|
||||
ATTENDANCE_MARK_TYPE.REGULARIZE => new Notification
|
||||
{
|
||||
Title = " Regularization Approved",
|
||||
Body = $" {name}'s regularization request for project {project?.ProjectName ?? ""} has been accepted."
|
||||
};
|
||||
mesaageNotificationIds.Add(employeeId);
|
||||
break;
|
||||
case ATTENDANCE_MARK_TYPE.REGULARIZE_REJECT:
|
||||
notificationFirebase = new Notification
|
||||
Title = "Regularization Approved",
|
||||
Body = $"{name}'s regularization request for project {project.ProjectName} has been accepted."
|
||||
},
|
||||
ATTENDANCE_MARK_TYPE.REGULARIZE_REJECT => new Notification
|
||||
{
|
||||
Title = "Regularization Denied",
|
||||
Body = $" {name}'s regularization request for project {project?.ProjectName ?? ""} has been rejected."
|
||||
};
|
||||
mesaageNotificationIds.Add(employeeId);
|
||||
break;
|
||||
default:
|
||||
notificationFirebase = new Notification
|
||||
Body = $"{name}'s regularization request for project {project.ProjectName} has been rejected."
|
||||
},
|
||||
_ => new Notification
|
||||
{
|
||||
Title = "Attendance Update",
|
||||
Body = $" {name} has update his/her attendance for project {project?.ProjectName ?? ""}."
|
||||
};
|
||||
break;
|
||||
Body = $"{name} has updated attendance for project {project.ProjectName}."
|
||||
}
|
||||
};
|
||||
|
||||
// Set notification recipients based on type
|
||||
if (markType == ATTENDANCE_MARK_TYPE.REQUEST_REGULARIZE)
|
||||
{
|
||||
messageNotificationIds = await dbContext.EmployeeRoleMappings
|
||||
.Where(er => (projectAssignedEmployeeIds.Contains(er.EmployeeId) || manageProjectsRoleIds.Contains(er.RoleId))
|
||||
&& regularizeAttendanceRoleIds.Contains(er.RoleId))
|
||||
.Select(er => er.EmployeeId)
|
||||
.Distinct()
|
||||
.ToListAsync();
|
||||
}
|
||||
else if (markType == ATTENDANCE_MARK_TYPE.REGULARIZE || markType == ATTENDANCE_MARK_TYPE.REGULARIZE_REJECT)
|
||||
{
|
||||
messageNotificationIds.Add(employeeId);
|
||||
}
|
||||
else
|
||||
{
|
||||
messageNotificationIds.AddRange(employeeIds);
|
||||
}
|
||||
|
||||
var dataPayload = new Dictionary<string, string>
|
||||
{
|
||||
{ "Keyword", "Attendance" },
|
||||
{ "ProjectId", projectId.ToString() },
|
||||
{ "Action", markType.ToString() }
|
||||
};
|
||||
|
||||
// Filter out current (logged-in) employee from notifications if origin is not provided (optional)
|
||||
if (string.IsNullOrWhiteSpace(origin))
|
||||
{
|
||||
messageNotificationIds = messageNotificationIds.Where(e => e != Guid.Empty && e != loggedInEmployeeId).Distinct().ToList();
|
||||
teamEmployeeIds = teamEmployeeIds.Where(e => e != Guid.Empty && e != loggedInEmployeeId).Distinct().ToList();
|
||||
}
|
||||
|
||||
// Fetch FCM tokens for notification recipients
|
||||
var registrationTokensForNotification = messageNotificationIds.Count > 0
|
||||
? await dbContext.FCMTokenMappings.Where(ft => messageNotificationIds.Contains(ft.EmployeeId) && ft.ExpiredAt >= DateTime.UtcNow && ft.TenantId == tenantId).Select(ft => ft.FcmToken).ToListAsync()
|
||||
: new List<string>();
|
||||
|
||||
// Fetch FCM tokens for data-only notification recipients
|
||||
var registrationTokensForData = teamEmployeeIds.Count > 0
|
||||
? await dbContext.FCMTokenMappings.Where(ft => teamEmployeeIds.Contains(ft.EmployeeId) && ft.ExpiredAt >= DateTime.UtcNow && ft.TenantId == tenantId).Select(ft => ft.FcmToken).ToListAsync()
|
||||
: new List<string>();
|
||||
|
||||
// Send notifications concurrently
|
||||
var sendNotificationTask = registrationTokensForNotification.Count > 0
|
||||
? SendMessageToMultipleDevicesWithDataAsync(registrationTokensForNotification, notificationFirebase, dataPayload)
|
||||
: Task.CompletedTask;
|
||||
|
||||
var sendDataOnlyTask = registrationTokensForData.Count > 0
|
||||
? SendMessageToMultipleDevicesOnlyDataAsync(registrationTokensForData, dataPayload)
|
||||
: Task.CompletedTask;
|
||||
|
||||
await Task.WhenAll(sendNotificationTask, sendDataOnlyTask);
|
||||
|
||||
_logger.LogInfo("Attendance message sent successfully for ProjectId {ProjectId}, MarkType {MarkType}, EmployeeId {EmployeeId}, Origin {Origin}",
|
||||
projectId, markType, employeeId, origin);
|
||||
}
|
||||
|
||||
// List of device registration tokens to send the message to
|
||||
|
||||
var data = new Dictionary<string, string>()
|
||||
{
|
||||
{ "Keyword", "Attendance" },
|
||||
{ "ProjectId", projectId.ToString() },
|
||||
{ "Action", markType.ToString() }
|
||||
};
|
||||
|
||||
var registrationTokensForNotificationTask = Task.Run(async () =>
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (mesaageNotificationIds.Any())
|
||||
{
|
||||
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
|
||||
var registrationTokensForNotification = await dbContext.FCMTokenMappings
|
||||
.Where(ft => mesaageNotificationIds.Contains(ft.EmployeeId) && ft.ExpiredAt >= DateTime.UtcNow && ft.TenantId == tenantId)
|
||||
.Select(ft => ft.FcmToken).ToListAsync();
|
||||
|
||||
await SendMessageToMultipleDevicesWithDataAsync(registrationTokensForNotification, notificationFirebase, data);
|
||||
}
|
||||
});
|
||||
var registrationTokensForDataTask = Task.Run(async () =>
|
||||
{
|
||||
if (teamEmployeeIds.Any())
|
||||
{
|
||||
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
|
||||
var registrationTokensForData = await dbContext.FCMTokenMappings
|
||||
.Where(ft => teamEmployeeIds.Contains(ft.EmployeeId) && ft.ExpiredAt >= DateTime.UtcNow && ft.TenantId == tenantId)
|
||||
.Select(ft => ft.FcmToken).ToListAsync();
|
||||
|
||||
await SendMessageToMultipleDevicesOnlyDataAsync(registrationTokensForData, data);
|
||||
}
|
||||
});
|
||||
|
||||
await Task.WhenAll(registrationTokensForNotificationTask, registrationTokensForDataTask);
|
||||
_logger.LogError(ex, "Error sending attendance message for ProjectId {ProjectId}, MarkType {MarkType}, EmployeeId {EmployeeId}, Origin {Origin}",
|
||||
projectId, markType, employeeId, origin);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#endregion
|
||||
// Task Controller
|
||||
public async Task SendAssignTaskMessageAsync(Guid workItemId, string name, List<Guid> teamMembers, Guid tenantId)
|
||||
{
|
||||
|
||||
@ -3,6 +3,7 @@ using Marco.Pms.Model.Employees;
|
||||
using Marco.Pms.Model.Entitlements;
|
||||
using Marco.Pms.Services.Helpers;
|
||||
using MarcoBMS.Services.Helpers;
|
||||
using MarcoBMS.Services.Service;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Marco.Pms.Services.Service
|
||||
@ -12,49 +13,18 @@ namespace Marco.Pms.Services.Service
|
||||
private readonly ApplicationDbContext _context;
|
||||
private readonly RolesHelper _rolesHelper;
|
||||
private readonly CacheUpdateHelper _cache;
|
||||
private readonly ILoggingService _logger;
|
||||
private readonly Guid tenantId;
|
||||
|
||||
public PermissionServices(ApplicationDbContext context, RolesHelper rolesHelper, CacheUpdateHelper cache, UserHelper userHelper)
|
||||
public PermissionServices(ApplicationDbContext context, RolesHelper rolesHelper, CacheUpdateHelper cache, ILoggingService logger, UserHelper userHelper)
|
||||
{
|
||||
_context = context;
|
||||
_rolesHelper = rolesHelper;
|
||||
_cache = cache;
|
||||
_logger = logger;
|
||||
tenantId = userHelper.GetTenantId();
|
||||
}
|
||||
|
||||
//public async Task<bool> HasPermission(Guid featurePermissionId, Guid employeeId, Guid? projectId = null)
|
||||
//{
|
||||
// var featurePermissionIds = await _cache.GetPermissions(employeeId);
|
||||
// if (featurePermissionIds == null)
|
||||
// {
|
||||
// List<FeaturePermission> featurePermission = await _rolesHelper.GetFeaturePermissionByEmployeeId(employeeId);
|
||||
// featurePermissionIds = featurePermission.Select(fp => fp.Id).ToList();
|
||||
// }
|
||||
// if (projectId != null)
|
||||
// {
|
||||
// var projectLevelPerissionIds = await _context.ProjectLevelPermissionMappings
|
||||
// .Where(pl => pl.ProjectId == projectId.Value && pl.EmployeeId == employeeId).Select(pl => pl.PermissionId).ToListAsync();
|
||||
|
||||
// var projectLevelModuleIds = new HashSet<Guid>
|
||||
// {
|
||||
// Guid.Parse("53176ebf-c75d-42e5-839f-4508ffac3def"),
|
||||
// Guid.Parse("9d4b5489-2079-40b9-bd77-6e1bf90bc19f"),
|
||||
// Guid.Parse("81ab8a87-8ccd-4015-a917-0627cee6a100"),
|
||||
// Guid.Parse("52c9cf54-1eb2-44d2-81bb-524cf29c0a94"),
|
||||
// Guid.Parse("a8cf4331-8f04-4961-8360-a3f7c3cc7462")
|
||||
// };
|
||||
|
||||
// var allProjectLevelPermissionIds = await _context.FeaturePermissions
|
||||
// .Where(fp => projectLevelModuleIds.Contains(fp.FeatureId) && !projectLevelPerissionIds.Contains(fp.Id)).Select(fp => fp.Id).ToListAsync();
|
||||
// featurePermissionIds.RemoveRange(allProjectLevelPermissionIds);
|
||||
|
||||
// featurePermissionIds.AddRange(projectLevelPerissionIds);
|
||||
// featurePermissionIds = featurePermissionIds.Distinct().ToList();
|
||||
// }
|
||||
// var hasPermission = featurePermissionIds.Contains(featurePermissionId);
|
||||
// return hasPermission;
|
||||
//}
|
||||
|
||||
/// <summary>
|
||||
/// Checks whether an employee has a specific feature permission, optionally within a project context.
|
||||
/// </summary>
|
||||
@ -156,5 +126,78 @@ namespace Marco.Pms.Services.Service
|
||||
}
|
||||
return projectIds.Contains(projectId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines if an employee has permission to access a specific service project.
|
||||
/// Permission is granted if the user is directly allocated to the project OR
|
||||
/// assigned to any active job ticket within the project.
|
||||
/// </summary>
|
||||
/// <param name="loggedInEmployeeId">The ID of the user requesting access.</param>
|
||||
/// <param name="projectId">The ID of the project to access.</param>
|
||||
/// <returns>True if access is allowed, otherwise False.</returns>
|
||||
public async Task<bool> HasServiceProjectPermission(Guid loggedInEmployeeId, Guid projectId)
|
||||
{
|
||||
Guid ReviewDoneStatus = Guid.Parse("ed10ab57-dbaa-4ca5-8ecd-56745dcbdbd7");
|
||||
Guid ClosedStatus = Guid.Parse("3ddeefb5-ae3c-4e10-a922-35e0a452bb69");
|
||||
// 1. Input Validation
|
||||
if (loggedInEmployeeId == Guid.Empty || projectId == Guid.Empty)
|
||||
{
|
||||
_logger.LogWarning("Permission check failed: Invalid input parameters. EmployeeId: {EmployeeId}, ProjectId: {ProjectId}", loggedInEmployeeId, projectId);
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
_logger.LogInfo("Starting permission check for Employee: {EmployeeId} on Project: {ProjectId}", loggedInEmployeeId, projectId);
|
||||
|
||||
// 2. Check Level 1: Is the user a generic Team Member of the project?
|
||||
// This is usually the most common case, so checking this first saves complex query execution.
|
||||
bool isTeamMember = await _context.ServiceProjectAllocations
|
||||
.AsNoTracking() // Optimization: Read-only query does not need tracking
|
||||
.AnyAsync(spa => spa.ProjectId == projectId
|
||||
&& spa.EmployeeId == loggedInEmployeeId
|
||||
&& spa.IsActive
|
||||
&& spa.TenantId == tenantId);
|
||||
|
||||
if (isTeamMember)
|
||||
{
|
||||
_logger.LogInfo("Access Granted: User {EmployeeId} is a team member of Project {ProjectId}.", loggedInEmployeeId, projectId);
|
||||
return true;
|
||||
}
|
||||
|
||||
// 3. Check Level 2: Is the user assigned to any ACTIVE specific Job Ticket?
|
||||
// Optimization: Combined the check for JobTicket and Mapping into a single Join query.
|
||||
// This prevents pulling a list of JobIds into memory (fixing memory bloat) and reduces DB roundtrips.
|
||||
bool hasActiveJobAssignment = await _context.JobTickets
|
||||
.AsNoTracking()
|
||||
.Where(jt => jt.ProjectId == projectId
|
||||
&& jt.StatusId != ReviewDoneStatus
|
||||
&& jt.StatusId != ClosedStatus
|
||||
&& jt.IsActive)
|
||||
.Join(_context.JobEmployeeMappings,
|
||||
ticket => ticket.Id,
|
||||
mapping => mapping.JobTicketId,
|
||||
(ticket, mapping) => mapping)
|
||||
.AnyAsync(mapping => mapping.AssigneeId == loggedInEmployeeId
|
||||
&& mapping.TenantId == tenantId);
|
||||
|
||||
if (hasActiveJobAssignment)
|
||||
{
|
||||
_logger.LogInfo("Access Granted: User {EmployeeId} is assigned active tickets in Project {ProjectId}.", loggedInEmployeeId, projectId);
|
||||
return true;
|
||||
}
|
||||
|
||||
// 4. Default Deny
|
||||
_logger.LogWarning("Access Denied: User {EmployeeId} has no permissions for Project {ProjectId}.", loggedInEmployeeId, projectId);
|
||||
return false;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// 5. Robust Error Handling
|
||||
// Log the full stack trace for debugging, but return false to maintain security (fail-closed).
|
||||
_logger.LogError(ex, "An error occurred while checking permissions for Employee: {EmployeeId} on Project: {ProjectId}", loggedInEmployeeId, projectId);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -220,6 +220,12 @@ namespace Marco.Pms.Services.Service
|
||||
_logger.LogInfo("Cache HIT. All {ProjectCount} projects found in cache.", projectIds.Count);
|
||||
}
|
||||
|
||||
if (pageNumber <= 0 || pageSize <= 0)
|
||||
{
|
||||
_logger.LogInfo("Successfully retrieved a total of {ProjectCount} projects.", responseVms.Count);
|
||||
return ApiResponse<object>.SuccessResponse(responseVms, "Projects retrieved successfully.", 200);
|
||||
}
|
||||
|
||||
var totalEntites = responseVms.Count;
|
||||
var totalPages = (int)Math.Ceiling((double)totalEntites / pageSize);
|
||||
|
||||
|
||||
@ -12,7 +12,7 @@ namespace Marco.Pms.Services.Service.ServiceInterfaces
|
||||
|
||||
Task SendEmployeeSuspendMessageAsync(Guid employeeId, Guid tenantId);
|
||||
|
||||
Task SendAttendanceMessageAsync(Guid projectId, string Name, ATTENDANCE_MARK_TYPE markType, Guid employeeId, Guid tenantId);
|
||||
Task SendAttendanceMessageAsync(Guid projectId, string Name, ATTENDANCE_MARK_TYPE markType, Guid employeeId, string origin, Guid loggedInEmployeeId, Guid tenantId);
|
||||
Task SendAssignTaskMessageAsync(Guid workItemId, string name, List<Guid> teamMembers, Guid tenantId);
|
||||
Task SendReportTaskMessageAsync(Guid taskAllocationId, string name, Guid tenantId);
|
||||
Task SendTaskCommentMessageAsync(Guid taskAllocationId, string name, Guid tenantId);
|
||||
|
||||
@ -15,6 +15,15 @@ namespace Marco.Pms.Services.Service.ServiceInterfaces
|
||||
Task<ApiResponse<object>> DeActivateServiceProjectAsync(Guid id, bool isActive, Employee loggedInEmployee, Guid tenantId);
|
||||
#endregion
|
||||
|
||||
#region =================================================================== Project Branch Functions ===================================================================
|
||||
Task<ApiResponse<object>> GetProjectBranchListByProjectAsync(Guid projectId, bool isActive, string? searchString, int pageNumber, int pageSize, Employee loggedInEmployee, Guid tenantId);
|
||||
Task<ApiResponse<object>> GetProjectBranchDetailsAsync(Guid id, Employee loggedInEmployee, Guid tenantId);
|
||||
Task<ApiResponse<object>> GetBranchTypeListAsync(string? searchString, Employee loggedInEmployee, Guid tenantId);
|
||||
Task<ApiResponse<object>> CreateProjectBranchAsync(ProjectBranchDto model, Employee loggedInEmployee, Guid tenantId);
|
||||
Task<ApiResponse<object>> UpdateProjectBranchAsync(Guid id, ProjectBranchDto model, Employee loggedInEmployee, Guid tenantId);
|
||||
Task<ApiResponse<object>> DeleteProjectBranchAsync(Guid id, bool isActive, Employee loggedInEmployee, Guid tenantId);
|
||||
#endregion
|
||||
|
||||
#region =================================================================== Service Project Allocation Functions ===================================================================
|
||||
Task<ApiResponse<object>> GetServiceProjectAllocationListAsync(Guid? projectId, Guid? employeeId, bool isActive, Employee loggedInEmployee, Guid tenantId);
|
||||
Task<ApiResponse<object>> ManageServiceProjectAllocationAsync(List<ServiceProjectAllocationDto> model, Employee loggedInEmployee, Guid tenantId);
|
||||
@ -25,7 +34,7 @@ namespace Marco.Pms.Services.Service.ServiceInterfaces
|
||||
#endregion
|
||||
|
||||
#region =================================================================== Job Tickets Functions ===================================================================
|
||||
Task<ApiResponse<object>> GetJobTicketsListAsync(Guid? projectId, int pageNumber, int pageSize, bool isActive, Employee loggedInEmployee, Guid tenantId);
|
||||
Task<ApiResponse<object>> GetJobTicketsListAsync(Guid? projectId, int pageNumber, int pageSize, bool isActive, bool isArchive, Employee loggedInEmployee, Guid tenantId);
|
||||
Task<ApiResponse<object>> GetJobTicketDetailsAsync(Guid id, Employee loggedInEmployee, Guid tenantId);
|
||||
Task<ApiResponse<object>> GetJobTagListAsync(Employee loggedInEmployee, Guid tenantId);
|
||||
Task<ApiResponse<object>> CreateJobTicketAsync(CreateJobTicketDto model, Employee loggedInEmployee, Guid tenantId);
|
||||
|
||||
@ -30,12 +30,14 @@ namespace Marco.Pms.Services.Service
|
||||
private readonly ILoggingService _logger;
|
||||
private readonly S3UploadService _s3Service;
|
||||
private readonly IMapper _mapper;
|
||||
private readonly UtilityMongoDBHelper _updateLogHelper;
|
||||
|
||||
|
||||
private readonly Guid NewStatus = Guid.Parse("32d76a02-8f44-4aa0-9b66-c3716c45a918");
|
||||
private readonly Guid AssignedStatus = Guid.Parse("cfa1886d-055f-4ded-84c6-42a2a8a14a66");
|
||||
private readonly Guid InProgressStatus = Guid.Parse("5a6873a5-fed7-4745-a52f-8f61bf3bd72d");
|
||||
private readonly Guid ReviewStatus = Guid.Parse("aab71020-2fb8-44d9-9430-c9a7e9bf33b0");
|
||||
private readonly Guid DoneStatus = Guid.Parse("ed10ab57-dbaa-4ca5-8ecd-56745dcbdbd7");
|
||||
private readonly Guid WorkDoneStatus = Guid.Parse("aab71020-2fb8-44d9-9430-c9a7e9bf33b0");
|
||||
private readonly Guid ReviewDoneStatus = Guid.Parse("ed10ab57-dbaa-4ca5-8ecd-56745dcbdbd7");
|
||||
private readonly Guid ClosedStatus = Guid.Parse("3ddeefb5-ae3c-4e10-a922-35e0a452bb69");
|
||||
private readonly Guid OnHoldStatus = Guid.Parse("75a0c8b8-9c6a-41af-80bf-b35bab722eb2");
|
||||
|
||||
@ -44,7 +46,8 @@ namespace Marco.Pms.Services.Service
|
||||
ApplicationDbContext context,
|
||||
ILoggingService logger,
|
||||
S3UploadService s3Service,
|
||||
IMapper mapper)
|
||||
IMapper mapper,
|
||||
UtilityMongoDBHelper updateLogHelper)
|
||||
{
|
||||
_serviceScopeFactory = serviceScopeFactory;
|
||||
_context = context;
|
||||
@ -52,11 +55,11 @@ namespace Marco.Pms.Services.Service
|
||||
_s3Service = s3Service;
|
||||
_mapper = mapper;
|
||||
_dbContextFactory = dbContextFactory;
|
||||
_updateLogHelper = updateLogHelper;
|
||||
}
|
||||
|
||||
#region =================================================================== Service Project Functions ===================================================================
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves a paginated list of active service projects for a tenant, including related services, job counts, and team member information.
|
||||
/// </summary>
|
||||
@ -94,7 +97,8 @@ namespace Marco.Pms.Services.Service
|
||||
{
|
||||
var normalizedSearch = searchString.Trim().ToLowerInvariant();
|
||||
serviceProjectQuery = serviceProjectQuery
|
||||
.Where(sp => sp.Name.ToLower().Contains(normalizedSearch));
|
||||
.Where(sp => sp.Name.ToLower().Contains(normalizedSearch) ||
|
||||
(!string.IsNullOrWhiteSpace(sp.ShortName) && sp.ShortName.ToLower().Contains(normalizedSearch)));
|
||||
}
|
||||
|
||||
// Calculate total count and pages for pagination metadata
|
||||
@ -126,8 +130,8 @@ namespace Marco.Pms.Services.Service
|
||||
.Select(g => new
|
||||
{
|
||||
ProjectId = g.Key,
|
||||
JobsPassedDueDateCount = g.Count(jt => jt.StatusId != DoneStatus && jt.StatusId != ClosedStatus && jt.DueDate.Date < DateTime.UtcNow.Date),
|
||||
ActiveJobsCount = g.Count(jt => jt.StatusId != DoneStatus && jt.StatusId != ClosedStatus && jt.StatusId != OnHoldStatus),
|
||||
JobsPassedDueDateCount = g.Count(jt => jt.StatusId != ReviewDoneStatus && jt.StatusId != ClosedStatus && jt.DueDate.Date < DateTime.UtcNow.Date),
|
||||
ActiveJobsCount = g.Count(jt => jt.StatusId != ReviewDoneStatus && jt.StatusId != ClosedStatus && jt.StatusId != OnHoldStatus),
|
||||
AssignedJobsCount = g.Count(jt => jt.StatusId == AssignedStatus),
|
||||
OnHoldJobsCount = g.Count(jt => jt.StatusId == OnHoldStatus)
|
||||
})
|
||||
@ -195,7 +199,6 @@ namespace Marco.Pms.Services.Service
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves detailed information for a specific service project, including related client, status, services, and audit information.
|
||||
/// </summary>
|
||||
@ -439,9 +442,7 @@ namespace Marco.Pms.Services.Service
|
||||
}
|
||||
|
||||
// Create BSON snapshot of existing entity for audit logging (MongoDB)
|
||||
using var scope = _serviceScopeFactory.CreateScope();
|
||||
var updateLogHelper = scope.ServiceProvider.GetRequiredService<UtilityMongoDBHelper>();
|
||||
BsonDocument existingEntityBson = updateLogHelper.EntityToBsonDocument(serviceProject);
|
||||
BsonDocument existingEntityBson = _updateLogHelper.EntityToBsonDocument(serviceProject);
|
||||
|
||||
// Map incoming DTO to the tracked entity
|
||||
_mapper.Map(model, serviceProject);
|
||||
@ -509,7 +510,7 @@ namespace Marco.Pms.Services.Service
|
||||
});
|
||||
|
||||
// Push update log asynchronously to MongoDB for audit
|
||||
var updateLogTask = updateLogHelper.PushToUpdateLogsAsync(new UpdateLogsObject
|
||||
var updateLogTask = _updateLogHelper.PushToUpdateLogsAsync(new UpdateLogsObject
|
||||
{
|
||||
EntityId = id.ToString(),
|
||||
UpdatedById = loggedInEmployee.Id.ToString(),
|
||||
@ -566,9 +567,7 @@ namespace Marco.Pms.Services.Service
|
||||
}
|
||||
|
||||
// Create BSON snapshot of existing entity for audit logging (MongoDB)
|
||||
using var scope = _serviceScopeFactory.CreateScope();
|
||||
var updateLogHelper = scope.ServiceProvider.GetRequiredService<UtilityMongoDBHelper>();
|
||||
BsonDocument existingEntityBson = updateLogHelper.EntityToBsonDocument(serviceProject);
|
||||
BsonDocument existingEntityBson = _updateLogHelper.EntityToBsonDocument(serviceProject);
|
||||
|
||||
// Update active status as requested by the client
|
||||
serviceProject.IsActive = isActive;
|
||||
@ -576,7 +575,7 @@ namespace Marco.Pms.Services.Service
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
// Push update log asynchronously to MongoDB for audit
|
||||
await updateLogHelper.PushToUpdateLogsAsync(new UpdateLogsObject
|
||||
await _updateLogHelper.PushToUpdateLogsAsync(new UpdateLogsObject
|
||||
{
|
||||
EntityId = id.ToString(),
|
||||
UpdatedById = loggedInEmployee.Id.ToString(),
|
||||
@ -599,6 +598,426 @@ namespace Marco.Pms.Services.Service
|
||||
|
||||
#endregion
|
||||
|
||||
#region =================================================================== Project Branch Functions ===================================================================
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves a paginated list of project branches filtered by activity status and optional search criteria.
|
||||
/// Implements enterprise-grade optimizations, detailed logging, and standardized error handling.
|
||||
/// </summary>
|
||||
/// <param name="projectId">Unique identifier for the project.</param>
|
||||
/// <param name="isActive">Filter by active/inactive branches.</param>
|
||||
/// <param name="searchString">Optional search string for filtering by branch name, address, or type.</param>
|
||||
/// <param name="pageNumber">Current page number for pagination.</param>
|
||||
/// <param name="pageSize">Number of records per page.</param>
|
||||
/// <param name="loggedInEmployee">Current logged-in employee details.</param>
|
||||
/// <param name="tenantId">Tenant identifier for multi-tenant architecture.</param>
|
||||
/// <returns>ApiResponse containing paginated branches or error details.</returns>
|
||||
public async Task<ApiResponse<object>> GetProjectBranchListByProjectAsync(Guid projectId, bool isActive, string? searchString, int pageNumber, int pageSize,
|
||||
Employee loggedInEmployee, Guid tenantId)
|
||||
{
|
||||
if (tenantId == Guid.Empty)
|
||||
{
|
||||
_logger.LogWarning("GetProjectBranchListByProjectAsync called with invalid tenant context by employee {EmployeeId}", loggedInEmployee.Id);
|
||||
return ApiResponse<object>.ErrorResponse("Access Denied", "Invalid tenant context.", 403);
|
||||
}
|
||||
// Log method invocation with parameters for audit and debugging
|
||||
_logger.LogInfo("Fetching project branches for ProjectId: {ProjectId}, IsActive: {IsActive}, Page: {PageNumber}, Size: {PageSize}", projectId, isActive, pageNumber, pageSize);
|
||||
|
||||
try
|
||||
{
|
||||
// Check if the service project exists
|
||||
var serviceProject = await _context.ServiceProjects
|
||||
.AsNoTracking()
|
||||
.FirstOrDefaultAsync(sp => sp.Id == projectId && sp.TenantId == tenantId);
|
||||
if (serviceProject == null)
|
||||
{
|
||||
_logger.LogWarning("Service project not found for ProjectId: {ProjectId}", projectId);
|
||||
return ApiResponse<object>.ErrorResponse("Service project not found", "Service project not found", 404);
|
||||
}
|
||||
|
||||
// Build base query with necessary includes and filters
|
||||
var branchQuery = _context.ProjectBranches
|
||||
.Include(pb => pb.Project).ThenInclude(sp => sp!.Status)
|
||||
.Include(pb => pb.CreatedBy).ThenInclude(e => e!.JobRole)
|
||||
.AsNoTracking()
|
||||
.Where(pb => pb.ProjectId == projectId && pb.TenantId == tenantId && pb.IsActive == isActive);
|
||||
|
||||
// Apply search filtering if search string is provided
|
||||
if (!string.IsNullOrWhiteSpace(searchString))
|
||||
{
|
||||
var normalized = searchString.Trim().ToLowerInvariant();
|
||||
branchQuery = branchQuery.Where(pb =>
|
||||
pb.BranchName.ToLower().Contains(normalized) ||
|
||||
pb.Address.ToLower().Contains(normalized) ||
|
||||
pb.BranchType.ToLower().Contains(normalized));
|
||||
}
|
||||
|
||||
// Count total records for pagination metadata
|
||||
var totalEntities = await branchQuery.CountAsync();
|
||||
var totalPages = (int)Math.Ceiling((double)totalEntities / pageSize);
|
||||
|
||||
// Fetch paginated data sorted by name descending
|
||||
var branches = await branchQuery
|
||||
.OrderByDescending(pb => pb.BranchName)
|
||||
.Skip((pageNumber - 1) * pageSize)
|
||||
.Take(pageSize)
|
||||
.ToListAsync();
|
||||
|
||||
// Map entities to view models
|
||||
var projectBranchVMs = _mapper.Map<List<ProjectBranchVM>>(branches);
|
||||
|
||||
// Prepare response with pagination metadata
|
||||
var response = new
|
||||
{
|
||||
CurrentPage = pageNumber,
|
||||
TotalPages = totalPages,
|
||||
TotalEntities = totalEntities,
|
||||
Data = projectBranchVMs
|
||||
};
|
||||
|
||||
// Log successful fetch
|
||||
_logger.LogInfo("Fetched {Count} branches for Project: {ProjectName}", projectBranchVMs.Count, serviceProject.Name);
|
||||
return ApiResponse<object>.SuccessResponse(response, $"{projectBranchVMs.Count} branches of project {serviceProject.Name} fetched successfully");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Log exception details
|
||||
_logger.LogError(ex, "Error occurred while fetching project branches for ProjectId: {ProjectId}", projectId);
|
||||
// Return standardized problem details response
|
||||
return ApiResponse<object>.ErrorResponse("An unexpected error occurred.", ex.Message, 500);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves detailed information for a single project branch by ID, including related project and employee metadata.
|
||||
/// Provides enterprise-grade optimization, structured error handling, and detailed logging.
|
||||
/// </summary>
|
||||
/// <param name="id">Unique identifier of the project branch.</param>
|
||||
/// <param name="loggedInEmployee">Information about the currently logged-in employee (for auditing/security).</param>
|
||||
/// <param name="tenantId">The current tenant's unique identifier (multi-tenancy support).</param>
|
||||
/// <returns>ApiResponse with the branch details or a standardized error.</returns>
|
||||
public async Task<ApiResponse<object>> GetProjectBranchDetailsAsync(Guid id, Employee loggedInEmployee, Guid tenantId)
|
||||
{
|
||||
if (tenantId == Guid.Empty)
|
||||
{
|
||||
_logger.LogWarning("GetProjectBranchDetailsAsync called with invalid tenant context by employee {EmployeeId}", loggedInEmployee.Id);
|
||||
return ApiResponse<object>.ErrorResponse("Access Denied", "Invalid tenant context.", 403);
|
||||
}
|
||||
_logger.LogInfo("Attempting to fetch details for ProjectBranchId: {ProjectBranchId}, TenantId: {TenantId} by EmployeeId: {EmployeeId}",
|
||||
id, tenantId, loggedInEmployee.Id);
|
||||
|
||||
try
|
||||
{
|
||||
// Query the branch with required related entities; .AsNoTracking improves read speed/performance for lookups.
|
||||
var projectBranch = await _context.ProjectBranches
|
||||
.AsNoTracking()
|
||||
.Include(pb => pb.Project).ThenInclude(sp => sp!.Status)
|
||||
.Include(pb => pb.CreatedBy).ThenInclude(e => e!.JobRole)
|
||||
.Include(pb => pb.UpdatedBy).ThenInclude(e => e!.JobRole)
|
||||
.FirstOrDefaultAsync(pb => pb.Id == id && pb.TenantId == tenantId);
|
||||
|
||||
// Not found: log and return a descriptive error, using the correct HTTP status code.
|
||||
if (projectBranch == null)
|
||||
{
|
||||
_logger.LogWarning("Project branch not found. ProjectBranchId: {ProjectBranchId}, TenantId: {TenantId}", id, tenantId);
|
||||
return ApiResponse<object>.ErrorResponse(
|
||||
"Project branch not found",
|
||||
"No project branch exists with the given ID for this tenant.",
|
||||
404
|
||||
);
|
||||
}
|
||||
|
||||
// Map entity to detail view model to avoid exposing domain internals in API.
|
||||
var branchDetails = _mapper.Map<ProjectBranchDetailsVM>(projectBranch);
|
||||
|
||||
_logger.LogInfo("Project branch details successfully fetched. ProjectBranchId: {ProjectBranchId}", id);
|
||||
|
||||
// Return success with data using a descriptive message.
|
||||
return ApiResponse<object>.SuccessResponse(branchDetails, "Project branch details fetched successfully.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Log the complete exception with an error log, capturing all contextual info for troubleshooting.
|
||||
_logger.LogError(ex, "Error while fetching project branch details. ProjectBranchId: {ProjectBranchId}, TenantId: {TenantId}, EmployeeId: {EmployeeId}",
|
||||
id, tenantId, loggedInEmployee.Id);
|
||||
|
||||
// Return a standardized error message; hide internal error details when handing unknown errors.
|
||||
return ApiResponse<object>.ErrorResponse(
|
||||
"An unexpected error occurred while fetching project branch details.",
|
||||
ex.Message,
|
||||
500
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves a filtered, distinct list of project branch types for a specified tenant.
|
||||
/// Supports optional search filtering, optimized for read-only access.
|
||||
/// </summary>
|
||||
/// <param name="searchString">Optional search string to filter branch types.</param>
|
||||
/// <param name="loggedInEmployee">The employee requesting data, for audit logging.</param>
|
||||
/// <param name="tenantId">Tenant identifier to scope data in a multi-tenant environment.</param>
|
||||
/// <returns>ApiResponse with list of branch types or error message.</returns>
|
||||
public async Task<ApiResponse<object>> GetBranchTypeListAsync(string? searchString, Employee loggedInEmployee, Guid tenantId)
|
||||
{
|
||||
if (tenantId == Guid.Empty)
|
||||
{
|
||||
_logger.LogWarning("GetBranchTypeListAsync called with invalid tenant context by employee {EmployeeId}", loggedInEmployee.Id);
|
||||
return ApiResponse<object>.ErrorResponse("Access Denied", "Invalid tenant context.", 403);
|
||||
}
|
||||
_logger.LogInfo("Fetching distinct project branch types for TenantId: {TenantId}, RequestedBy: {EmployeeId}", tenantId, loggedInEmployee.Id);
|
||||
|
||||
try
|
||||
{
|
||||
// Build initial query with no tracking for optimized read performance
|
||||
var branchTypeQuery = _context.ProjectBranches
|
||||
.AsNoTracking()
|
||||
.Where(pb => pb.TenantId == tenantId)
|
||||
.Select(pb => pb.BranchType);
|
||||
|
||||
// Apply search filter if provided
|
||||
if (!string.IsNullOrWhiteSpace(searchString))
|
||||
{
|
||||
_logger.LogDebug("Applying search filter for branch types with searchString: {SearchString}", searchString);
|
||||
branchTypeQuery = branchTypeQuery.Where(bt => bt.Contains(searchString));
|
||||
}
|
||||
|
||||
// Get distinct branch types asynchronously
|
||||
var branchTypes = await branchTypeQuery
|
||||
.Distinct()
|
||||
.OrderBy(bt => bt)
|
||||
.ToListAsync();
|
||||
|
||||
_logger.LogInfo("Fetched {Count} distinct branch types for TenantId: {TenantId}", branchTypes.Count, tenantId);
|
||||
|
||||
return ApiResponse<object>.SuccessResponse(branchTypes, $"{branchTypes.Count} project branch types fetched successfully.", 200);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error fetching branch types for TenantId: {TenantId}", tenantId);
|
||||
return ApiResponse<object>.ErrorResponse("Failed to fetch branch types due to an internal error.", ex.Message, 500);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new project branch associated with a specific service project.
|
||||
/// Applies enterprise-grade validation, logging, and exception handling.
|
||||
/// </summary>
|
||||
/// <param name="model">DTO containing project branch creation data.</param>
|
||||
/// <param name="loggedInEmployee">Logged-in employee details for auditing.</param>
|
||||
/// <param name="tenantId">Tenant identifier for multi-tenancy context.</param>
|
||||
/// <returns>ApiResponse containing created project branch details or error info.</returns>
|
||||
public async Task<ApiResponse<object>> CreateProjectBranchAsync(ProjectBranchDto model, Employee loggedInEmployee, Guid tenantId)
|
||||
{
|
||||
|
||||
if (tenantId == Guid.Empty)
|
||||
{
|
||||
_logger.LogWarning("CreateProjectBranchAsync called with invalid tenant context by employee {EmployeeId}", loggedInEmployee.Id);
|
||||
return ApiResponse<object>.ErrorResponse("Access Denied", "Invalid tenant context.", 403);
|
||||
}
|
||||
|
||||
_logger.LogInfo("Starting project branch creation. ProjectId: {ProjectId}, TenantId: {TenantId}, CreatedBy: {EmployeeId}",
|
||||
model.ProjectId, tenantId, loggedInEmployee.Id);
|
||||
|
||||
try
|
||||
{
|
||||
// Validate existence of related service project for given tenant
|
||||
var serviceProject = await _context.ServiceProjects
|
||||
.AsNoTracking()
|
||||
.FirstOrDefaultAsync(sp => sp.Id == model.ProjectId && sp.TenantId == tenantId && sp.IsActive);
|
||||
|
||||
if (serviceProject == null)
|
||||
{
|
||||
_logger.LogWarning("Service project not found for ProjectId: {ProjectId}, TenantId: {TenantId}", model.ProjectId, tenantId);
|
||||
return ApiResponse<object>.ErrorResponse("Service project not found", "No service project exists with the given ID for this tenant.", 404);
|
||||
}
|
||||
|
||||
// Map DTO to domain entity and initialize audit and status fields
|
||||
var projectBranch = _mapper.Map<ProjectBranch>(model);
|
||||
projectBranch.Id = Guid.NewGuid();
|
||||
projectBranch.IsActive = true;
|
||||
projectBranch.CreatedAt = DateTime.UtcNow;
|
||||
projectBranch.CreatedById = loggedInEmployee.Id;
|
||||
projectBranch.TenantId = tenantId;
|
||||
|
||||
// Add and persist new project branch
|
||||
await using var transaction = await _context.Database.BeginTransactionAsync();
|
||||
_context.ProjectBranches.Add(projectBranch);
|
||||
await _context.SaveChangesAsync();
|
||||
await transaction.CommitAsync();
|
||||
|
||||
// Map to response view models assembling nested related data
|
||||
var response = _mapper.Map<ProjectBranchVM>(projectBranch);
|
||||
response.Project = _mapper.Map<BasicServiceProjectVM>(serviceProject);
|
||||
response.CreatedBy = _mapper.Map<BasicEmployeeVM>(loggedInEmployee);
|
||||
|
||||
_logger.LogInfo("Project branch created successfully. ProjectBranchId: {ProjectBranchId}", projectBranch.Id);
|
||||
return ApiResponse<object>.SuccessResponse(response, "Created project branch successfully", 201);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error occurred while creating project branch. ProjectId: {ProjectId}, TenantId: {TenantId}, EmployeeId: {EmployeeId}",
|
||||
model.ProjectId, tenantId, loggedInEmployee.Id);
|
||||
|
||||
return ApiResponse<object>.ErrorResponse("An unexpected error occurred while creating the project branch.", ex.Message, 500);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates an existing project branch with new data. Ensures data consistency, logs changes, and maintains comprehensive audit trail.
|
||||
/// Implements enterprise best practices for validation, logging, transaction management, and error handling.
|
||||
/// </summary>
|
||||
/// <param name="id">ID of the project branch to update.</param>
|
||||
/// <param name="model">DTO containing updated project branch data.</param>
|
||||
/// <param name="loggedInEmployee">Current employee performing the update (for audit and logging).</param>
|
||||
/// <param name="tenantId">Tenant ID for multi-tenant data isolation.</param>
|
||||
/// <returns>ApiResponse indicating success or failure with detailed messages.</returns>
|
||||
public async Task<ApiResponse<object>> UpdateProjectBranchAsync(Guid id, ProjectBranchDto model, Employee loggedInEmployee, Guid tenantId)
|
||||
{
|
||||
if (tenantId == Guid.Empty)
|
||||
{
|
||||
_logger.LogWarning("UpdateProjectBranchAsync called with invalid tenant context by employee {EmployeeId}", loggedInEmployee.Id);
|
||||
return ApiResponse<object>.ErrorResponse("Access Denied", "Invalid tenant context.", 403);
|
||||
}
|
||||
|
||||
// Validate ID consistency between route parameter and payload DTO
|
||||
if (!model.Id.HasValue && model.Id != id)
|
||||
{
|
||||
_logger.LogWarning("ID mismatch: Route ID {RouteId} != Payload ID {PayloadId}", id, model.Id ?? Guid.Empty);
|
||||
return ApiResponse<object>.ErrorResponse("ID mismatch between route and payload", "The ID provided in the route does not match the payload.", 400);
|
||||
}
|
||||
|
||||
// Fetch current entity state for auditing
|
||||
var projectBranch = await _context.ProjectBranches
|
||||
.AsNoTracking()
|
||||
.FirstOrDefaultAsync(pb => pb.Id == id && pb.TenantId == tenantId);
|
||||
|
||||
if (projectBranch == null)
|
||||
{
|
||||
_logger.LogWarning("Project branch not found for update. Id: {Id}, TenantId: {TenantId}", id, tenantId);
|
||||
return ApiResponse<object>.ErrorResponse("Project branch not found", "No project branch exists with the provided ID for this tenant.", 404);
|
||||
}
|
||||
|
||||
// Convert existing entity to BSON for detailed audit logging
|
||||
BsonDocument existingEntityBson = _updateLogHelper.EntityToBsonDocument(projectBranch);
|
||||
|
||||
// Map the incoming DTO onto the existing entity
|
||||
_mapper.Map(model, projectBranch);
|
||||
projectBranch.UpdatedAt = DateTime.UtcNow;
|
||||
projectBranch.UpdatedById = loggedInEmployee.Id;
|
||||
|
||||
try
|
||||
{
|
||||
// Execute update within a transaction to ensure atomicity
|
||||
await using var transaction = await _context.Database.BeginTransactionAsync();
|
||||
|
||||
// Mark the entity as modified
|
||||
_context.ProjectBranches.Update(projectBranch);
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
// Commit transaction
|
||||
await transaction.CommitAsync();
|
||||
|
||||
// Log the update in a dedicated audit log asynchronously
|
||||
var updateLogTask = _updateLogHelper.PushToUpdateLogsAsync(new UpdateLogsObject
|
||||
{
|
||||
EntityId = id.ToString(),
|
||||
UpdatedById = loggedInEmployee.Id.ToString(),
|
||||
OldObject = existingEntityBson,
|
||||
UpdatedAt = DateTime.UtcNow
|
||||
}, "ProjectBranchModificationLog");
|
||||
|
||||
// Fetch the latest entity details with related info for response
|
||||
var branchTask = Task.Run(async () =>
|
||||
{
|
||||
await using var context = await _dbContextFactory.CreateDbContextAsync();
|
||||
return await context.ProjectBranches
|
||||
.Include(pb => pb.Project).ThenInclude(sp => sp!.Status)
|
||||
.Include(pb => pb.CreatedBy).ThenInclude(e => e!.JobRole)
|
||||
.AsNoTracking()
|
||||
.FirstOrDefaultAsync(pb => pb.Id == id && pb.TenantId == tenantId);
|
||||
});
|
||||
|
||||
await Task.WhenAll(updateLogTask, branchTask);
|
||||
|
||||
// Map updated entity to view model for API response
|
||||
var response = _mapper.Map<ProjectBranchVM>(branchTask.Result);
|
||||
|
||||
_logger.LogInfo("Successfully updated project branch. Id: {Id}", id);
|
||||
return ApiResponse<object>.SuccessResponse(response, "Project branch updated successfully.", 200);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Log detailed error for troubleshooting
|
||||
_logger.LogError(ex, "Error during project branch update. Id: {Id}, TenantId: {TenantId}", id, tenantId);
|
||||
return ApiResponse<object>.ErrorResponse("Failed to update project branch due to an internal error.", ex.Message, 500);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Soft deletes or restores a project branch by toggling its IsActive flag.
|
||||
/// Implements audit logging, transaction safety, and detailed error handling to ensure enterprise readiness.
|
||||
/// </summary>
|
||||
/// <param name="id">The unique identifier of the project branch to be deleted or restored.</param>
|
||||
/// <param name="isActive">Boolean indicating active state; false to soft delete, true to restore.</param>
|
||||
/// <param name="loggedInEmployee">The authenticated employee performing the operation, for auditing purposes.</param>
|
||||
/// <param name="tenantId">Tenant ID to enforce multi-tenant data isolation.</param>
|
||||
/// <returns>ApiResponse indicating the result of the operation, with status and descriptive message.</returns>
|
||||
public async Task<ApiResponse<object>> DeleteProjectBranchAsync(Guid id, bool isActive, Employee loggedInEmployee, Guid tenantId)
|
||||
{
|
||||
if (tenantId == Guid.Empty)
|
||||
{
|
||||
_logger.LogWarning("DeleteProjectBranchAsync called with invalid tenant context by employee {EmployeeId}", loggedInEmployee.Id);
|
||||
return ApiResponse<object>.ErrorResponse("Access Denied", "Invalid tenant context.", 403);
|
||||
}
|
||||
_logger.LogInfo("Starting soft delete operation for ProjectBranchId: {ProjectBranchId}, TenantId: {TenantId}, By EmployeeId: {EmployeeId}",
|
||||
id, tenantId, loggedInEmployee.Id);
|
||||
|
||||
try
|
||||
{
|
||||
// Fetch the existing project branch record for the tenant
|
||||
var projectBranch = await _context.ProjectBranches
|
||||
.FirstOrDefaultAsync(pb => pb.Id == id && pb.TenantId == tenantId);
|
||||
|
||||
if (projectBranch == null)
|
||||
{
|
||||
_logger.LogWarning("Project branch not found for soft delete. ProjectBranchId: {ProjectBranchId}, TenantId: {TenantId}", id, tenantId);
|
||||
return ApiResponse<object>.ErrorResponse("Project branch not found", "No project branch exists with the given ID for this tenant.", 404);
|
||||
}
|
||||
|
||||
// Capture existing entity state for audit logging
|
||||
BsonDocument existingEntityBson = _updateLogHelper.EntityToBsonDocument(projectBranch);
|
||||
|
||||
// Update the IsActive flag to soft delete or restore
|
||||
projectBranch.IsActive = isActive;
|
||||
|
||||
// Save changes within a transaction to ensure atomicity
|
||||
await using var transaction = await _context.Database.BeginTransactionAsync();
|
||||
await _context.SaveChangesAsync();
|
||||
await transaction.CommitAsync();
|
||||
|
||||
// Log the change asynchronously for audit trail
|
||||
await _updateLogHelper.PushToUpdateLogsAsync(new UpdateLogsObject
|
||||
{
|
||||
EntityId = id.ToString(),
|
||||
UpdatedById = loggedInEmployee.Id.ToString(),
|
||||
OldObject = existingEntityBson,
|
||||
UpdatedAt = DateTime.UtcNow
|
||||
}, "ProjectBranchModificationLog");
|
||||
|
||||
_logger.LogInfo("Soft delete operation completed successfully for ProjectBranchId: {ProjectBranchId}", id);
|
||||
|
||||
return ApiResponse<object>.SuccessResponse(new { }, isActive ? "Branch restored successfully" : "Branch deleted successfully", 200);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error occurred during soft delete operation for ProjectBranchId: {ProjectBranchId}, TenantId: {TenantId}", id, tenantId);
|
||||
return ApiResponse<object>.ErrorResponse("Failed to delete project branch due to an internal error.", ex.Message, 500);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region =================================================================== Service Project Allocation Functions ===================================================================
|
||||
|
||||
/// <summary>
|
||||
@ -1046,7 +1465,7 @@ namespace Marco.Pms.Services.Service
|
||||
/// <param name="tenantId">Tenant context.</param>
|
||||
/// <param name="loggedInEmployee">Employee requesting data.</param>
|
||||
/// <returns>Paged list of JobTicketVM plus metadata, or error response.</returns>
|
||||
public async Task<ApiResponse<object>> GetJobTicketsListAsync(Guid? projectId, int pageNumber, int pageSize, bool isActive, Employee loggedInEmployee, Guid tenantId)
|
||||
public async Task<ApiResponse<object>> GetJobTicketsListAsync(Guid? projectId, int pageNumber, int pageSize, bool isActive, bool isArchive, Employee loggedInEmployee, Guid tenantId)
|
||||
{
|
||||
if (tenantId == Guid.Empty)
|
||||
{
|
||||
@ -1077,6 +1496,7 @@ namespace Marco.Pms.Services.Service
|
||||
.Where(jt =>
|
||||
jt.TenantId == tenantId &&
|
||||
jt.IsActive == isActive &&
|
||||
jt.IsArchive == isArchive &&
|
||||
jt.Project != null &&
|
||||
jt.Status != null &&
|
||||
jt.CreatedBy != null &&
|
||||
@ -1193,11 +1613,13 @@ namespace Marco.Pms.Services.Service
|
||||
.Include(jt => jt.Status)
|
||||
.Include(jt => jt.Project)
|
||||
.Include(jt => jt.CreatedBy).ThenInclude(e => e!.JobRole)
|
||||
.Include(jt => jt.ProjectBranch)
|
||||
.AsNoTracking()
|
||||
.FirstOrDefaultAsync(jt =>
|
||||
jt.Id == id &&
|
||||
jt.TenantId == tenantId &&
|
||||
jt.IsActive &&
|
||||
!jt.IsArchive &&
|
||||
jt.Project != null &&
|
||||
jt.Status != null &&
|
||||
jt.CreatedBy != null &&
|
||||
@ -1206,7 +1628,7 @@ namespace Marco.Pms.Services.Service
|
||||
if (jobTicket == null)
|
||||
{
|
||||
_logger.LogWarning("Job ticket not found or inactive. JobTicketId: {JobTicketId}, TenantId: {TenantId}", id, tenantId);
|
||||
return ApiResponse<object>.ErrorResponse("Job not found", "Job ticket not found or inactive.", 404);
|
||||
return ApiResponse<object>.ErrorResponse("The job could not be found. Please check the job details and try again.", "Job ticket not found or inactive.", 404);
|
||||
}
|
||||
|
||||
// Load all job statuses for status mappings in logs
|
||||
@ -1258,6 +1680,7 @@ namespace Marco.Pms.Services.Service
|
||||
Status = status,
|
||||
NextStatus = nextStatus,
|
||||
Comment = ul.Comment,
|
||||
UpdatedAt = ul.UpdatedAt,
|
||||
UpdatedBy = _mapper.Map<BasicEmployeeVM>(ul.UpdatedBy)
|
||||
};
|
||||
}).ToList();
|
||||
@ -1360,6 +1783,11 @@ namespace Marco.Pms.Services.Service
|
||||
/// <returns>ApiResponse containing the created job ticket view or error details.</returns>
|
||||
public async Task<ApiResponse<object>> CreateJobTicketAsync(CreateJobTicketDto model, Employee loggedInEmployee, Guid tenantId)
|
||||
{
|
||||
if (tenantId == Guid.Empty)
|
||||
{
|
||||
_logger.LogWarning("CreateJobTicketAsync called with invalid tenant context by employee {EmployeeId}", loggedInEmployee.Id);
|
||||
return ApiResponse<object>.ErrorResponse("Access Denied", "Invalid tenant context.", 403);
|
||||
}
|
||||
await using var transaction = await _context.Database.BeginTransactionAsync();
|
||||
try
|
||||
{
|
||||
@ -1390,6 +1818,40 @@ namespace Marco.Pms.Services.Service
|
||||
return ApiResponse<object>.ErrorResponse("Service project not found", "Service project not found or inactive", 404);
|
||||
}
|
||||
|
||||
if (model.ProjectBranchId.HasValue)
|
||||
{
|
||||
// Log the attempt to fetch project branch
|
||||
_logger.LogInfo("Attempting to fetch project branch with ID: {ProjectBranchId}, Project ID: {ProjectId}, Tenant ID: {TenantId}",
|
||||
model.ProjectBranchId, serviceProject.Id, tenantId);
|
||||
|
||||
// Query project branch with all necessary filters
|
||||
var projectBranch = await _context.ProjectBranches
|
||||
.AsNoTracking()
|
||||
.FirstOrDefaultAsync(pb =>
|
||||
pb.Id == model.ProjectBranchId.Value &&
|
||||
pb.ProjectId == serviceProject.Id &&
|
||||
pb.TenantId == tenantId &&
|
||||
pb.IsActive);
|
||||
|
||||
// Check if project branch exists
|
||||
if (projectBranch == null)
|
||||
{
|
||||
// Log the failure to find project branch
|
||||
_logger.LogWarning("Project branch not found for ID: {ProjectBranchId}, Project ID: {ProjectId}, Tenant ID: {TenantId}",
|
||||
model.ProjectBranchId, serviceProject.Id, tenantId);
|
||||
|
||||
// Return a structured error response
|
||||
return ApiResponse<object>.ErrorResponse(
|
||||
"Project branch not found",
|
||||
"The specified project branch does not exist or is not active for the given project and tenant.",
|
||||
404);
|
||||
}
|
||||
|
||||
// Log successful retrieval
|
||||
_logger.LogInfo("Successfully retrieved project branch with ID: {ProjectBranchId}", model.ProjectBranchId);
|
||||
}
|
||||
|
||||
|
||||
var hasAssignees = model.Assignees?.Any(a => a.IsActive) ?? false;
|
||||
|
||||
string uIDPrefix = $"JT/{DateTime.Now:MMyy}";
|
||||
@ -1406,6 +1868,8 @@ namespace Marco.Pms.Services.Service
|
||||
jobTicket.StatusId = hasAssignees ? AssignedStatus : NewStatus;
|
||||
jobTicket.UIDPrefix = uIDPrefix;
|
||||
jobTicket.UIDPostfix = uIDPostfix;
|
||||
jobTicket.IsActive = true;
|
||||
jobTicket.IsArchive = false;
|
||||
jobTicket.CreatedAt = DateTime.UtcNow;
|
||||
jobTicket.CreatedById = loggedInEmployee.Id;
|
||||
jobTicket.TenantId = tenantId;
|
||||
@ -1570,12 +2034,12 @@ namespace Marco.Pms.Services.Service
|
||||
var jobTicket = await _context.JobTickets
|
||||
.Include(jt => jt.Project)
|
||||
.Include(jt => jt.CreatedBy).ThenInclude(e => e!.JobRole)
|
||||
.FirstOrDefaultAsync(jt => jt.Id == model.JobTicketId && jt.TenantId == tenantId);
|
||||
.FirstOrDefaultAsync(jt => jt.Id == model.JobTicketId && jt.TenantId == tenantId && jt.IsActive && !jt.IsArchive);
|
||||
|
||||
if (jobTicket == null)
|
||||
{
|
||||
_logger.LogWarning("Job ticket {JobTicketId} not found for status change in tenant {TenantId}", model.JobTicketId, tenantId);
|
||||
return ApiResponse<object>.ErrorResponse("Job Not Found", "Job ticket not found.", 404);
|
||||
return ApiResponse<object>.ErrorResponse("The job could not be found. Please check the job details and try again.", "Job ticket not found.", 404);
|
||||
}
|
||||
|
||||
var jobStatusMapping = await GetJobStatusMappingAsync(jobTicket.StatusId, model.StatusId, jobTicket.ProjectId, loggedInEmployee.Id, tenantId);
|
||||
@ -1650,7 +2114,7 @@ namespace Marco.Pms.Services.Service
|
||||
var projectTask = Task.Run(async () =>
|
||||
{
|
||||
await using var context = await _dbContextFactory.CreateDbContextAsync();
|
||||
return await context.ServiceProjects.FirstOrDefaultAsync(sp => sp.Id == model.ProjectId && sp.TenantId == tenantId && sp.IsActive);
|
||||
return await context.ServiceProjects.FirstOrDefaultAsync(sp => sp.Id == jobTicket.ProjectId && sp.TenantId == tenantId && sp.IsActive);
|
||||
});
|
||||
var statusTask = Task.Run(async () =>
|
||||
{
|
||||
@ -1663,7 +2127,7 @@ namespace Marco.Pms.Services.Service
|
||||
// Validate existence of foreign entities
|
||||
if (projectTask.Result == null)
|
||||
{
|
||||
_logger.LogWarning("Service project not found during job ticket update. ProjectId: {ProjectId}, TenantId: {TenantId}", model.ProjectId, tenantId);
|
||||
_logger.LogWarning("Service project not found during job ticket update. ProjectId: {ProjectId}, TenantId: {TenantId}", jobTicket.ProjectId, tenantId);
|
||||
return ApiResponse<object>.ErrorResponse("Service project not found", "Service project not found", 404);
|
||||
}
|
||||
if (statusTask.Result == null)
|
||||
@ -1697,11 +2161,21 @@ namespace Marco.Pms.Services.Service
|
||||
};
|
||||
_context.StatusUpdateLogs.Add(updateLog);
|
||||
}
|
||||
if (jobTicket.IsArchive != model.IsArchive)
|
||||
{
|
||||
// Validate if job ticket status permits archiving
|
||||
if (model.IsArchive && jobTicket.StatusId != ReviewDoneStatus && jobTicket.StatusId != ClosedStatus)
|
||||
{
|
||||
_logger.LogWarning("Archiving failed: Job status not eligible. JobTicketId: {JobTicketId}, StatusId: {StatusId}", jobTicket.Id, jobTicket.StatusId);
|
||||
return ApiResponse<object>.ErrorResponse(
|
||||
"Archiving failed: Only jobs with status Done or Closed can be archived.",
|
||||
"Invalid status: Job not eligible for archiving.",
|
||||
400);
|
||||
}
|
||||
}
|
||||
|
||||
// Create BSON snapshot of existing entity for audit logging (MongoDB)
|
||||
using var scope = _serviceScopeFactory.CreateScope();
|
||||
var updateLogHelper = scope.ServiceProvider.GetRequiredService<UtilityMongoDBHelper>();
|
||||
BsonDocument existingEntityBson = updateLogHelper.EntityToBsonDocument(jobTicket);
|
||||
BsonDocument existingEntityBson = _updateLogHelper.EntityToBsonDocument(jobTicket);
|
||||
|
||||
// Map updated properties from DTO, set audit metadata
|
||||
_mapper.Map(model, jobTicket);
|
||||
@ -1825,7 +2299,7 @@ namespace Marco.Pms.Services.Service
|
||||
await transaction.CommitAsync();
|
||||
|
||||
// Push update log asynchronously to MongoDB for audit
|
||||
var updateLogTask = updateLogHelper.PushToUpdateLogsAsync(new UpdateLogsObject
|
||||
var updateLogTask = _updateLogHelper.PushToUpdateLogsAsync(new UpdateLogsObject
|
||||
{
|
||||
EntityId = id.ToString(),
|
||||
UpdatedById = loggedInEmployee.Id.ToString(),
|
||||
@ -1859,13 +2333,13 @@ namespace Marco.Pms.Services.Service
|
||||
{
|
||||
await transaction.RollbackAsync();
|
||||
_logger.LogError(dbEx, "Database error while updating job ticket for project {ProjectId} by employee {EmployeeId} in tenant {TenantId}",
|
||||
model.ProjectId, loggedInEmployee.Id, tenantId);
|
||||
jobTicket.ProjectId, loggedInEmployee.Id, tenantId);
|
||||
return ApiResponse<object>.ErrorResponse("Database Error", "An error occurred while saving data to the database.", 500);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Unhandled exception while updating job ticket for project {ProjectId} by employee {EmployeeId} in tenant {TenantId}",
|
||||
model.ProjectId, loggedInEmployee.Id, tenantId);
|
||||
jobTicket.ProjectId, loggedInEmployee.Id, tenantId);
|
||||
return ApiResponse<object>.ErrorResponse("Internal Server Error", "An unexpected error occurred.", 500);
|
||||
}
|
||||
}
|
||||
@ -1906,18 +2380,18 @@ namespace Marco.Pms.Services.Service
|
||||
.Include(jc => jc.JobTicket).ThenInclude(jt => jt!.Status)
|
||||
.Include(jc => jc.CreatedBy).ThenInclude(e => e!.JobRole)
|
||||
.Include(jc => jc.UpdatedBy).ThenInclude(e => e!.JobRole)
|
||||
.Where(jc => jc.TenantId == tenantId && jc.JobTicket != null && jc.CreatedBy != null && jc.CreatedBy.JobRole != null);
|
||||
.Where(jc => jc.TenantId == tenantId && jc.JobTicket != null && !jc.JobTicket.IsArchive && jc.CreatedBy != null && jc.CreatedBy.JobRole != null);
|
||||
|
||||
// Filter by jobTicketId if provided after verifying existence
|
||||
if (jobTicketId.HasValue)
|
||||
{
|
||||
var jobTicketExists = await _context.JobTickets.AnyAsync(jt =>
|
||||
jt.Id == jobTicketId && jt.TenantId == tenantId);
|
||||
jt.Id == jobTicketId && jt.TenantId == tenantId && !jt.IsArchive);
|
||||
|
||||
if (!jobTicketExists)
|
||||
{
|
||||
_logger.LogWarning("Job ticket {JobTicketId} not found in tenant {TenantId} for comment listing", jobTicketId, tenantId);
|
||||
return ApiResponse<object>.ErrorResponse("Job not found", "Job ticket not found.", 404);
|
||||
return ApiResponse<object>.ErrorResponse("The job could not be found or is archived. Please check the job details and try again.", "Job ticket not found.", 404);
|
||||
}
|
||||
|
||||
commentQuery = commentQuery.Where(jc => jc.JobTicketId == jobTicketId.Value);
|
||||
@ -2024,12 +2498,12 @@ namespace Marco.Pms.Services.Service
|
||||
var jobTicket = await _context.JobTickets
|
||||
.Include(jt => jt.Status)
|
||||
.AsNoTracking()
|
||||
.FirstOrDefaultAsync(jt => jt.Id == model.JobTicketId && jt.TenantId == tenantId);
|
||||
.FirstOrDefaultAsync(jt => jt.Id == model.JobTicketId && jt.TenantId == tenantId && !jt.IsArchive);
|
||||
|
||||
if (jobTicket == null)
|
||||
{
|
||||
_logger.LogWarning("Job ticket {JobTicketId} not found or inaccessible in tenant {TenantId}", model.JobTicketId, tenantId);
|
||||
return ApiResponse<object>.ErrorResponse("Job Not Found", "Job ticket not found or inaccessible.", 404);
|
||||
return ApiResponse<object>.ErrorResponse("The job could not be found or is archived. Please check the job details and try again.", "Job ticket not found or inaccessible.", 404);
|
||||
}
|
||||
|
||||
// Create new comment entity
|
||||
@ -2129,6 +2603,11 @@ namespace Marco.Pms.Services.Service
|
||||
/// <returns>ApiResponse containing updated comment details or error information.</returns>
|
||||
public async Task<ApiResponse<object>> UpdateCommentAsync(Guid id, JobCommentDto model, Employee loggedInEmployee, Guid tenantId)
|
||||
{
|
||||
if (tenantId == Guid.Empty)
|
||||
{
|
||||
_logger.LogWarning("UpdateCommentAsync called with invalid tenant context by employee {EmployeeId}", loggedInEmployee.Id);
|
||||
return ApiResponse<object>.ErrorResponse("Access Denied", "Invalid tenant context.", 403);
|
||||
}
|
||||
// Transaction ensures atomic update of comment and attachments.
|
||||
await using var transaction = await _context.Database.BeginTransactionAsync();
|
||||
try
|
||||
@ -2153,7 +2632,7 @@ namespace Marco.Pms.Services.Service
|
||||
await using var context = await _dbContextFactory.CreateDbContextAsync();
|
||||
return await context.JobTickets
|
||||
.AsNoTracking()
|
||||
.FirstOrDefaultAsync(jc => jc.Id == model.JobTicketId && jc.TenantId == tenantId && jc.IsActive);
|
||||
.FirstOrDefaultAsync(jt => jt.Id == model.JobTicketId && jt.TenantId == tenantId && !jt.IsArchive);
|
||||
});
|
||||
|
||||
await Task.WhenAll(jobCommentTask, jobTicketTask);
|
||||
@ -2163,7 +2642,7 @@ namespace Marco.Pms.Services.Service
|
||||
if (jobTicket == null)
|
||||
{
|
||||
_logger.LogWarning("Job ticket {JobTicketId} not found for updating comment {CommentId}", model.JobTicketId, id);
|
||||
return ApiResponse<object>.ErrorResponse("Job not found", "Job not found", 404);
|
||||
return ApiResponse<object>.ErrorResponse("The job could not be found or is archived. Please check the job details and try again.", "The job could not be found. Please check the job details and try again.", 404);
|
||||
}
|
||||
if (jobComment == null)
|
||||
{
|
||||
@ -2172,9 +2651,7 @@ namespace Marco.Pms.Services.Service
|
||||
}
|
||||
|
||||
// Audit: BSON snapshot before update (MongoDB)
|
||||
using var scope = _serviceScopeFactory.CreateScope();
|
||||
var updateLogHelper = scope.ServiceProvider.GetRequiredService<UtilityMongoDBHelper>();
|
||||
BsonDocument existingEntityBson = updateLogHelper.EntityToBsonDocument(jobComment);
|
||||
BsonDocument existingEntityBson = _updateLogHelper.EntityToBsonDocument(jobComment);
|
||||
|
||||
// Update comment core fields and audit
|
||||
_mapper.Map(model, jobComment);
|
||||
@ -2274,7 +2751,7 @@ namespace Marco.Pms.Services.Service
|
||||
}
|
||||
|
||||
// Push audit log to MongoDB
|
||||
var updateLogTask = updateLogHelper.PushToUpdateLogsAsync(new UpdateLogsObject
|
||||
var updateLogTask = _updateLogHelper.PushToUpdateLogsAsync(new UpdateLogsObject
|
||||
{
|
||||
EntityId = id.ToString(),
|
||||
UpdatedById = loggedInEmployee.Id.ToString(),
|
||||
@ -2345,6 +2822,11 @@ namespace Marco.Pms.Services.Service
|
||||
#region =================================================================== Job Tagging Functions ===================================================================
|
||||
public async Task<ApiResponse<object>> GetAttendanceForSelfAsync(Guid jobTicketId, Employee loggedInEmployee, Guid tenantId)
|
||||
{
|
||||
if (tenantId == Guid.Empty)
|
||||
{
|
||||
_logger.LogWarning("GetAttendanceForSelfAsync called with invalid tenant context by employee {EmployeeId}", loggedInEmployee.Id);
|
||||
return ApiResponse<object>.ErrorResponse("Access Denied", "Invalid tenant context.", 403);
|
||||
}
|
||||
_logger.LogInfo("GetAttendanceForSelfAsync initiated for EmployeeId: {EmployeeId}, JobTicketId: {JobTicketId}", loggedInEmployee.Id, jobTicketId);
|
||||
|
||||
try
|
||||
@ -2353,12 +2835,12 @@ namespace Marco.Pms.Services.Service
|
||||
var jobTicket = await _context.JobTickets
|
||||
.AsNoTracking()
|
||||
.Include(jt => jt.Status)
|
||||
.FirstOrDefaultAsync(jt => jt.Id == jobTicketId && jt.TenantId == tenantId && jt.IsActive);
|
||||
.FirstOrDefaultAsync(jt => jt.Id == jobTicketId && jt.TenantId == tenantId && !jt.IsArchive);
|
||||
|
||||
if (jobTicket == null)
|
||||
{
|
||||
_logger.LogWarning("JobTicket not found. JobTicketId: {JobTicketId}, TenantId: {TenantId}", jobTicketId, tenantId);
|
||||
return ApiResponse<object>.ErrorResponse("Job not found", "Job is not found", 404);
|
||||
return ApiResponse<object>.ErrorResponse("The job could not be found. Please check the job details and try again.", "Job is not found", 404);
|
||||
}
|
||||
|
||||
var jobEmployeeMapping = await _context.JobEmployeeMappings
|
||||
@ -2419,6 +2901,12 @@ namespace Marco.Pms.Services.Service
|
||||
}
|
||||
public async Task<ApiResponse<object>> GetAttendanceLogForAttendanceAsync(Guid jobAttendanceId, Employee loggedInEmployee, Guid tenantId)
|
||||
{
|
||||
if (tenantId == Guid.Empty)
|
||||
{
|
||||
_logger.LogWarning("GetAttendanceLogForAttendanceAsync called with invalid tenant context by employee {EmployeeId}", loggedInEmployee.Id);
|
||||
return ApiResponse<object>.ErrorResponse("Access Denied", "Invalid tenant context.", 403);
|
||||
}
|
||||
|
||||
_logger.LogInfo("GetAttendanceLogForAttendanceAsync called for JobAttendanceId: {JobAttendanceId}, TenantId: {TenantId}, EmployeeId: {EmployeeId}", jobAttendanceId, tenantId, loggedInEmployee.Id);
|
||||
|
||||
try
|
||||
@ -2497,6 +2985,13 @@ namespace Marco.Pms.Services.Service
|
||||
}
|
||||
public async Task<ApiResponse<object>> GetAttendanceForJobTeamAsync(Guid jobTicketId, DateTime? startDate, DateTime? endDate, Employee loggedInEmployee, Guid tenantId)
|
||||
{
|
||||
|
||||
if (tenantId == Guid.Empty)
|
||||
{
|
||||
_logger.LogWarning("GetAttendanceForJobTeamAsync called with invalid tenant context by employee {EmployeeId}", loggedInEmployee.Id);
|
||||
return ApiResponse<object>.ErrorResponse("Access Denied", "Invalid tenant context.", 403);
|
||||
}
|
||||
|
||||
_logger.LogInfo("GetAttendanceForJobTeamAsync called for JobTicketId: {JobTicketId}, TenantId: {TenantId}, EmployeeId: {EmployeeId}", jobTicketId, tenantId, loggedInEmployee.Id);
|
||||
|
||||
try
|
||||
@ -2504,7 +2999,7 @@ namespace Marco.Pms.Services.Service
|
||||
// Validate the existence and active status of the job ticket including its status related data
|
||||
var jobTicket = await _context.JobTickets
|
||||
.AsNoTracking()
|
||||
.FirstOrDefaultAsync(jt => jt.Id == jobTicketId && jt.TenantId == tenantId && jt.IsActive);
|
||||
.FirstOrDefaultAsync(jt => jt.Id == jobTicketId && jt.TenantId == tenantId && !jt.IsArchive);
|
||||
|
||||
if (jobTicket == null)
|
||||
{
|
||||
@ -2554,6 +3049,13 @@ namespace Marco.Pms.Services.Service
|
||||
}
|
||||
public async Task<ApiResponse<object>> ManageJobTaggingAsync(JobAttendanceDto model, Employee loggedInEmployee, Guid tenantId)
|
||||
{
|
||||
|
||||
if (tenantId == Guid.Empty)
|
||||
{
|
||||
_logger.LogWarning("ManageJobTaggingAsync called with invalid tenant context by employee {EmployeeId}", loggedInEmployee.Id);
|
||||
return ApiResponse<object>.ErrorResponse("Access Denied", "Invalid tenant context.", 403);
|
||||
}
|
||||
|
||||
_logger.LogInfo("ManageJobTaggingAsync called for EmployeeId: {EmployeeId}, JobTicketId: {JobTicketId}", loggedInEmployee.Id, model.JobTcketId);
|
||||
|
||||
try
|
||||
@ -2562,11 +3064,11 @@ namespace Marco.Pms.Services.Service
|
||||
var jobTicket = await _context.JobTickets
|
||||
.AsNoTracking()
|
||||
.Include(jt => jt.Status)
|
||||
.FirstOrDefaultAsync(jt => jt.Id == model.JobTcketId && jt.TenantId == tenantId && jt.IsActive);
|
||||
.FirstOrDefaultAsync(jt => jt.Id == model.JobTcketId && jt.TenantId == tenantId && !jt.IsArchive);
|
||||
if (jobTicket == null)
|
||||
{
|
||||
_logger.LogWarning("JobTicket not found. JobTicketId: {JobTicketId}, TenantId: {TenantId}", model.JobTcketId, tenantId);
|
||||
return ApiResponse<object>.ErrorResponse("Job not found", "Job not found", 404);
|
||||
return ApiResponse<object>.ErrorResponse("The job could not be found. Please check the job details and try again.", "The job could not be found. Please check the job details and try again.", 404);
|
||||
}
|
||||
|
||||
// Check if the current user is part of the job team
|
||||
@ -2733,8 +3235,6 @@ namespace Marco.Pms.Services.Service
|
||||
|
||||
//private async Task DeleteTalkingPointAttachments(List<Guid> documentIds)
|
||||
//{
|
||||
// using var scope = _serviceScopeFactory.CreateScope();
|
||||
// var _updateLogHelper = scope.ServiceProvider.GetRequiredService<UtilityMongoDBHelper>();
|
||||
|
||||
// var attachmentTask = Task.Run(async () =>
|
||||
// {
|
||||
@ -2778,8 +3278,6 @@ namespace Marco.Pms.Services.Service
|
||||
|
||||
private async Task DeleteJobAttachemnts(List<Guid> documentIds)
|
||||
{
|
||||
using var scope = _serviceScopeFactory.CreateScope();
|
||||
var _updateLogHelper = scope.ServiceProvider.GetRequiredService<UtilityMongoDBHelper>();
|
||||
|
||||
var attachmentTask = Task.Run(async () =>
|
||||
{
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user