Compare commits

...

19 Commits

Author SHA1 Message Date
a3ffe1d3d6 Corrected the spelling mistake JobTicketId in job attendance 2025-11-25 12:55:50 +05:30
55c2ca73de Merge pull request 'Project_Branch_Management' (#155) from Project_Branch_Management into main
Reviewed-on: #155
2025-11-25 06:49:18 +00:00
bcba454b6e Added a function to check if have service project permission 2025-11-25 12:17:23 +05:30
e73413c849 Added the date in project report API in market controller 2025-11-24 12:01:56 +05:30
457e3b411e Updated the dynamic sorting in collection controller 2025-11-24 11:04:53 +05:30
8f5a49deed Added dynamic sortting, grouping, cloumn searching in collection controller 2025-11-22 00:31:47 +05:30
f1f5fc263f Update firebase helper function for record attendance 2025-11-21 13:07:55 +05:30
64aab7d712 Removed Project ForignKey From ProjectContactMapping Table 2025-11-21 12:04:35 +05:30
c7a73e78fb Removed Project ForignKey From PaymentRequest Table 2025-11-21 11:33:57 +05:30
1b94592026 Removed the project forign key and able to create payment request for both infra and service project 2025-11-20 17:59:59 +05:30
9c95b12a8f Removed the project forign key and able to create expense for both infra and service project 2025-11-20 16:47:25 +05:30
df0e9f7b46 Added the update at in job details update log histeroy vm 2025-11-20 15:39:53 +05:30
24e45037da Added an API to get list of branch types 2025-11-20 14:35:02 +05:30
a31266ee4a Update the Id check in branch update API 2025-11-20 12:25:58 +05:30
bd2f9d953f Added IsArchive field in ob ticket table 2025-11-20 11:58:45 +05:30
d5a7ad0716 Added An APi to get Projects Completion Status 2025-11-19 18:30:16 +05:30
4108915b92 Added the project branch in job ticket table 2025-11-19 18:11:54 +05:30
dad135571d Added the project branch CRUD oprations 2025-11-19 15:36:45 +05:30
5a402925b1 Added the Project branch related tables 2025-11-19 15:10:40 +05:30
66 changed files with 73883 additions and 765 deletions

View File

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

File diff suppressed because one or more lines are too long

View File

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

View File

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

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,29 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Marco.Pms.DataAccess.Migrations
{
/// <inheritdoc />
public partial class Added_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");
}
}
}

File diff suppressed because one or more lines are too long

View File

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

View File

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

View File

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

View File

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

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,106 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Marco.Pms.DataAccess.Migrations
{
/// <inheritdoc />
public partial class Corrected_JobTicketId_Spelling_In_JobAttendance : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_JobAttendance_JobTickets_JobTcketId",
table: "JobAttendance");
migrationBuilder.DropForeignKey(
name: "FK_JobAttendanceLogs_JobTickets_JobTcketId",
table: "JobAttendanceLogs");
migrationBuilder.RenameColumn(
name: "JobTcketId",
table: "JobAttendanceLogs",
newName: "JobTicketId");
migrationBuilder.RenameIndex(
name: "IX_JobAttendanceLogs_JobTcketId",
table: "JobAttendanceLogs",
newName: "IX_JobAttendanceLogs_JobTicketId");
migrationBuilder.RenameColumn(
name: "JobTcketId",
table: "JobAttendance",
newName: "JobTicketId");
migrationBuilder.RenameIndex(
name: "IX_JobAttendance_JobTcketId",
table: "JobAttendance",
newName: "IX_JobAttendance_JobTicketId");
migrationBuilder.AddForeignKey(
name: "FK_JobAttendance_JobTickets_JobTicketId",
table: "JobAttendance",
column: "JobTicketId",
principalTable: "JobTickets",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
migrationBuilder.AddForeignKey(
name: "FK_JobAttendanceLogs_JobTickets_JobTicketId",
table: "JobAttendanceLogs",
column: "JobTicketId",
principalTable: "JobTickets",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_JobAttendance_JobTickets_JobTicketId",
table: "JobAttendance");
migrationBuilder.DropForeignKey(
name: "FK_JobAttendanceLogs_JobTickets_JobTicketId",
table: "JobAttendanceLogs");
migrationBuilder.RenameColumn(
name: "JobTicketId",
table: "JobAttendanceLogs",
newName: "JobTcketId");
migrationBuilder.RenameIndex(
name: "IX_JobAttendanceLogs_JobTicketId",
table: "JobAttendanceLogs",
newName: "IX_JobAttendanceLogs_JobTcketId");
migrationBuilder.RenameColumn(
name: "JobTicketId",
table: "JobAttendance",
newName: "JobTcketId");
migrationBuilder.RenameIndex(
name: "IX_JobAttendance_JobTicketId",
table: "JobAttendance",
newName: "IX_JobAttendance_JobTcketId");
migrationBuilder.AddForeignKey(
name: "FK_JobAttendance_JobTickets_JobTcketId",
table: "JobAttendance",
column: "JobTcketId",
principalTable: "JobTickets",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
migrationBuilder.AddForeignKey(
name: "FK_JobAttendanceLogs_JobTickets_JobTcketId",
table: "JobAttendanceLogs",
column: "JobTcketId",
principalTable: "JobTickets",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
}
}
}

View File

@ -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")
@ -5061,7 +5054,7 @@ namespace Marco.Pms.DataAccess.Migrations
b.Property<Guid>("EmployeeId")
.HasColumnType("char(36)");
b.Property<Guid>("JobTcketId")
b.Property<Guid>("JobTicketId")
.HasColumnType("char(36)");
b.Property<DateTime>("TaggedInAt")
@ -5083,7 +5076,7 @@ namespace Marco.Pms.DataAccess.Migrations
b.HasIndex("EmployeeId");
b.HasIndex("JobTcketId");
b.HasIndex("JobTicketId");
b.HasIndex("TenantId");
@ -5111,7 +5104,7 @@ namespace Marco.Pms.DataAccess.Migrations
b.Property<Guid>("JobAttendanceId")
.HasColumnType("char(36)");
b.Property<Guid>("JobTcketId")
b.Property<Guid>("JobTicketId")
.HasColumnType("char(36)");
b.Property<string>("Latitude")
@ -5137,7 +5130,7 @@ namespace Marco.Pms.DataAccess.Migrations
b.HasIndex("JobAttendanceId");
b.HasIndex("JobTcketId");
b.HasIndex("JobTicketId");
b.HasIndex("TenantId");
@ -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");
@ -8221,7 +8253,7 @@ namespace Marco.Pms.DataAccess.Migrations
b.HasOne("Marco.Pms.Model.ServiceProject.JobTicket", "JobTicket")
.WithMany()
.HasForeignKey("JobTcketId")
.HasForeignKey("JobTicketId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
@ -8258,7 +8290,7 @@ namespace Marco.Pms.DataAccess.Migrations
b.HasOne("Marco.Pms.Model.ServiceProject.JobTicket", "JobTicket")
.WithMany()
.HasForeignKey("JobTcketId")
.HasForeignKey("JobTicketId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
@ -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")

View File

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

View File

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

View File

@ -5,7 +5,7 @@ namespace Marco.Pms.Model.Dtos.ServiceProject
{
public class JobAttendanceDto
{
public required Guid JobTcketId { get; set; }
public required Guid JobTicketId { get; set; }
public required TAGGING_MARK_TYPE Action { get; set; }
public string? Latitude { get; set; }
public string? Longitude { get; set; }

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

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

View File

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

View File

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

View File

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

View File

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

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

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

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

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

View File

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

View File

@ -8,10 +8,10 @@ namespace Marco.Pms.Model.ServiceProject
public class JobAttendance : TenantRelation
{
public Guid Id { get; set; }
public Guid JobTcketId { get; set; }
public Guid JobTicketId { get; set; }
[ValidateNever]
[ForeignKey("JobTcketId")]
[ForeignKey("JobTicketId")]
public JobTicket? JobTicket { get; set; }
public TAGGING_MARK_TYPE Action { get; set; }
public Guid EmployeeId { get; set; }

View File

@ -14,10 +14,10 @@ namespace Marco.Pms.Model.ServiceProject
[ValidateNever]
[ForeignKey("JobAttendanceId")]
public JobAttendance? JobAttendance { get; set; }
public Guid JobTcketId { get; set; }
public Guid JobTicketId { get; set; }
[ValidateNever]
[ForeignKey("JobTcketId")]
[ForeignKey("JobTicketId")]
public JobTicket? JobTicket { get; set; }
public Guid? DocumentId { get; set; }

View File

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

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

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

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

View File

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

View File

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

View File

@ -7,5 +7,6 @@
public string? Description { get; set; }
public string? JobTicketUId { get; set; }
public string? StatusName { get; set; }
public bool IsArchive { get; set; }
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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,14 +127,27 @@ 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
// 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)
@ -139,156 +155,146 @@ namespace Marco.Pms.Services.Service
{
ProjectName = g.Select(pa => pa.Project!.Name).FirstOrDefault(),
EmployeeIds = g.Select(pa => pa.EmployeeId).Distinct().ToList()
}).FirstOrDefaultAsync();
});
})
.FirstOrDefaultAsync();
var teamAttendanceRoleTask = Task.Run(async () =>
if (project == null)
{
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();
});
await Task.WhenAll(projectTask, teamAttendanceRoleTask, manageProjectsRoleTask, regularizeAttendanceRoleTask);
var teamAttendanceRoleIds = teamAttendanceRoleTask.Result;
var regularizeAttendanceRoleIds = regularizeAttendanceRoleTask.Result;
var manageProjectsRoleIds = manageProjectsRoleTask.Result;
var project = projectTask.Result;
List<Guid> projectAssignedEmployeeIds = project?.EmployeeIds ?? new List<Guid>();
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();
});
await Task.WhenAll(employeeIdsTask, teamEmployeeIdsTask);
var employeeIds = employeeIdsTask.Result;
var teamEmployeeIds = teamEmployeeIdsTask.Result;
var mesaageNotificationIds = new List<Guid>();
Notification notificationFirebase;
switch (markType)
{
case ATTENDANCE_MARK_TYPE.CHECK_IN:
notificationFirebase = 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
{
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
{
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
{
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 Denied",
Body = $" {name}'s regularization request for project {project?.ProjectName ?? ""} has been rejected."
};
mesaageNotificationIds.Add(employeeId);
break;
default:
notificationFirebase = new Notification
{
Title = "Attendance Update",
Body = $" {name} has update his/her attendance for project {project?.ProjectName ?? ""}."
};
break;
_logger.LogWarning("No active project allocations found for ProjectId: {ProjectId}", projectId);
return; // or throw if critical
}
// List of device registration tokens to send the message to
var projectAssignedEmployeeIds = project.EmployeeIds;
var data = new Dictionary<string, string>()
// 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();
await Task.WhenAll(teamAttendanceRoleIdsTask, regularizeAttendanceRoleIdsTask, manageProjectsRoleIdsTask);
var teamAttendanceRoleIds = teamAttendanceRoleIdsTask.Result;
var regularizeAttendanceRoleIds = regularizeAttendanceRoleIdsTask.Result;
var manageProjectsRoleIds = manageProjectsRoleIdsTask.Result;
// 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();
// 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();
List<Guid> messageNotificationIds = new();
// 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}."
},
ATTENDANCE_MARK_TYPE.CHECK_OUT => new Notification
{
Title = "Attendance Update",
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}."
},
ATTENDANCE_MARK_TYPE.REGULARIZE => 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."
},
_ => new Notification
{
Title = "Attendance Update",
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() }
};
var registrationTokensForNotificationTask = Task.Run(async () =>
// Filter out current (logged-in) employee from notifications if origin is not provided (optional)
if (string.IsNullOrWhiteSpace(origin))
{
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);
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);
}
catch (Exception ex)
{
_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)
{

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff