Compare commits

...

84 Commits

Author SHA1 Message Date
7e20807325 corrected the distinct by error 2025-10-11 15:57:53 +05:30
186486d934 Added the new API to get organizations for dropdown 2025-10-11 15:36:52 +05:30
d07f0311ae Optmized the work status master APIs 2025-10-11 13:52:20 +05:30
2f6031e62c revert e02636b6b66287a22fcfb34c150aece1b136cc68
revert Optmized the work status master APIs
2025-10-11 08:19:59 +00:00
e02636b6b6 Optmized the work status master APIs 2025-10-11 13:48:36 +05:30
bad784e147 Optimized the contact related msater APIs 2025-10-11 12:59:31 +05:30
886d0bb3b1 Clearing the employee profile cache when selecting the tenant 2025-10-10 18:32:01 +05:30
74dd9eeb8d organization Tenant Mapping is already existed then return error message 2025-10-10 17:53:23 +05:30
1939a63d9a Assigning the appilcation role to root employee of the any organization 2025-10-10 17:05:43 +05:30
c07db9f94d removed employee project cache when adding the subscription 2025-10-10 15:59:42 +05:30
53da15416a Removing all employees profile from cache for certain tenant 2025-10-10 15:46:56 +05:30
a809bdd469 Merge pull request 'Sloved issues of permissions not been assigned when updating the subscription' (#144) from Ashutosh_Bug#1461 into main
Reviewed-on: #144
2025-10-10 08:49:19 +00:00
bb2c098cb9 Sloved issues of permissions not been assigned when updating the subscription 2025-10-10 14:17:32 +05:30
bd3b70f4ea Merge pull request 'Added the RequestedAt and RequestedBy in attendance' (#143) from Ashutosh_Enhancement#1452 into main
Reviewed-on: #143
2025-10-10 07:06:25 +00:00
000cef3bb7 enhanced the get team based on project 2025-10-10 12:36:11 +05:30
522deae8f7 Added the RequestedAt and RequestedBy in attendance 2025-10-10 12:08:53 +05:30
9a8aa4f5ce Changed the index of reject button in expense controller 2025-10-09 16:52:34 +05:30
af92ab977b Added the get filter API 2025-10-09 16:47:06 +05:30
c06dc8ebe7 Selecting the default services in project allocation 2025-10-09 16:00:26 +05:30
8609db64d2 Made the serviceId is nullable in project allocation 2025-10-09 15:23:37 +05:30
e831f50505 Removed the logic to save the FCM from verify FCM API 2025-10-08 11:33:34 +05:30
ba88fbced6 Removed the FCM token from MPIN verify API 2025-10-08 11:27:30 +05:30
13d2e1cd7d Corrected the mistake of showing all images except selected project 2025-10-07 12:34:48 +05:30
26acfec408 Added the todays completed tasks 2025-10-06 18:33:37 +05:30
3bd38f3c68 Changed the logic in API to get Attendance logs by employee 2025-10-06 16:10:58 +05:30
2e29dc9946 Chnaged the check for persentages 2025-10-06 14:42:29 +05:30
db752a4678 Merge branch 'main' of https://git.marcoaiot.com/admin/marco.pms.api 2025-10-06 13:16:31 +05:30
245182eb07 Added the attendance persentage and task persentage 2025-10-06 13:15:48 +05:30
9daf76c6eb return all employees when allEmployee=true 2025-10-05 00:24:26 +05:30
c33afa58c3 Solved spelling mistake 2025-10-04 17:51:58 +05:30
4ceb5c3cb2 Changed the authontication logic for delete contact notes 2025-10-04 17:50:18 +05:30
d0dabf776b Added the condition to not get services object in project allocation 2025-10-04 17:26:08 +05:30
04223578ad Added the ExpenseUId in expenses tables 2025-10-04 16:56:04 +05:30
f94a7de4ab change email in complete system 2025-10-01 17:46:11 +05:30
eb3a65428e Modyfied the attendance report API 2025-10-01 15:09:51 +05:30
8ddb414e91 Added the organization type in assignd organization list 2025-09-30 18:08:07 +05:30
d8329f1fab Removed the tenant check from master services 2025-09-30 17:10:30 +05:30
d0912cca56 Merge pull request 'Organization_Management' (#142) from Organization_Management into main
Reviewed-on: #142
2025-09-30 09:05:14 +00:00
d957c692f1 Added new API to get monthly attendance report 2025-09-30 14:34:29 +05:30
40ca680f45 Added the Organization sub-menu in project menu in MenuStaticMaster 2025-09-30 12:42:29 +05:30
8dee7b0fa2 Merge branch 'main' of https://git.marcoaiot.com/admin/marco.pms.api into Organization_Management 2025-09-30 12:36:39 +05:30
1e1fe6692d Added the permission check in note create API 2025-09-30 12:34:58 +05:30
0df13975a7 Added logic to add hard coded menu if menu from mongodb connot be found 2025-09-30 12:30:04 +05:30
8fb5aa632d Removed the cache get logic from expenses get APIs 2025-09-30 12:26:10 +05:30
7e4d4b0f5b Chnaged the log to check if sprid in used wile searching 2025-09-30 11:40:43 +05:30
64bde99ab5 Corrected the check to add self organization 2025-09-30 10:18:15 +05:30
90851a3349 Added HasAdmin check when adding note for contact 2025-09-29 17:22:45 +05:30
2c0cd87227 Sloved the issue in get task list where user is getting the list of all tasks accept the task of selected project 2025-09-29 16:50:38 +05:30
c3670eaab9 Added Primary organization of tenant in get organization list 2025-09-29 14:48:42 +05:30
1cd8203d27 Added the PMC, Promotor and Primary organization of the tenant in list of assigned organization to project 2025-09-29 14:33:57 +05:30
5df66b79ca Added check to check the Designation in create contact API 2025-09-29 11:15:14 +05:30
31da8a4402 Added the AsNoTracking in GetCurrentEmployeeAsync and get permission Ids helper functions 2025-09-29 10:18:44 +05:30
27a2610388 Added the HasApplicationAccess in Employee VM 2025-09-29 10:08:14 +05:30
cd175c4fdf Added the services in organization details API 2025-09-29 09:42:21 +05:30
d6145ee0ba Added the forgin key for approver in attends table 2025-09-27 14:50:51 +05:30
04086ed7e0 Creating the application user in manage employee API through mobile 2025-09-27 14:18:42 +05:30
a9b4b79ee4 Added the service ID and Organization ID in get project allocation API 2025-09-27 10:33:42 +05:30
70092e5601 Added the created by and updated by in organization update API 2025-09-26 16:12:46 +05:30
33cd7113a7 Merge pull request 'Add new API to get organization details' (#141) from Ashutosh_Task_#1362 into Organization_Management
Reviewed-on: #141
2025-09-26 10:26:46 +00:00
1d4d61ef2c Add new API to get organization details 2025-09-26 15:53:38 +05:30
f93686c1df Removed the Activity from master menu 2025-09-26 14:59:24 +05:30
f2213db807 Added organizationId filter to the employee list for organization assiged to the project 2025-09-26 11:43:44 +05:30
eddbdde355 Merge pull request 'Added new API to get list of employees assigned to project and filtered by services and organizations' (#140) from Ashutosh_Task#1360 into Organization_Management
Reviewed-on: #140
2025-09-26 06:02:25 +00:00
0048281616 Added new API to get list of employees assigned to project and filtered by services and organizations 2025-09-26 11:30:28 +05:30
f248557704 Optimized the Organization APIs 2025-09-25 17:17:21 +05:30
18d590ccbe Added the promotor and PMC in project VM 2025-09-25 16:56:21 +05:30
4c6070fee5 Merge pull request 'Added the service Id in Query parameter to get infra details API' (#139) from Ashutosh_Enhancement_#1355 into Organization_Management
Reviewed-on: #139
2025-09-25 10:16:20 +00:00
0fb5c3935d Added the service Id in Query parameter to get infra details API 2025-09-25 15:45:47 +05:30
2f052174aa Changed the VM when creating and updating project 2025-09-25 12:37:57 +05:30
2258771229 Merge pull request 'Enhanced the Create and update project API' (#138) from Ashutosh_Enhancement#1347 into Organization_Management
Reviewed-on: #138
2025-09-25 06:50:39 +00:00
e4e49595e6 Enhanced the Create and update project API 2025-09-25 12:18:44 +05:30
44d2827dcc Added the global services ids list parameter in create tenant DTO 2025-09-25 09:54:08 +05:30
fef3db297c Mapping the tenant with organization when creating the organization 2025-09-24 19:43:45 +05:30
0d4d4c3754 Seprated the service ID in task list from filter 2025-09-24 19:35:16 +05:30
162d735d16 Added new API to get employee list 2025-09-24 19:20:21 +05:30
38e1f7a0fc Added no tracking in upate bucket API 2025-09-24 17:47:08 +05:30
fe1dfd7293 Getting the employee list of user's organization only 2025-09-24 17:23:32 +05:30
42da1f12cf Added the assigned by in organization mapping 2025-09-24 17:20:09 +05:30
c5da400e6b Enhanced the manage employe API 2025-09-24 13:06:51 +05:30
a405cfec13 If team meamber not found send empty 2025-09-23 13:24:19 +05:30
bc3e927777 Solved the typo 2025-09-23 13:17:58 +05:30
c304b615f7 Fixed the spelling mistake 2025-09-23 13:15:11 +05:30
8d64e9702d Added the get filter API in image gallery 2025-09-23 13:05:23 +05:30
98acbb66d6 Merge pull request 'Added the new parameter of service ids in image gallery filter' (#137) from Ashutosh_Enhancement_#1297 into Organization_Management
Reviewed-on: #137
2025-09-23 06:26:27 +00:00
74 changed files with 35817 additions and 1129 deletions

View File

@ -993,11 +993,6 @@ namespace Marco.Pms.DataAccess.Data
);
modelBuilder.Entity<OrgTypeMaster>().HasData(
new OrgTypeMaster
{
Id = Guid.Parse("743806fe-d991-4079-b223-e4e2da44f435"),
Name = "Tenant"
},
new OrgTypeMaster
{
Id = Guid.Parse("5ee49bcd-b6d3-482f-9aaf-484afe04abec"),
@ -1007,11 +1002,6 @@ namespace Marco.Pms.DataAccess.Data
{
Id = Guid.Parse("a283356a-9b02-4029-afb7-e65c703efdd4"),
Name = "Sub-Contractor"
},
new OrgTypeMaster
{
Id = Guid.Parse("b1877a3b-8832-47b1-bbe3-dc7e98672f49"),
Name = "PMC"
}
);

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,84 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Marco.Pms.DataAccess.Migrations
{
/// <inheritdoc />
public partial class Added_Assigned_By_In_Mapping_Tables : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<Guid>(
name: "AssignedById",
table: "TenantOrgMappings",
type: "char(36)",
nullable: false,
defaultValue: new Guid("08dd8b35-d98b-44f1-896d-12aec3f035aa"),
collation: "ascii_general_ci");
migrationBuilder.AddColumn<Guid>(
name: "AssignedById",
table: "ProjectOrgMappings",
type: "char(36)",
nullable: false,
defaultValue: new Guid("08dd8b35-d98b-44f1-896d-12aec3f035aa"),
collation: "ascii_general_ci");
migrationBuilder.CreateIndex(
name: "IX_TenantOrgMappings_AssignedById",
table: "TenantOrgMappings",
column: "AssignedById");
migrationBuilder.CreateIndex(
name: "IX_ProjectOrgMappings_AssignedById",
table: "ProjectOrgMappings",
column: "AssignedById");
migrationBuilder.AddForeignKey(
name: "FK_ProjectOrgMappings_Employees_AssignedById",
table: "ProjectOrgMappings",
column: "AssignedById",
principalTable: "Employees",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
migrationBuilder.AddForeignKey(
name: "FK_TenantOrgMappings_Employees_AssignedById",
table: "TenantOrgMappings",
column: "AssignedById",
principalTable: "Employees",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_ProjectOrgMappings_Employees_AssignedById",
table: "ProjectOrgMappings");
migrationBuilder.DropForeignKey(
name: "FK_TenantOrgMappings_Employees_AssignedById",
table: "TenantOrgMappings");
migrationBuilder.DropIndex(
name: "IX_TenantOrgMappings_AssignedById",
table: "TenantOrgMappings");
migrationBuilder.DropIndex(
name: "IX_ProjectOrgMappings_AssignedById",
table: "ProjectOrgMappings");
migrationBuilder.DropColumn(
name: "AssignedById",
table: "TenantOrgMappings");
migrationBuilder.DropColumn(
name: "AssignedById",
table: "ProjectOrgMappings");
}
}
}

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,40 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
#pragma warning disable CA1814 // Prefer jagged arrays over multidimensional
namespace Marco.Pms.DataAccess.Migrations
{
/// <inheritdoc />
public partial class Deleted_Organization_Types : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DeleteData(
table: "OrgTypeMasters",
keyColumn: "Id",
keyValue: new Guid("743806fe-d991-4079-b223-e4e2da44f435"));
migrationBuilder.DeleteData(
table: "OrgTypeMasters",
keyColumn: "Id",
keyValue: new Guid("b1877a3b-8832-47b1-bbe3-dc7e98672f49"));
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.InsertData(
table: "OrgTypeMasters",
columns: new[] { "Id", "Name" },
values: new object[,]
{
{ new Guid("743806fe-d991-4079-b223-e4e2da44f435"), "Tenant" },
{ new Guid("b1877a3b-8832-47b1-bbe3-dc7e98672f49"), "PMC" }
});
}
}
}

File diff suppressed because one or more lines are too long

View File

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

File diff suppressed because one or more lines are too long

View File

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

File diff suppressed because one or more lines are too long

View File

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

View File

@ -172,7 +172,10 @@ namespace Marco.Pms.DataAccess.Migrations
b.Property<int>("Activity")
.HasColumnType("int");
b.Property<Guid?>("ApprovedBy")
b.Property<DateTime?>("ApprovedAt")
.HasColumnType("datetime(6)");
b.Property<Guid?>("ApprovedById")
.HasColumnType("char(36)");
b.Property<DateTime>("AttendanceDate")
@ -185,7 +188,7 @@ namespace Marco.Pms.DataAccess.Migrations
b.Property<DateTime>("Date")
.HasColumnType("datetime(6)");
b.Property<Guid>("EmployeeID")
b.Property<Guid>("EmployeeId")
.HasColumnType("char(36)");
b.Property<DateTime?>("InTime")
@ -200,12 +203,22 @@ namespace Marco.Pms.DataAccess.Migrations
b.Property<Guid>("ProjectID")
.HasColumnType("char(36)");
b.Property<DateTime?>("RequestedAt")
.HasColumnType("datetime(6)");
b.Property<Guid?>("RequestedById")
.HasColumnType("char(36)");
b.Property<Guid>("TenantId")
.HasColumnType("char(36)");
b.HasKey("Id");
b.HasIndex("EmployeeID");
b.HasIndex("ApprovedById");
b.HasIndex("EmployeeId");
b.HasIndex("RequestedById");
b.HasIndex("TenantId");
@ -1830,6 +1843,10 @@ namespace Marco.Pms.DataAccess.Migrations
.IsRequired()
.HasColumnType("longtext");
b.Property<string>("ExpenseUId")
.IsRequired()
.HasColumnType("longtext");
b.Property<Guid>("ExpensesTypeId")
.HasColumnType("char(36)");
@ -3588,11 +3605,6 @@ namespace Marco.Pms.DataAccess.Migrations
b.ToTable("OrgTypeMasters");
b.HasData(
new
{
Id = new Guid("743806fe-d991-4079-b223-e4e2da44f435"),
Name = "Tenant"
},
new
{
Id = new Guid("5ee49bcd-b6d3-482f-9aaf-484afe04abec"),
@ -3602,11 +3614,6 @@ namespace Marco.Pms.DataAccess.Migrations
{
Id = new Guid("a283356a-9b02-4029-afb7-e65c703efdd4"),
Name = "Sub-Contractor"
},
new
{
Id = new Guid("b1877a3b-8832-47b1-bbe3-dc7e98672f49"),
Name = "PMC"
});
});
@ -3682,6 +3689,9 @@ namespace Marco.Pms.DataAccess.Migrations
.ValueGeneratedOnAdd()
.HasColumnType("char(36)");
b.Property<Guid>("AssignedById")
.HasColumnType("char(36)");
b.Property<DateTime>("AssignedDate")
.HasColumnType("datetime(6)");
@ -3705,6 +3715,8 @@ namespace Marco.Pms.DataAccess.Migrations
b.HasKey("Id");
b.HasIndex("AssignedById");
b.HasIndex("OrganizationId");
b.HasIndex("OrganizationTypeId");
@ -3765,6 +3777,9 @@ namespace Marco.Pms.DataAccess.Migrations
.ValueGeneratedOnAdd()
.HasColumnType("char(36)");
b.Property<Guid>("AssignedById")
.HasColumnType("char(36)");
b.Property<DateTime>("AssignedDate")
.HasColumnType("datetime(6)");
@ -3785,6 +3800,8 @@ namespace Marco.Pms.DataAccess.Migrations
b.HasKey("Id");
b.HasIndex("AssignedById");
b.HasIndex("OrganizationId");
b.HasIndex("TenantId");
@ -4701,10 +4718,18 @@ namespace Marco.Pms.DataAccess.Migrations
{
b.HasOne("Marco.Pms.Model.Employees.Employee", "Approver")
.WithMany()
.HasForeignKey("EmployeeID")
.HasForeignKey("ApprovedById");
b.HasOne("Marco.Pms.Model.Employees.Employee", "Employee")
.WithMany()
.HasForeignKey("EmployeeId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Marco.Pms.Model.Employees.Employee", "RequestedBy")
.WithMany()
.HasForeignKey("RequestedById");
b.HasOne("Marco.Pms.Model.TenantModels.Tenant", "Tenant")
.WithMany()
.HasForeignKey("TenantId")
@ -4713,6 +4738,10 @@ namespace Marco.Pms.DataAccess.Migrations
b.Navigation("Approver");
b.Navigation("Employee");
b.Navigation("RequestedBy");
b.Navigation("Tenant");
});
@ -5737,6 +5766,12 @@ namespace Marco.Pms.DataAccess.Migrations
modelBuilder.Entity("Marco.Pms.Model.OrganizationModel.ProjectOrgMapping", b =>
{
b.HasOne("Marco.Pms.Model.Employees.Employee", "AssignedBy")
.WithMany()
.HasForeignKey("AssignedById")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Marco.Pms.Model.OrganizationModel.Organization", "Organization")
.WithMany()
.HasForeignKey("OrganizationId")
@ -5767,6 +5802,8 @@ namespace Marco.Pms.DataAccess.Migrations
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("AssignedBy");
b.Navigation("Organization");
b.Navigation("OrganizationType");
@ -5807,6 +5844,12 @@ namespace Marco.Pms.DataAccess.Migrations
modelBuilder.Entity("Marco.Pms.Model.OrganizationModel.TenantOrgMapping", b =>
{
b.HasOne("Marco.Pms.Model.Employees.Employee", "AssignedBy")
.WithMany()
.HasForeignKey("AssignedById")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Marco.Pms.Model.OrganizationModel.Organization", "Organization")
.WithMany()
.HasForeignKey("OrganizationId")
@ -5819,6 +5862,8 @@ namespace Marco.Pms.DataAccess.Migrations
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("AssignedBy");
b.Navigation("Organization");
b.Navigation("Tenant");

View File

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

View File

@ -1,7 +1,9 @@
using Marco.Pms.DataAccess.Data;
using Marco.Pms.Model.Master;
using Marco.Pms.Model.MongoDBModels;
using Marco.Pms.Model.MongoDBModels.Masters;
using Marco.Pms.Model.MongoDBModels.Project;
using Marco.Pms.Model.OrganizationModel;
using Marco.Pms.Model.Projects;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
@ -55,7 +57,7 @@ namespace Marco.Pms.Helpers
var indexModel = new CreateIndexModel<ProjectMongoDB>(indexKeys, indexOptions);
await _projectCollection.Indexes.CreateOneAsync(indexModel);
}
public async Task<bool> UpdateProjectDetailsOnlyToCache(Project project, StatusMaster projectStatus)
public async Task<bool> UpdateProjectDetailsOnlyToCache(Project project, StatusMaster projectStatus, Organization promotor, Organization pmc)
{
// Build the update definition
var updates = Builders<ProjectMongoDB>.Update.Combine(
@ -67,6 +69,26 @@ namespace Marco.Pms.Helpers
Id = projectStatus.Id.ToString(),
Status = projectStatus.Status
}),
Builders<ProjectMongoDB>.Update.Set(r => r.Promoter, new OrganizationMongoDB
{
Id = promotor.Id.ToString(),
Name = promotor.Name,
ContactPerson = promotor.ContactPerson,
Email = promotor.Email,
Address = promotor.Address,
ContactNumber = promotor.ContactNumber,
SPRID = promotor.SPRID
}),
Builders<ProjectMongoDB>.Update.Set(r => r.PMC, new OrganizationMongoDB
{
Id = pmc.Id.ToString(),
Name = pmc.Name,
ContactPerson = pmc.ContactPerson,
Email = pmc.Email,
Address = promotor.Address,
ContactNumber = promotor.ContactNumber,
SPRID = promotor.SPRID
}),
Builders<ProjectMongoDB>.Update.Set(r => r.StartDate, project.StartDate),
Builders<ProjectMongoDB>.Update.Set(r => r.EndDate, project.EndDate),
Builders<ProjectMongoDB>.Update.Set(r => r.ContactPerson, project.ContactPerson)

View File

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

View File

@ -2,7 +2,7 @@
{
public class CreateActivityMasterDto
{
public required Guid ActitvityGroupId { get; set; }
public required Guid ActivityGroupId { get; set; }
public required string ActivityName { get; set; }
public required string UnitOfMeasurement { get; set; }
public List<CreateCheckListDto>? CheckList { get; set; }

View File

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

View File

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

View File

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

View File

@ -9,8 +9,8 @@
public string? Email { get; set; }
public required string Gender { get; set; }
public required string BirthDate { get; set; }
public required string JoiningDate { get; set; }
public required DateTime BirthDate { get; set; }
public required DateTime JoiningDate { get; set; }
public required string PermanentAddress { get; set; }
public required string CurrentAddress { get; set; }
@ -19,6 +19,8 @@
public string? EmergencyPhoneNumber { get; set; }
public string? EmergencyContactPerson { get; set; }
public Guid JobRoleId { get; set; }
public required Guid OrganizationId { get; set; }
public required bool HasApplicationAccess { get; set; }
}
public class MobileUserManageDto
{
@ -26,10 +28,13 @@
public required string FirstName { get; set; }
public required string LastName { get; set; }
public required string PhoneNumber { get; set; }
public string? Email { get; set; }
public required DateTime JoiningDate { get; set; }
public required string Gender { get; set; }
public Guid JobRoleId { get; set; }
public string? ProfileImage { get; set; }
public required Guid OrganizationId { get; set; }
public required bool HasApplicationAccess { get; set; }
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -7,17 +7,18 @@ namespace Marco.Pms.Model.Dtos.Project
{
[Required(ErrorMessage = "Project Name is required!")]
[DisplayName("Project Name")]
public string? Name { get; set; }
public required string Name { get; set; }
[DisplayName("Short Name")]
public string? ShortName { get; set; }
[DisplayName("Project Address")]
[Required(ErrorMessage = "Project Address is required!")]
public string? ProjectAddress { get; set; }
public required string ProjectAddress { get; set; }
[DisplayName("Contact Person")]
public string? ContactPerson { get; set; }
public required string ContactPerson { get; set; }
public DateTime? StartDate { get; set; }
@ -25,6 +26,8 @@ namespace Marco.Pms.Model.Dtos.Project
[DisplayName("Project Status")]
[Required(ErrorMessage = "Project Status is required!")]
public Guid ProjectStatusId { get; set; }
public required Guid ProjectStatusId { get; set; }
public required Guid PromoterId { get; set; }
public required Guid PMCId { get; set; }
}
}

View File

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

View File

@ -5,20 +5,20 @@ namespace Marco.Pms.Model.Dtos.Project
{
public class UpdateProjectDto
{
public Guid Id { get; set; }
public required Guid Id { get; set; }
[Required(ErrorMessage = "Project Name is required!")]
[DisplayName("Project Name")]
public string? Name { get; set; }
public required string Name { get; set; }
[DisplayName("Short Name")]
public string? ShortName { get; set; }
[DisplayName("Project Address")]
[Required(ErrorMessage = "Project Address is required!")]
public string? ProjectAddress { get; set; }
public required string ProjectAddress { get; set; }
[DisplayName("Contact Person")]
public string? ContactPerson { get; set; }
public required string ContactPerson { get; set; }
public DateTime? StartDate { get; set; }
@ -26,6 +26,8 @@ namespace Marco.Pms.Model.Dtos.Project
[DisplayName("Project Status")]
[Required(ErrorMessage = "Project Status is required!")]
public Guid ProjectStatusId { get; set; }
public required Guid ProjectStatusId { get; set; }
public required Guid PromoterId { get; set; }
public required Guid PMCId { get; set; }
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,13 @@
namespace Marco.Pms.Model.MongoDBModels
{
public class OrganizationMongoDB
{
public string Id { get; set; } = string.Empty;
public string? Name { get; set; }
public string? Email { get; set; }
public string? ContactPerson { get; set; }
public string? Address { get; set; }
public string? ContactNumber { get; set; }
public double SPRID { get; set; }
}
}

View File

@ -13,6 +13,8 @@ namespace Marco.Pms.Model.MongoDBModels.Project
public DateTime? StartDate { get; set; }
public DateTime? EndDate { get; set; }
public StatusMasterMongoDB? ProjectStatus { get; set; }
public OrganizationMongoDB? Promoter { get; set; }
public OrganizationMongoDB? PMC { get; set; }
public int TeamSize { get; set; }
public double CompletedWork { get; set; }
public double PlannedWork { get; set; }

View File

@ -1,4 +1,5 @@
using Marco.Pms.Model.Utilities;
using Marco.Pms.Model.Employees;
using Marco.Pms.Model.Utilities;
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
using System.ComponentModel.DataAnnotations.Schema;
@ -27,6 +28,12 @@ namespace Marco.Pms.Model.OrganizationModel
[ValidateNever]
[ForeignKey("OrganizationTypeId")]
public OrgTypeMaster? OrganizationType { get; set; }
public Guid AssignedById { get; set; }
[ValidateNever]
[ForeignKey("AssignedById")]
public Employee? AssignedBy { get; set; }
public DateTime AssignedDate { get; set; }
public DateTime? CompletionDate { get; set; }
}

View File

@ -1,4 +1,5 @@
using Marco.Pms.Model.Utilities;
using Marco.Pms.Model.Employees;
using Marco.Pms.Model.Utilities;
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
using System.ComponentModel.DataAnnotations.Schema;
@ -14,6 +15,11 @@ namespace Marco.Pms.Model.OrganizationModel
public Organization? Organization { get; set; }
public double SPRID { get; set; }
public bool IsActive { get; set; } = true;
public Guid AssignedById { get; set; }
[ValidateNever]
[ForeignKey("AssignedById")]
public Employee? AssignedBy { get; set; }
public DateTime AssignedDate { get; set; }
public DateTime? ReassignedDate { get; set; }
}

View File

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

View File

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

View File

@ -19,6 +19,7 @@ namespace Marco.Pms.Model.ViewModels.Expenses
public DateTime TransactionDate { get; set; }
public DateTime CreatedAt { get; set; }
public string SupplerName { get; set; } = string.Empty;
public string? ExpenseUId { get; set; }
public double Amount { get; set; }
public ExpensesStatusMasterVM? Status { get; set; }
public List<ExpensesStatusMasterVM>? NextStatus { get; set; }

View File

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

View File

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

View File

@ -0,0 +1,20 @@
using Marco.Pms.Model.ViewModels.Activities;
using Marco.Pms.Model.ViewModels.Master;
namespace Marco.Pms.Model.ViewModels.Organization
{
public class ProjectOrganizationVM
{
public Guid Id { get; set; }
public string? Name { get; set; }
public string? Email { get; set; }
public string? ContactPerson { get; set; }
public double SPRID { get; set; }
public string? logoImage { get; set; }
public string? OrganizationType { get; set; }
public DateTime AssignedDate { get; set; }
public BasicEmployeeVM? AssignedBy { get; set; }
public ServiceMasterVM? Service { get; set; }
public DateTime? CompletionDate { get; set; }
}
}

View File

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

View File

@ -1,4 +1,5 @@
using Marco.Pms.Model.Master;
using Marco.Pms.Model.ViewModels.Organization;
namespace Marco.Pms.Model.ViewModels.Projects
{
@ -12,6 +13,8 @@ namespace Marco.Pms.Model.ViewModels.Projects
public DateTime? StartDate { get; set; }
public DateTime? EndDate { get; set; }
public StatusMaster? ProjectStatus { get; set; }
public BasicOrganizationVm? Promoter { get; set; }
public BasicOrganizationVm? PMC { get; set; }
}
}

View File

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

View File

@ -12,6 +12,7 @@ namespace Marco.Pms.Model.ViewModels.Tenant
public string ContactNumber { get; set; } = string.Empty;
public string? logoImage { get; set; } // Base64
public string? OrganizationSize { get; set; }
public Guid OrganizationId { get; set; }
public Industry? Industry { get; set; }
public TenantStatus? TenantStatus { get; set; }
}

View File

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

View File

@ -1,4 +1,5 @@
using Marco.Pms.DataAccess.Data;
using AutoMapper;
using Marco.Pms.DataAccess.Data;
using Marco.Pms.Model.AttendanceModule;
using Marco.Pms.Model.Dtos.Attendance;
using Marco.Pms.Model.Employees;
@ -6,6 +7,7 @@ using Marco.Pms.Model.Entitlements;
using Marco.Pms.Model.Mapper;
using Marco.Pms.Model.Projects;
using Marco.Pms.Model.Utilities;
using Marco.Pms.Model.ViewModels.Activities;
using Marco.Pms.Model.ViewModels.AttendanceVM;
using Marco.Pms.Services.Hubs;
using Marco.Pms.Services.Service;
@ -28,48 +30,41 @@ namespace MarcoBMS.Services.Controllers
public class AttendanceController : ControllerBase
{
private readonly ApplicationDbContext _context;
private readonly EmployeeHelper _employeeHelper;
private readonly IProjectServices _projectServices;
private readonly IServiceScopeFactory _serviceScopeFactory;
private readonly UserHelper _userHelper;
private readonly S3UploadService _s3Service;
private readonly PermissionServices _permission;
private readonly ILoggingService _logger;
private readonly IHubContext<MarcoHub> _signalR;
private readonly IFirebaseService _firebase;
private readonly Guid tenantId;
private readonly IMapper _mapper;
public AttendanceController(
ApplicationDbContext context, EmployeeHelper employeeHelper, IProjectServices projectServices, UserHelper userHelper,
S3UploadService s3Service, ILoggingService logger, PermissionServices permission, IHubContext<MarcoHub> signalR, IFirebaseService firebase)
ApplicationDbContext context,
UserHelper userHelper,
IServiceScopeFactory serviceScopeFactory,
ILoggingService logger,
PermissionServices permission,
IMapper mapper)
{
_context = context;
_employeeHelper = employeeHelper;
_projectServices = projectServices;
_serviceScopeFactory = serviceScopeFactory;
_userHelper = userHelper;
_s3Service = s3Service;
_logger = logger;
_permission = permission;
_signalR = signalR;
_firebase = firebase;
}
private Guid GetTenantId()
{
return _userHelper.GetTenantId();
//var tenant = User.FindFirst("TenantId")?.Value;
//return (tenant != null ? Convert.ToInt32(tenant) : 1);
_mapper = mapper;
tenantId = userHelper.GetTenantId();
}
[HttpGet("log/attendance/{attendanceid}")]
public async Task<IActionResult> GetAttendanceLogById(Guid attendanceid)
{
Guid TenantId = GetTenantId();
using var scope = _serviceScopeFactory.CreateScope();
var _s3Service = scope.ServiceProvider.GetRequiredService<S3UploadService>();
List<AttendanceLog> lstAttendance = await _context.AttendanceLogs
.Include(a => a.Document)
.Include(a => a.Employee)
.Include(a => a.UpdatedByEmployee)
.Where(c => c.AttendanceId == attendanceid && c.TenantId == TenantId)
.Where(c => c.AttendanceId == attendanceid && c.TenantId == tenantId)
.ToListAsync();
List<AttendanceLogVM> attendanceLogVMs = new List<AttendanceLogVM>();
@ -85,30 +80,42 @@ namespace MarcoBMS.Services.Controllers
}
[HttpGet("log/employee/{employeeId}")]
public async Task<IActionResult> GetAttendanceLogByEmployeeId(Guid employeeId, [FromQuery] string? dateFrom = null, [FromQuery] string? dateTo = null)
public async Task<IActionResult> GetAttendanceLogByEmployeeId(Guid employeeId, [FromQuery] DateTime? dateFrom = null, [FromQuery] DateTime? dateTo = null)
{
Guid TenantId = GetTenantId();
DateTime fromDate = new DateTime();
DateTime toDate = new DateTime();
if (dateFrom != null && DateTime.TryParse(dateFrom, out fromDate) == false)
{
_logger.LogWarning("User sent Invalid from Date while featching attendance logs");
return BadRequest(ApiResponse<object>.ErrorResponse("Invalid Date", "Invalid Date", 400));
}
if (dateTo != null && DateTime.TryParse(dateTo, out toDate) == false)
{
_logger.LogWarning("User sent Invalid to Date while featching attendance logs");
return BadRequest(ApiResponse<object>.ErrorResponse("Invalid Date", "Invalid Date", 400));
}
if (employeeId == Guid.Empty)
{
_logger.LogWarning("The employee Id sent by user is empty");
return BadRequest(ApiResponse<object>.ErrorResponse("Employee ID is required and must not be Empty.", "Employee ID is required and must not be empty.", 400));
}
List<Attendance> attendances = await _context.Attendes.Where(c => c.EmployeeID == employeeId && c.TenantId == TenantId && c.AttendanceDate.Date >= fromDate && c.AttendanceDate.Date <= toDate).ToListAsync();
Employee? employee = await _context.Employees.Include(e => e.JobRole).FirstOrDefaultAsync(e => e.Id == employeeId && e.TenantId == TenantId && e.IsActive);
Employee? employee = await _context.Employees.Include(e => e.JobRole).FirstOrDefaultAsync(e => e.Id == employeeId && e.TenantId == tenantId);
if (employee == null)
{
_logger.LogWarning("Employee {EmployeeId} not found", employeeId);
return NotFound(ApiResponse<object>.ErrorResponse("Employee not found", "Employee not found in database", 404));
}
if (!dateFrom.HasValue)
{
dateFrom = DateTime.UtcNow;
}
if (!dateTo.HasValue)
{
var days = 0 - 7;
dateTo = dateFrom.Value.AddDays(days);
}
List<Attendance> attendances = await _context.Attendes
.Include(a => a.RequestedBy)
.ThenInclude(e => e!.JobRole)
.Include(a => a.RequestedBy)
.ThenInclude(e => e!.JobRole)
.Where(c => c.EmployeeId == employeeId && c.TenantId == tenantId && c.AttendanceDate.Date >= dateFrom && c.AttendanceDate.Date <= dateTo).ToListAsync();
var projectIds = attendances.Select(a => a.ProjectID).Distinct().ToList();
var projects = await _context.Projects.Where(p => projectIds.Contains(p.Id) && p.TenantId == tenantId).ToListAsync();
List<EmployeeAttendanceVM> results = new List<EmployeeAttendanceVM>();
if (employee != null)
@ -121,11 +128,17 @@ namespace MarcoBMS.Services.Controllers
EmployeeId = employee.Id,
FirstName = employee.FirstName,
LastName = employee.LastName,
ProjectId = attendance.ProjectID,
ProjectName = projects.Where(p => p.Id == attendance.ProjectID).Select(p => p.Name).FirstOrDefault(),
CheckInTime = attendance.InTime,
CheckOutTime = attendance.OutTime,
JobRoleName = employee.JobRole != null ? employee.JobRole.Name : "",
Activity = attendance.Activity,
EmployeeAvatar = null
EmployeeAvatar = null,
RequestedAt = attendance.RequestedAt,
RequestedBy = _mapper.Map<BasicEmployeeVM>(attendance.RequestedBy),
ApprovedAt = attendance.ApprovedAt,
Approver = _mapper.Map<BasicEmployeeVM>(attendance.Approver)
};
results.Add(result);
}
@ -145,11 +158,12 @@ namespace MarcoBMS.Services.Controllers
/// <returns></returns>
[HttpGet("project/log")]
public async Task<IActionResult> EmployeeAttendanceByDateRange([FromQuery] Guid projectId, [FromQuery] Guid? organizationId, [FromQuery] string? dateFrom = null, [FromQuery] string? dateTo = null)
{
Guid tenantId = GetTenantId();
var LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
using var scope = _serviceScopeFactory.CreateScope();
var _projectServices = scope.ServiceProvider.GetRequiredService<IProjectServices>();
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
var project = await _context.Projects.AsNoTracking().FirstOrDefaultAsync(p => p.Id == projectId && p.TenantId == tenantId);
if (project == null)
@ -158,13 +172,13 @@ namespace MarcoBMS.Services.Controllers
return NotFound(ApiResponse<object>.ErrorResponse("Project not found."));
}
var hasTeamAttendancePermission = await _permission.HasPermission(PermissionsMaster.TeamAttendance, LoggedInEmployee.Id);
var hasSelfAttendancePermission = await _permission.HasPermission(PermissionsMaster.SelfAttendance, LoggedInEmployee.Id);
var hasProjectPermission = await _permission.HasProjectPermission(LoggedInEmployee, projectId);
var hasTeamAttendancePermission = await _permission.HasPermission(PermissionsMaster.TeamAttendance, loggedInEmployee.Id);
var hasSelfAttendancePermission = await _permission.HasPermission(PermissionsMaster.SelfAttendance, loggedInEmployee.Id);
var hasProjectPermission = await _permission.HasProjectPermission(loggedInEmployee, projectId);
if (!hasProjectPermission)
{
_logger.LogWarning("Employee {EmployeeId} tries to access attendance of project {ProjectId}, but don't have access", LoggedInEmployee.Id, projectId);
_logger.LogWarning("Employee {EmployeeId} tries to access attendance of project {ProjectId}, but don't have access", loggedInEmployee.Id, projectId);
return Unauthorized(ApiResponse<object>.ErrorResponse("Unauthorized access", "Unauthorized access", 404));
}
@ -197,7 +211,13 @@ namespace MarcoBMS.Services.Controllers
if (hasTeamAttendancePermission)
{
List<Attendance> lstAttendance = await _context.Attendes.Where(c => c.ProjectID == projectId && c.AttendanceDate.Date >= fromDate.Date && c.AttendanceDate.Date <= toDate.Date && c.TenantId == tenantId).ToListAsync();
List<Attendance> lstAttendance = await _context.Attendes
.Include(a => a.RequestedBy)
.ThenInclude(e => e!.JobRole)
.Include(a => a.Approver)
.ThenInclude(e => e!.JobRole)
.Where(c => c.ProjectID == projectId && c.AttendanceDate.Date >= fromDate.Date && c.AttendanceDate.Date <= toDate.Date && c.TenantId == tenantId)
.ToListAsync();
List<ProjectAllocation> projectteam = await _projectServices.GetTeamByProject(tenantId, projectId, organizationId, true);
@ -209,9 +229,13 @@ namespace MarcoBMS.Services.Controllers
Id = attendance.Id,
CheckInTime = attendance.InTime,
CheckOutTime = attendance.OutTime,
Activity = attendance.Activity
Activity = attendance.Activity,
ApprovedAt = attendance.ApprovedAt,
Approver = _mapper.Map<BasicEmployeeVM>(attendance.Approver),
RequestedAt = attendance.RequestedAt,
RequestedBy = _mapper.Map<BasicEmployeeVM>(attendance.RequestedBy)
};
teamMember = projectteam.Find(x => x.EmployeeId == attendance.EmployeeID);
teamMember = projectteam.Find(x => x.EmployeeId == attendance.EmployeeId);
if (teamMember != null)
{
result1.EmployeeAvatar = null;
@ -222,6 +246,8 @@ namespace MarcoBMS.Services.Controllers
result1.LastName = teamMember.Employee.LastName;
result1.JobRoleName = teamMember.Employee.JobRole != null ? teamMember.Employee.JobRole.Name : null;
result1.OrganizationName = teamMember.Employee.Organization?.Name;
result1.ProjectId = projectId;
result1.ProjectName = teamMember.Project?.Name;
}
else
{
@ -239,13 +265,23 @@ namespace MarcoBMS.Services.Controllers
else if (hasSelfAttendancePermission)
{
List<Attendance> lstAttendances = await _context.Attendes
.Where(c => c.ProjectID == projectId && c.EmployeeID == LoggedInEmployee.Id && c.AttendanceDate.Date >= fromDate.Date && c.AttendanceDate.Date <= toDate.Date && c.TenantId == tenantId)
.Include(a => a.RequestedBy)
.ThenInclude(e => e!.JobRole)
.Include(a => a.Approver)
.ThenInclude(e => e!.JobRole)
.Where(c => c.ProjectID == projectId && c.EmployeeId == loggedInEmployee.Id && c.AttendanceDate.Date >= fromDate.Date &&
c.AttendanceDate.Date <= toDate.Date && c.TenantId == tenantId)
.ToListAsync();
var projectAllocationQuery = _context.ProjectAllocations
.Include(pa => pa.Project)
.Include(pa => pa.Employee)
.ThenInclude(e => e!.Organization)
.Where(pa => pa.ProjectId == projectId && pa.EmployeeId == LoggedInEmployee.Id && pa.TenantId == tenantId && pa.IsActive);
.Include(pa => pa.Employee)
.ThenInclude(e => e!.JobRole)
.Where(pa => pa.EmployeeId == loggedInEmployee.Id && pa.TenantId == tenantId && pa.IsActive &&
pa.ProjectId == projectId && pa.Project != null &&
pa.Employee != null && pa.Employee.Organization != null && pa.Employee.JobRole != null);
if (organizationId.HasValue)
{
@ -267,9 +303,15 @@ namespace MarcoBMS.Services.Controllers
LastName = projectAllocation.Employee?.LastName,
JobRoleName = projectAllocation.Employee?.JobRole?.Name,
OrganizationName = projectAllocation.Employee?.Organization?.Name,
ProjectId = attendance.ProjectID,
ProjectName = projectAllocation.Project?.Name,
CheckInTime = attendance.InTime,
CheckOutTime = attendance.OutTime,
Activity = attendance.Activity
Activity = attendance.Activity,
ApprovedAt = attendance.ApprovedAt,
Approver = _mapper.Map<BasicEmployeeVM>(attendance.Approver),
RequestedAt = attendance.RequestedAt,
RequestedBy = _mapper.Map<BasicEmployeeVM>(attendance.RequestedBy)
};
result.Add(result1);
}
@ -291,7 +333,6 @@ namespace MarcoBMS.Services.Controllers
/// <returns>An IActionResult containing a list of employee attendance records or an error response.</returns>
public async Task<IActionResult> EmployeeAttendanceByProjectAsync([FromQuery] Guid projectId, [FromQuery] Guid? organizationId, [FromQuery] bool includeInactive, [FromQuery] string? date = null)
{
var tenantId = GetTenantId();
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
// --- 1. Initial Validation and Permission Checks ---
@ -353,7 +394,9 @@ namespace MarcoBMS.Services.Controllers
[HttpGet("regularize")]
public async Task<IActionResult> GetRequestRegularizeAttendance([FromQuery] Guid projectId, [FromQuery] Guid? organizationId, [FromQuery] bool IncludeInActive)
{
Guid TenantId = GetTenantId();
using var scope = _serviceScopeFactory.CreateScope();
var _projectServices = scope.ServiceProvider.GetRequiredService<IProjectServices>();
Employee LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
var result = new List<EmployeeAttendanceVM>();
var hasProjectPermission = await _permission.HasProjectPermission(LoggedInEmployee, projectId);
@ -364,35 +407,45 @@ namespace MarcoBMS.Services.Controllers
return Unauthorized(ApiResponse<object>.ErrorResponse("Unauthorized access", "Unauthorized access", 404));
}
List<Attendance> lstAttendance = await _context.Attendes.Where(c => c.ProjectID == projectId && c.Activity == ATTENDANCE_MARK_TYPE.REQUEST_REGULARIZE && c.TenantId == TenantId).ToListAsync();
List<Attendance> lstAttendance = await _context.Attendes
.Include(a => a.RequestedBy)
.ThenInclude(e => e!.JobRole)
.Include(a => a.Approver)
.ThenInclude(e => e!.JobRole)
.Where(c => c.ProjectID == projectId && c.Activity == ATTENDANCE_MARK_TYPE.REQUEST_REGULARIZE && c.TenantId == tenantId)
.ToListAsync();
List<ProjectAllocation> projectteam = await _projectServices.GetTeamByProject(TenantId, projectId, organizationId, true);
List<ProjectAllocation> projectteam = await _projectServices.GetTeamByProject(tenantId, projectId, organizationId, true);
var idList = projectteam.Select(p => p.EmployeeId).ToList();
var jobRole = await _context.JobRoles.ToListAsync();
foreach (Attendance attende in lstAttendance)
{
var result1 = new EmployeeAttendanceVM()
{
Id = attende.Id,
CheckInTime = attende.InTime,
CheckOutTime = attende.OutTime,
Activity = attende.Activity,
EmployeeAvatar = null,
EmployeeId = attende.EmployeeID,
};
var teamMember = projectteam.Find(m => m.EmployeeId == attende.EmployeeID);
var teamMember = projectteam.Find(m => m.EmployeeId == attende.EmployeeId);
if (teamMember != null && teamMember.Employee != null && teamMember.Employee.JobRole != null)
{
result1.FirstName = teamMember.Employee.FirstName;
result1.LastName = teamMember.Employee.LastName;
result1.JobRoleName = teamMember.Employee.JobRole.Name;
result1.OrganizationName = teamMember.Employee.Organization?.Name;
var result1 = new EmployeeAttendanceVM()
{
Id = attende.Id,
CheckInTime = attende.InTime,
CheckOutTime = attende.OutTime,
Activity = attende.Activity,
EmployeeAvatar = null,
EmployeeId = attende.EmployeeId,
FirstName = teamMember.Employee.FirstName,
LastName = teamMember.Employee.LastName,
JobRoleName = teamMember.Employee.JobRole.Name,
OrganizationName = teamMember.Employee.Organization?.Name,
ProjectId = projectId,
ProjectName = teamMember.Project?.Name,
ApprovedAt = attende.ApprovedAt,
Approver = _mapper.Map<BasicEmployeeVM>(attende.Approver),
RequestedAt = attende.RequestedAt,
RequestedBy = _mapper.Map<BasicEmployeeVM>(attende.RequestedBy)
};
result.Add(result1);
}
result.Add(result1);
}
result.Sort(delegate (EmployeeAttendanceVM x, EmployeeAttendanceVM y)
@ -417,13 +470,17 @@ namespace MarcoBMS.Services.Controllers
return BadRequest(ApiResponse<object>.ErrorResponse("Invalid data", errors, 400));
}
Guid TenantId = GetTenantId();
var currentEmployee = await _userHelper.GetCurrentEmployeeAsync();
using var scope = _serviceScopeFactory.CreateScope();
var _signalR = scope.ServiceProvider.GetRequiredService<IHubContext<MarcoHub>>();
var _employeeHelper = scope.ServiceProvider.GetRequiredService<EmployeeHelper>();
var _firebase = scope.ServiceProvider.GetRequiredService<IFirebaseService>();
var currentEmployee = await _userHelper.GetCurrentEmployeeAsync();
using var transaction = await _context.Database.BeginTransactionAsync();
try
{
Attendance? attendance = await _context.Attendes.FirstOrDefaultAsync(a => a.Id == recordAttendanceDot.Id && a.TenantId == TenantId); ;
Attendance? attendance = await _context.Attendes.FirstOrDefaultAsync(a => a.Id == recordAttendanceDot.Id && a.TenantId == tenantId); ;
if (recordAttendanceDot.MarkTime == null)
{
@ -451,10 +508,6 @@ namespace MarcoBMS.Services.Controllers
{
attendance.IsApproved = true;
attendance.Activity = ATTENDANCE_MARK_TYPE.REGULARIZE;
//string timeString = "10:30 PM"; // Format: "hh:mm tt"
attendance.OutTime = finalDateTime;
}
else if (recordAttendanceDot.Action == ATTENDANCE_MARK_TYPE.REQUEST_REGULARIZE)
@ -465,6 +518,8 @@ namespace MarcoBMS.Services.Controllers
{
attendance.OutTime = finalDateTime;
attendance.Activity = ATTENDANCE_MARK_TYPE.REQUEST_REGULARIZE;
attendance.RequestedById = currentEmployee.Id;
attendance.RequestedAt = DateTime.UtcNow;
}
else
{
@ -477,13 +532,16 @@ namespace MarcoBMS.Services.Controllers
{
attendance.IsApproved = true;
attendance.Activity = ATTENDANCE_MARK_TYPE.REGULARIZE;
attendance.ApprovedBy = currentEmployee.Id;
attendance.ApprovedById = currentEmployee.Id;
attendance.ApprovedAt = DateTime.UtcNow;
// do nothing
}
else if (recordAttendanceDot.Action == ATTENDANCE_MARK_TYPE.REGULARIZE_REJECT)
{
attendance.IsApproved = false;
attendance.Activity = ATTENDANCE_MARK_TYPE.REGULARIZE_REJECT;
attendance.ApprovedById = currentEmployee.Id;
attendance.ApprovedAt = DateTime.UtcNow;
// do nothing
}
attendance.Date = DateTime.UtcNow;
@ -494,11 +552,11 @@ namespace MarcoBMS.Services.Controllers
else
{
attendance = new Attendance();
attendance.TenantId = TenantId;
attendance.TenantId = tenantId;
attendance.AttendanceDate = recordAttendanceDot.Date;
// attendance.Activity = recordAttendanceDot.Action;
attendance.Comment = recordAttendanceDot.Comment;
attendance.EmployeeID = recordAttendanceDot.EmployeeID;
attendance.EmployeeId = recordAttendanceDot.EmployeeID;
attendance.ProjectID = recordAttendanceDot.ProjectID;
attendance.Date = DateTime.UtcNow;
@ -526,7 +584,7 @@ namespace MarcoBMS.Services.Controllers
Latitude = recordAttendanceDot.Latitude,
Longitude = recordAttendanceDot.Longitude,
TenantId = TenantId,
TenantId = tenantId,
UpdatedBy = currentEmployee.Id,
UpdatedOn = recordAttendanceDot.Date
};
@ -573,7 +631,7 @@ namespace MarcoBMS.Services.Controllers
var name = $"{vm.FirstName} {vm.LastName}";
await _firebase.SendAttendanceMessageAsync(attendance.ProjectID, name, recordAttendanceDot.Action, attendance.EmployeeID, TenantId);
await _firebase.SendAttendanceMessageAsync(attendance.ProjectID, name, recordAttendanceDot.Action, attendance.EmployeeId, tenantId);
});
@ -609,7 +667,12 @@ namespace MarcoBMS.Services.Controllers
return BadRequest(ApiResponse<object>.ErrorResponse("Invalid data", errors, 400));
}
Guid tenantId = GetTenantId();
using var scope = _serviceScopeFactory.CreateScope();
var _s3Service = scope.ServiceProvider.GetRequiredService<S3UploadService>();
var _signalR = scope.ServiceProvider.GetRequiredService<IHubContext<MarcoHub>>();
var _employeeHelper = scope.ServiceProvider.GetRequiredService<EmployeeHelper>();
var _firebase = scope.ServiceProvider.GetRequiredService<IFirebaseService>();
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
var batchId = Guid.NewGuid();
@ -641,7 +704,7 @@ namespace MarcoBMS.Services.Controllers
TenantId = tenantId,
AttendanceDate = recordAttendanceDot.Date,
Comment = recordAttendanceDot.Comment,
EmployeeID = recordAttendanceDot.EmployeeID,
EmployeeId = recordAttendanceDot.EmployeeID,
ProjectID = recordAttendanceDot.ProjectID,
Date = DateTime.UtcNow,
InTime = finalDateTime,
@ -673,6 +736,8 @@ namespace MarcoBMS.Services.Controllers
{
attendance.OutTime = finalDateTime;
attendance.Activity = ATTENDANCE_MARK_TYPE.REQUEST_REGULARIZE;
attendance.RequestedById = loggedInEmployee.Id;
attendance.RequestedAt = DateTime.UtcNow;
}
else
{
@ -683,10 +748,14 @@ namespace MarcoBMS.Services.Controllers
case ATTENDANCE_MARK_TYPE.REGULARIZE:
attendance.IsApproved = true;
attendance.Activity = ATTENDANCE_MARK_TYPE.REGULARIZE;
attendance.ApprovedById = loggedInEmployee.Id;
attendance.ApprovedAt = DateTime.UtcNow;
break;
case ATTENDANCE_MARK_TYPE.REGULARIZE_REJECT:
attendance.IsApproved = false;
attendance.Activity = ATTENDANCE_MARK_TYPE.REGULARIZE_REJECT;
attendance.ApprovedById = loggedInEmployee.Id;
attendance.ApprovedAt = DateTime.UtcNow;
break;
}
@ -782,7 +851,7 @@ namespace MarcoBMS.Services.Controllers
var name = $"{vm.FirstName} {vm.LastName}";
await _firebase.SendAttendanceMessageAsync(attendance.ProjectID, name, recordAttendanceDot.Action, attendance.EmployeeID, tenantId);
await _firebase.SendAttendanceMessageAsync(attendance.ProjectID, name, recordAttendanceDot.Action, attendance.EmployeeId, tenantId);
});
@ -819,6 +888,7 @@ namespace MarcoBMS.Services.Controllers
// This single query joins ProjectAllocations with Employees and performs a LEFT JOIN with Attendances.
// This is far more efficient than fetching collections and joining them in memory.
var query = _context.ProjectAllocations
.Include(pa => pa.Project)
.Include(pa => pa.Employee)
.ThenInclude(e => e!.Organization)
.Include(pa => pa.Employee)
@ -835,7 +905,12 @@ namespace MarcoBMS.Services.Controllers
query = query.Where(pa => pa.Employee != null && pa.Employee.OrganizationId == organizationId);
}
List<Attendance> lstAttendance = await _context.Attendes.Where(c => c.ProjectID == projectId && c.AttendanceDate.Date == forDate && c.TenantId == tenantId).ToListAsync();
List<Attendance> lstAttendance = await _context.Attendes
.Include(a => a.RequestedBy)
.ThenInclude(e => e!.JobRole)
.Include(a => a.Approver)
.ThenInclude(e => e!.JobRole)
.Where(c => c.ProjectID == projectId && c.AttendanceDate.Date == forDate && c.TenantId == tenantId).ToListAsync();
var teamAttendance = await query
.AsNoTracking()
@ -852,18 +927,21 @@ namespace MarcoBMS.Services.Controllers
LastName = teamMember.Employee?.LastName,
OrganizationName = teamMember.Employee?.Organization?.Name,
JobRoleName = teamMember.Employee?.JobRole?.Name,
ProjectId = projectId,
ProjectName = teamMember.Project?.Name
};
//var member = emp.Where(e => e.Id == teamMember.EmployeeId);
var attendance = lstAttendance.Find(x => x.EmployeeID == teamMember.EmployeeId) ?? new Attendance();
var attendance = lstAttendance.Find(x => x.EmployeeId == teamMember.EmployeeId) ?? new Attendance();
if (attendance != null)
{
result1.Id = attendance.Id;
result1.CheckInTime = attendance.InTime;
result1.CheckOutTime = attendance.OutTime;
result1.Activity = attendance.Activity;
result1.ApprovedAt = attendance.ApprovedAt;
result1.Approver = _mapper.Map<BasicEmployeeVM>(attendance.Approver);
result1.RequestedAt = attendance.RequestedAt;
result1.RequestedBy = _mapper.Map<BasicEmployeeVM>(attendance.RequestedBy);
}
return result1;
})
@ -882,12 +960,21 @@ namespace MarcoBMS.Services.Controllers
// This query fetches the employee's project allocation and their attendance in a single trip.
Attendance lstAttendance = await _context.Attendes
.FirstOrDefaultAsync(c => c.ProjectID == projectId && c.EmployeeID == employeeId && c.AttendanceDate.Date == forDate && c.TenantId == tenantId) ?? new Attendance();
.Include(a => a.RequestedBy)
.ThenInclude(e => e!.JobRole)
.Include(a => a.Approver)
.ThenInclude(e => e!.JobRole)
.FirstOrDefaultAsync(c => c.ProjectID == projectId && c.EmployeeId == employeeId && c.AttendanceDate.Date == forDate && c.TenantId == tenantId) ?? new Attendance();
var projectAllocationQuery = _context.ProjectAllocations
.Include(pa => pa.Project)
.Include(pa => pa.Employee)
.ThenInclude(e => e!.Organization)
.Where(pa => pa.ProjectId == projectId && pa.EmployeeId == employeeId && pa.TenantId == tenantId && pa.IsActive);
.Include(pa => pa.Employee)
.ThenInclude(e => e!.JobRole)
.Where(pa => pa.EmployeeId == employeeId && pa.TenantId == tenantId && pa.IsActive &&
pa.ProjectId == projectId && pa.Project != null &&
pa.Employee != null && pa.Employee.Organization != null && pa.Employee.JobRole != null);
if (organizationId.HasValue)
{
@ -907,9 +994,15 @@ namespace MarcoBMS.Services.Controllers
OrganizationName = projectAllocation.Employee?.Organization?.Name,
LastName = projectAllocation.Employee?.LastName,
JobRoleName = projectAllocation.Employee?.JobRole?.Name,
ProjectId = projectId,
ProjectName = projectAllocation.Project?.Name,
CheckInTime = lstAttendance.InTime,
CheckOutTime = lstAttendance.OutTime,
Activity = lstAttendance.Activity
Activity = lstAttendance.Activity,
ApprovedAt = lstAttendance.ApprovedAt,
Approver = _mapper.Map<BasicEmployeeVM>(lstAttendance.Approver),
RequestedAt = lstAttendance.RequestedAt,
RequestedBy = _mapper.Map<BasicEmployeeVM>(lstAttendance.RequestedBy)
};
result.Add(result1);
}

View File

@ -7,6 +7,7 @@ using Marco.Pms.Model.Employees;
using Marco.Pms.Model.Entitlements;
using Marco.Pms.Model.Utilities;
using Marco.Pms.Model.ViewModels.Tenant;
using Marco.Pms.Services.Helpers;
using MarcoBMS.Services.Helpers;
using MarcoBMS.Services.Service;
using Microsoft.AspNetCore.Authorization;
@ -319,36 +320,6 @@ namespace MarcoBMS.Services.Controllers
return Unauthorized(ApiResponse<object>.ErrorResponse("MPIN mismatch", "MPIN did not match", 401));
}
if (!string.IsNullOrWhiteSpace(verifyMPIN.FcmToken))
{
var existingFCMTokenMapping = await _context.FCMTokenMappings.Where(ft => ft.FcmToken == verifyMPIN.FcmToken).ToListAsync();
if (existingFCMTokenMapping.Any())
{
_context.FCMTokenMappings.RemoveRange(existingFCMTokenMapping);
}
var fcmTokenMapping = new FCMTokenMapping
{
EmployeeId = requestEmployee.Id,
FcmToken = verifyMPIN.FcmToken,
ExpiredAt = DateTime.UtcNow.AddDays(6),
TenantId = tenantId
};
_context.FCMTokenMappings.Add(fcmTokenMapping);
_logger.LogInfo("New FCM Token registering for employee {EmployeeId}", requestEmployee.Id);
try
{
await _context.SaveChangesAsync();
}
catch (Exception ex)
{
_logger.LogError(ex, "Exception occured while saving FCM Token for employee {EmployeeId}", requestEmployee.Id);
return StatusCode(500, ApiResponse<object>.ErrorResponse("Internal Error", ex.Message, 500));
}
}
// Generate new tokens
var jwtToken = _refreshTokenService.GenerateJwtToken(requestEmployee.Email, tenantId, requestEmployee.OrganizationId, _jwtSettings);
var refreshToken = await _refreshTokenService.CreateRefreshToken(requestEmployee.ApplicationUserId, tenantId.ToString(), requestEmployee.OrganizationId, _jwtSettings);
@ -368,6 +339,100 @@ namespace MarcoBMS.Services.Controllers
}
}
[HttpPost("login-otp/v1")]
public async Task<IActionResult> LoginWithOTP([FromBody] VerifyOTPDto verifyOTP)
{
await using var _context = await _dbContextFactory.CreateDbContextAsync();
using var scope = _serviceScopeFactory.CreateScope();
var _refreshTokenService = scope.ServiceProvider.GetRequiredService<RefreshTokenService>();
try
{
// Validate input
if (string.IsNullOrWhiteSpace(verifyOTP.Email) ||
string.IsNullOrWhiteSpace(verifyOTP.OTP) ||
verifyOTP.OTP.Length != 4 ||
!verifyOTP.OTP.All(char.IsDigit))
{
_logger.LogWarning("OTP login failed - invalid input provided");
return BadRequest(ApiResponse<object>.ErrorResponse("Invalid input", "Please provide a valid 4-digit OTP and Email", 400));
}
// Fetch employee by email
var requestEmployee = await _context.Employees
.Include(e => e.ApplicationUser)
.FirstOrDefaultAsync(e => e.Email == verifyOTP.Email && e.IsActive);
if (requestEmployee == null || string.IsNullOrWhiteSpace(requestEmployee.ApplicationUserId))
{
_logger.LogWarning("OTP login failed - user not found for email {Email}", verifyOTP.Email);
return NotFound(ApiResponse<object>.ErrorResponse("User not found", "User not found", 404));
}
Guid userId = Guid.Parse(requestEmployee.ApplicationUserId);
// Fetch most recent OTP
var otpDetails = await _context.OTPDetails
.Where(o => o.UserId == userId)
.OrderByDescending(o => o.TimeStamp)
.FirstOrDefaultAsync();
if (otpDetails == null)
{
_logger.LogWarning("OTP login failed - no OTP found for user {UserId}", userId);
return NotFound(ApiResponse<object>.ErrorResponse("OTP not found", "No OTP was generated for this user", 404));
}
// Validate OTP expiration
var validUntil = otpDetails.TimeStamp.AddSeconds(otpDetails.ExpriesInSec);
if (DateTime.UtcNow > validUntil || otpDetails.IsUsed)
{
_logger.LogWarning("OTP login failed - OTP expired for user {UserId}", userId);
return BadRequest(ApiResponse<object>.ErrorResponse("OTP expired", "The OTP has expired, please request a new one", 400));
}
// Match OTP
if (otpDetails.OTP != verifyOTP.OTP)
{
_logger.LogWarning("OTP login failed - incorrect OTP entered for user {UserId}", userId);
return Unauthorized(ApiResponse<object>.ErrorResponse("Invalid OTP", "OTP did not match", 401));
}
// Generate access and refresh tokens
//var accessToken = _refreshTokenService.GenerateJwtTokenWithOrganization(requestEmployee.ApplicationUser?.UserName, requestEmployee.OrganizationId, _jwtSettings);
//var refreshToken = await _refreshTokenService.CreateRefreshTokenWithOrganization(requestEmployee.ApplicationUserId, requestEmployee.OrganizationId, _jwtSettings);
var tenant = await _context.Tenants.FirstOrDefaultAsync(t => t.OrganizationId == requestEmployee.OrganizationId);
var accessToken = _refreshTokenService.GenerateJwtToken(requestEmployee.ApplicationUser?.UserName,
tenant?.Id ?? Guid.Empty, requestEmployee.OrganizationId, _jwtSettings);
var refreshToken = await _refreshTokenService.CreateRefreshToken(requestEmployee.ApplicationUserId,
tenant?.Id.ToString(), requestEmployee.OrganizationId, _jwtSettings);
// Fetch MPIN token if exists
var mpinDetails = await _context.MPINDetails
.FirstOrDefaultAsync(p => p.UserId == userId);
// Build and return response
var response = new
{
token = accessToken,
refreshToken,
mpinToken = mpinDetails?.MPINToken
};
otpDetails.IsUsed = true;
await _context.SaveChangesAsync();
_logger.LogInfo("OTP login successful for employee {EmployeeId}", requestEmployee.Id);
return Ok(ApiResponse<object>.SuccessResponse(response, "User logged in successfully.", 200));
}
catch (Exception ex)
{
_logger.LogError(ex, "An unexpected error occurred during OTP login for email {Email}", verifyOTP.Email ?? string.Empty);
return StatusCode(500, ApiResponse<object>.ErrorResponse("Unexpected error", ex.Message, 500));
}
}
// new login APIs
[HttpPost("login")]
@ -944,7 +1009,7 @@ namespace MarcoBMS.Services.Controllers
}
[HttpPost("login-otp")]
public async Task<IActionResult> LoginWithOTP([FromBody] VerifyOTPDto verifyOTP)
public async Task<IActionResult> LoginWithOTPAsync([FromBody] VerifyOTPDto verifyOTP)
{
await using var _context = await _dbContextFactory.CreateDbContextAsync();
using var scope = _serviceScopeFactory.CreateScope();
@ -1294,33 +1359,37 @@ namespace MarcoBMS.Services.Controllers
_logger.LogInfo("Fetching TenantOrgMappings for OrganizationId: {OrganizationId}", organizationId);
// Retrieve all TenantOrgMappings that match the organizationId and have a related Tenant
var tenantOrganizationMapping = await _context.TenantOrgMappings
.Include(to => to.Tenant)
.ThenInclude(t => t!.TenantStatus)
.Include(to => to.Tenant)
.ThenInclude(t => t!.Industry)
.Where(to => to.OrganizationId == organizationId && to.Tenant != null)
.ToListAsync();
var tenantOrgMappingTask = Task.Run(async () =>
{
await using var context = await _dbContextFactory.CreateDbContextAsync();
return await context.TenantOrgMappings.Where(to => to.OrganizationId == organizationId && to.Tenant != null).Select(to => to.TenantId).ToListAsync();
});
var projectTask = Task.Run(async () =>
{
await using var context = await _dbContextFactory.CreateDbContextAsync();
return await context.Projects.Where(to => to.PromoterId == organizationId || to.PMCId == organizationId).Select(to => to.TenantId).ToListAsync();
});
var tenantList = tenantOrganizationMapping.Select(to => to.Tenant!).ToList();
await Task.WhenAll(tenantOrgMappingTask, projectTask);
var tenantIds = tenantOrgMappingTask.Result;
tenantIds.AddRange(projectTask.Result);
tenantIds = tenantIds.Distinct().ToList();
// Additionally fetch the Tenant record associated directly with this OrganizationId if any
var tenant = await _context.Tenants
var tenants = await _context.Tenants
.Include(t => t.Industry)
.Include(t => t.TenantStatus)
.FirstOrDefaultAsync(t => t.OrganizationId == organizationId);
if (tenant != null)
{
tenantList.Add(tenant);
}
.Where(t => t.OrganizationId == organizationId || tenantIds.Contains(t.Id)).ToListAsync();
tenantList = tenantList.Distinct().ToList();
tenants = tenants.Distinct().ToList();
// Map the tenant entities to TenantListVM view models
var response = _mapper.Map<List<TenantListVM>>(tenantList);
var response = _mapper.Map<List<TenantListVM>>(tenants);
_logger.LogInfo("Fetched {Count} tenants for OrganizationId: {OrganizationId}", tenantList.Count, organizationId);
_logger.LogInfo("Fetched {Count} tenants for OrganizationId: {OrganizationId}", tenants.Count, organizationId);
_logger.LogDebug("GetTenantAsync method completed successfully.");
return Ok(ApiResponse<object>.SuccessResponse(response, "Successfully fetched the list of tenant", 200));
@ -1401,6 +1470,9 @@ namespace MarcoBMS.Services.Controllers
// Generate and store refresh token
var refreshToken = await _refreshTokenService.CreateRefreshToken(loggedInEmployee.ApplicationUserId, tenantId.ToString(), loggedInEmployee.OrganizationId, _jwtSettings);
var _cache = scope.ServiceProvider.GetRequiredService<CacheUpdateHelper>();
await _cache.ClearAllEmployeesFromCacheByOnlyEmployeeId(loggedInEmployee.Id);
_logger.LogInfo("Tenant selected and tokens generated for TenantId: {TenantId} and Employee: {EmployeeEmail}", tenantId, loggedInEmployee.Email ?? string.Empty);
// Return success response including tokens

View File

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

View File

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

View File

@ -19,6 +19,7 @@ using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.SignalR;
using Microsoft.CodeAnalysis;
using Microsoft.EntityFrameworkCore;
using System.Data;
using System.Net;
@ -31,9 +32,9 @@ namespace MarcoBMS.Services.Controllers
public class EmployeeController : ControllerBase
{
private readonly IDbContextFactory<ApplicationDbContext> _dbContextFactory;
private readonly ApplicationDbContext _context;
private readonly IServiceScopeFactory _serviceScope;
private readonly IServiceScopeFactory _serviceScopeFactory;
private readonly UserManager<ApplicationUser> _userManager;
private readonly IEmailSender _emailSender;
private readonly EmployeeHelper _employeeHelper;
@ -46,9 +47,11 @@ namespace MarcoBMS.Services.Controllers
private readonly IMapper _mapper;
private readonly IProjectServices _projectServices;
private readonly Guid tenantId;
private readonly Guid organizationId;
public EmployeeController(IServiceScopeFactory serviceScope,
public EmployeeController(IDbContextFactory<ApplicationDbContext> dbContextFactory,
IServiceScopeFactory serviceScopeFactory,
UserManager<ApplicationUser> userManager,
IEmailSender emailSender,
ApplicationDbContext context,
@ -62,7 +65,8 @@ namespace MarcoBMS.Services.Controllers
IMapper mapper,
GeneralHelper generalHelper)
{
_serviceScope = serviceScope;
_dbContextFactory = dbContextFactory ?? throw new ArgumentNullException(nameof(dbContextFactory));
_serviceScopeFactory = serviceScopeFactory;
_context = context;
_userManager = userManager;
_emailSender = emailSender;
@ -76,6 +80,7 @@ namespace MarcoBMS.Services.Controllers
_projectServices = projectServices;
_mapper = mapper;
tenantId = _userHelper.GetTenantId();
organizationId = _userHelper.GetCurrentOrganizationId();
}
[HttpGet]
@ -117,9 +122,93 @@ namespace MarcoBMS.Services.Controllers
}
}
[HttpGet]
[Route("list/{projectid?}")]
public async Task<IActionResult> GetEmployeesByProject(Guid? projectid, [FromQuery] bool ShowInactive)
[HttpGet("list/organizations/{projectId}")]
public async Task<IActionResult> GetEmployeesByProjectAsync(Guid projectId, [FromQuery] string searchString, [FromQuery] Guid? organizationId)
{
try
{
// Get the currently logged-in employee information
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
var projectTask = Task.Run(async () =>
{
await using var context = await _dbContextFactory.CreateDbContextAsync();
return await context.Projects.FirstOrDefaultAsync(p => p.Id == projectId && p.TenantId == tenantId);
});
var tenantTask = Task.Run(async () =>
{
await using var context = await _dbContextFactory.CreateDbContextAsync();
return await context.Tenants.FirstOrDefaultAsync(t => t.Id == tenantId);
});
await Task.WhenAll(projectTask, tenantTask);
var project = projectTask.Result;
var tenant = tenantTask.Result;
if (project == null || tenant == null)
{
_logger.LogWarning("Project {ProjectId} not found in database for tenant {TenantId}", projectId, tenantId);
return NotFound(ApiResponse<object>.ErrorResponse("Project not found", "Project not found", 404));
}
// Check if the logged-in employee has permission for the requested project
var hasProjectPermission = await _permission.HasProjectPermission(loggedInEmployee, projectId);
if (!hasProjectPermission)
{
_logger.LogWarning("User {EmployeeId} attempts to get employees for project {ProjectId} without permission", loggedInEmployee.Id, projectId);
return StatusCode(403, ApiResponse<object>.ErrorResponse("Access denied", "User does not have access to view the employees for this project", 403));
}
var organizationQuery = _context.ProjectOrgMappings
.Include(po => po.ProjectService)
.Where(po => po.ProjectService != null && po.ProjectService.ProjectId == projectId);
if (loggedInEmployee.OrganizationId != project.PMCId && loggedInEmployee.OrganizationId != project.PromoterId && loggedInEmployee.OrganizationId != tenant.OrganizationId)
{
organizationQuery = organizationQuery.Where(po => po.ParentOrganizationId == loggedInEmployee.OrganizationId || po.OrganizationId == loggedInEmployee.OrganizationId);
}
var organizationIds = await organizationQuery.Select(po => po.OrganizationId).ToListAsync();
if (loggedInEmployee.OrganizationId == project.PMCId || loggedInEmployee.OrganizationId == project.PromoterId || loggedInEmployee.OrganizationId == tenant.OrganizationId)
{
organizationIds.Add(project.PMCId);
organizationIds.Add(project.PromoterId);
organizationIds.Add(tenant.OrganizationId);
}
// Fetch employees allocated to the project matching the search criteria
var employeesQuery = _context.Employees
.AsNoTracking() // Improves performance by disabling change tracking for read-only query
.Include(e => e.JobRole)
.Where(e => (e.FirstName + " " + e.LastName).Contains(searchString) && organizationIds.Contains(e.OrganizationId));
if (organizationId.HasValue)
{
employeesQuery = employeesQuery.Where(e => e.OrganizationId == organizationId);
}
var employees = await employeesQuery
.ToListAsync();
var result = employees.Select(e => _mapper.Map<EmployeeVM>(e)).Distinct().ToList();
_logger.LogInfo("Employees fetched for project {ProjectId} by user {EmployeeId}. Count: {Count}", projectId, loggedInEmployee.Id, employees.Count);
// Return the employee list wrapped in a successful API response
return Ok(ApiResponse<object>.SuccessResponse(result, "Employee list fetched successfully", 200));
}
catch (Exception ex)
{
// Log the exception and return a 500 status code with error message
_logger.LogError(ex, "Error occurred while fetching employees for project {ProjectId}", projectId);
return StatusCode(500, ApiResponse<object>.ErrorResponse("Internal server error", "An unexpected error occurred", 500));
}
}
[HttpGet("list/{projectId?}")]
public async Task<IActionResult> GetEmployeesByProjectAsync(Guid? projectId, [FromQuery] bool showInactive = false)
{
// Step 1: Validate incoming request model state
if (!ModelState.IsValid)
@ -133,61 +222,117 @@ namespace MarcoBMS.Services.Controllers
return BadRequest(ApiResponse<object>.ErrorResponse("Invalid data", errors, 400));
}
// Step 2: Get logged-in employee
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
_logger.LogInfo("GetEmployeesByProject called by EmployeeId: {EmployeeId}, ProjectId: {ProjectId}, ShowInactive: {ShowInactive}",
loggedInEmployee.Id, projectid ?? Guid.Empty, ShowInactive);
// Step 3: Fetch project access and permissions
var projectIds = await _projectServices.GetMyProjectIdsAsync(tenantId, loggedInEmployee);
var hasViewAllEmployeesPermission = await _permission.HasPermission(PermissionsMaster.ViewAllEmployees, loggedInEmployee.Id);
var hasViewTeamMembersPermission = await _permission.HasPermission(PermissionsMaster.ViewTeamMembers, loggedInEmployee.Id);
List<EmployeeVM> result = new();
// Step 4: Determine access level and fetch employees accordingly
if (hasViewAllEmployeesPermission || projectid != null)
List<EmployeeVM> result = new List<EmployeeVM>();
try
{
result = await _employeeHelper.GetEmployeeByProjectId(tenantId, projectid, ShowInactive);
_logger.LogInfo("Employee list fetched using full access or specific project.");
}
else if (hasViewTeamMembersPermission && !ShowInactive)
{
var employeeIds = await _context.ProjectAllocations
.Where(pa => projectIds.Contains(pa.ProjectId) && pa.IsActive && pa.TenantId == tenantId)
.Select(pa => pa.EmployeeId)
.Distinct()
.ToListAsync();
// Dependency injection scope for services
using var scope = _serviceScopeFactory.CreateScope();
var employees = await _context.Employees
.Include(fp => fp.JobRole)
.Where(e => employeeIds.Contains(e.Id) && e.JobRole != null && e.IsActive && e.TenantId == tenantId)
// Step 2: Get logged-in employee details
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
_logger.LogInfo("GetEmployeesByProject called. EmployeeId: {EmployeeId}, ProjectId: {ProjectId}, showInactive: {ShowInactive}",
loggedInEmployee.Id, projectId ?? Guid.Empty, showInactive);
// Step 3: Fetch permissions concurrently
var viewAllTask = Task.Run(async () =>
{
var _permission = scope.ServiceProvider.GetRequiredService<PermissionServices>();
return await _permission.HasPermission(PermissionsMaster.ViewAllEmployees, loggedInEmployee.Id);
});
var viewTeamTask = Task.Run(async () =>
{
var _permission = scope.ServiceProvider.GetRequiredService<PermissionServices>();
return await _permission.HasPermission(PermissionsMaster.ViewTeamMembers, loggedInEmployee.Id);
});
await Task.WhenAll(viewAllTask, viewTeamTask);
var hasViewAllEmployeesPermission = viewAllTask.Result;
var hasViewTeamMembersPermission = viewTeamTask.Result;
List<Employee> employees = new List<Employee>();
// Step 4: Query based on permission
if (hasViewAllEmployeesPermission && !projectId.HasValue)
{
// OrganizationId needs to be retrieved from loggedInEmployee or context based on your app's structure
var employeeQuery = _context.Employees
.AsNoTracking() // Optimize EF query for read-only operation[web:1][web:13][web:18]
.Include(e => e.JobRole)
.Where(e => e.OrganizationId == organizationId);
employeeQuery = showInactive
? employeeQuery.Where(e => !e.IsActive)
: employeeQuery.Where(e => e.IsActive);
employees = await employeeQuery.ToListAsync();
_logger.LogInfo("Employee list fetched with full access. Count: {Count}", employees.Count);
}
else if (hasViewTeamMembersPermission && !showInactive && !projectId.HasValue)
{
// Only active team members with limited permission
var projectIds = await _projectServices.GetMyProjectIdsAsync(tenantId, loggedInEmployee);
employees = await _context.ProjectAllocations
.AsNoTracking()
.Include(pa => pa.Employee)
.ThenInclude(e => e!.JobRole)
.Where(pa =>
projectIds.Contains(pa.ProjectId)
&& pa.IsActive
&& pa.Employee != null
&& pa.Employee.IsActive
&& pa.TenantId == tenantId)
.Select(pa => pa.Employee!)
.Distinct()
.ToListAsync();
result = employees.Select(e => e.ToEmployeeVMFromEmployee()).ToList();
_logger.LogInfo("Employee list fetched with limited access (active only). Count: {Count}", employees.Count);
}
// If a specific projectId is provided, override employee fetching to ensure strict project context
if (projectId.HasValue)
{
employees = await _context.ProjectAllocations
.AsNoTracking()
.Include(pa => pa.Employee)
.ThenInclude(e => e!.JobRole)
.Where(pa =>
pa.ProjectId == projectId
&& pa.IsActive
&& pa.Employee != null
&& pa.Employee.IsActive
&& pa.TenantId == tenantId)
.Select(pa => pa.Employee!)
.Distinct()
.ToListAsync();
_logger.LogInfo("Employee list fetched for specific project. ProjectId: {ProjectId}. Count: {Count}",
projectId, employees.Count);
}
// Step 5: Map to view model
result = employees.Select(e => _mapper.Map<EmployeeVM>(e)).Distinct().ToList();
_logger.LogInfo("Employees successfully fetched. EmployeeId: {EmployeeId} for ProjectId: {ProjectId}. Final Count: {Count}",
loggedInEmployee.Id, projectId ?? Guid.Empty, result.Count);
_logger.LogInfo("Employee list fetched using limited access (active only).");
}
else
{
_logger.LogWarning("Access denied for EmployeeId: {EmployeeId} - insufficient permissions.", loggedInEmployee.Id);
return Ok(ApiResponse<object>.SuccessResponse(result, "Filter applied.", 200));
}
// Step 5: Log and return results
_logger.LogInfo("Employees fetched successfully by EmployeeId: {EmployeeId} for ProjectId: {ProjectId}. Count: {Count}",
loggedInEmployee.Id, projectid ?? Guid.Empty, result.Count);
return Ok(ApiResponse<object>.SuccessResponse(result, "Filter applied.", 200));
catch (Exception ex)
{
// Step 6: Error logging and response[web:6]
_logger.LogError(ex, "Exception occurred while getting the list of employees");
return StatusCode(500, ApiResponse<object>.ErrorResponse("Internal server error. Please try again later.", null, 500));
}
}
[HttpGet("basic")]
public async Task<IActionResult> GetEmployeesByProjectBasic(Guid? projectId, [FromQuery] string? searchString)
public async Task<IActionResult> GetEmployeesByProjectBasic(Guid? projectId, [FromQuery] string? searchString, [FromQuery] bool allEmployee)
{
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
var employeeQuery = _context.Employees.Where(e => e.TenantId == tenantId);
var employeeQuery = _context.Employees.Where(e => e.IsActive);
if (projectId != null && projectId != Guid.Empty)
{
var hasProjectPermission = await _permission.HasProjectPermission(loggedInEmployee, projectId.Value);
@ -199,13 +344,26 @@ namespace MarcoBMS.Services.Controllers
var employeeIds = await _context.ProjectAllocations.Where(pa => pa.ProjectId == projectId && pa.IsActive && pa.TenantId == tenantId).Select(p => p.EmployeeId).ToListAsync();
employeeQuery = employeeQuery.Where(e => employeeIds.Contains(e.Id));
}
else
{
employeeQuery = employeeQuery.Where(e => e.OrganizationId == organizationId);
}
if (!string.IsNullOrWhiteSpace(searchString))
{
var searchStringLower = searchString.ToLower();
employeeQuery = employeeQuery.Where(e => (e.FirstName + " " + e.LastName).ToLower().Contains(searchStringLower));
}
var response = await employeeQuery.Take(10).Select(e => _mapper.Map<BasicEmployeeVM>(e)).ToListAsync();
var query = employeeQuery.OrderBy(e => e.FirstName);
if (!allEmployee)
{
query = (IOrderedQueryable<Employee>)query.Take(10);
}
var response = await query
.Select(e => _mapper.Map<BasicEmployeeVM>(e))
.ToListAsync();
return Ok(ApiResponse<object>.SuccessResponse(response, $"{response.Count} records of employees fetched successfully", 200));
}
@ -315,7 +473,7 @@ namespace MarcoBMS.Services.Controllers
}
[HttpPost("manage")]
[HttpPost("old/manage")]
public async Task<IActionResult> CreateUser([FromBody] CreateUserDto model)
{
Guid tenantId = _userHelper.GetTenantId();
@ -448,6 +606,203 @@ namespace MarcoBMS.Services.Controllers
return Ok(ApiResponse<object>.SuccessResponse("Success.", responsemessage, 200));
}
[HttpPost("manage")]
public async Task<IActionResult> CreateEmployeeAsync([FromBody] CreateUserDto model)
{
// Correlation and context capture for logs
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
{
if (model == null)
{
_logger.LogWarning("Model is null in CreateEmployeeAsync");
return BadRequest(ApiResponse<object>.ErrorResponse("Invalid payload", "Request body is required", 400));
}
// Basic validation
if (model.HasApplicationAccess && string.IsNullOrWhiteSpace(model.Email))
{
_logger.LogWarning("Application access requested but email is missing");
return BadRequest(ApiResponse<object>.ErrorResponse("Invalid email", "Application users must have a valid email", 400));
}
await using var transaction = await _context.Database.BeginTransactionAsync();
try
{
// Load existing employee if updating, constrained by organization scope
Employee? existingEmployee = null;
if (model.Id.HasValue && model.Id.Value != Guid.Empty)
{
existingEmployee = await _context.Employees
.FirstOrDefaultAsync(e => e.Id == model.Id);
if (existingEmployee == null)
{
_logger.LogInfo("Employee not found for update. Id={EmployeeId}", model.Id);
return NotFound(ApiResponse<object>.ErrorResponse("Employee not found", "Employee not found in database", 404));
}
}
// Identity user creation path (only if needed)
ApplicationUser? identityUserToCreate = null;
ApplicationUser? createdIdentityUser = null;
if (model.HasApplicationAccess)
{
// Only attempt identity resolution/creation if email supplied and either:
// - Creating new employee, or
// - Updating but existing employee does not have ApplicationUserId
var needsIdentity = string.IsNullOrWhiteSpace(existingEmployee?.ApplicationUserId);
if (needsIdentity && !string.IsNullOrWhiteSpace(model.Email))
{
var existingUser = await _userManager.FindByEmailAsync(model.Email);
if (existingUser == null)
{
// Seat check only when provisioning a new identity user
var isSeatsAvailable = await _generalHelper.CheckSeatsRemainingAsync(tenantId);
if (!isSeatsAvailable)
{
_logger.LogWarning("Maximum users reached for Tenant {TenantId}. Cannot create identity user for {Email}", tenantId, model.Email);
return BadRequest(ApiResponse<object>.ErrorResponse(
"Maximum number of users reached. Cannot add new user",
"Maximum number of users reached. Cannot add new user", 400));
}
identityUserToCreate = new ApplicationUser
{
UserName = model.Email,
Email = model.Email,
EmailConfirmed = true
};
}
else
{
// If identity exists, re-use it; do not re-create
createdIdentityUser = existingUser;
}
}
}
// For create path: enforce uniqueness of employee email if applicable to business rules
// Consider adding a unique filtered index: (OrganizationId, Email) WHERE Email IS NOT NULL
if (!model.Id.HasValue || model.Id == Guid.Empty)
{
if (!string.IsNullOrWhiteSpace(model.Email))
{
var emailExists = await _context.Employees
.AnyAsync(e => e.Email == model.Email);
if (emailExists)
{
_logger.LogInfo("Employee email already exists. Email={Email}", model.Email);
return StatusCode(403, ApiResponse<object>.ErrorResponse(
"Employee with email already exists",
"Employee with this email already exists", 403));
}
}
}
// Create identity user if needed
if (identityUserToCreate != null && !string.IsNullOrWhiteSpace(identityUserToCreate.Email))
{
var createResult = await _userManager.CreateAsync(identityUserToCreate, "User@123");
if (!createResult.Succeeded)
{
_logger.LogWarning("Failed to create identity user for {Email}. Errors={Errors}",
identityUserToCreate.Email,
string.Join(", ", createResult.Errors.Select(e => $"{e.Code}:{e.Description}")));
return BadRequest(ApiResponse<object>.ErrorResponse("Failed to create user", createResult.Errors, 400));
}
createdIdentityUser = identityUserToCreate;
_logger.LogInfo("Identity user created. IdentityUserId={UserId}, Email={Email}",
createdIdentityUser.Id, createdIdentityUser.Email);
}
Guid employeeId;
EmployeeVM employeeVM;
string responseMessage;
if (existingEmployee != null)
{
// Update flow
_mapper.Map(model, existingEmployee);
if (createdIdentityUser != null && !string.IsNullOrWhiteSpace(createdIdentityUser.Email))
{
existingEmployee.ApplicationUserId = createdIdentityUser.Id;
await SendResetIfApplicableAsync(createdIdentityUser, existingEmployee.FirstName ?? "User");
}
await _context.SaveChangesAsync();
employeeId = existingEmployee.Id;
employeeVM = _mapper.Map<EmployeeVM>(existingEmployee);
responseMessage = "Employee Updated Successfully";
_logger.LogInfo("Employee updated. EmployeeId={EmployeeId}, Org={OrgId}", employeeId, existingEmployee.OrganizationId);
}
else
{
// Create flow
var newEmployee = _mapper.Map<Employee>(model);
newEmployee.IsSystem = false;
newEmployee.IsActive = true;
newEmployee.IsPrimary = false;
if (createdIdentityUser != null && !string.IsNullOrWhiteSpace(createdIdentityUser.Email))
{
newEmployee.ApplicationUserId = createdIdentityUser.Id;
await SendResetIfApplicableAsync(createdIdentityUser, newEmployee.FirstName ?? "User");
}
await _context.Employees.AddAsync(newEmployee);
await _context.SaveChangesAsync();
employeeId = newEmployee.Id;
employeeVM = _mapper.Map<EmployeeVM>(newEmployee);
responseMessage = "Employee Created Successfully";
_logger.LogInfo("Employee created. EmployeeId={EmployeeId}, Org={OrgId}", employeeId, newEmployee.OrganizationId);
}
await transaction.CommitAsync();
// SignalR notification
var notification = new
{
LoggedInUserId = loggedInEmployee.Id,
Keyword = "Employee",
EmployeeId = employeeId
};
// Consider broadcasting to tenant/organization group instead of Clients.All to avoid cross-tenant leaks:
// await _signalR.Clients.Group($"org:{model.OrganizationId}").SendAsync("NotificationEventHandler", notification);
await _signalR.Clients.All.SendAsync("NotificationEventHandler", notification);
_logger.LogInfo("Notification broadcasted for EmployeeId={EmployeeId}", employeeId);
return Ok(ApiResponse<object>.SuccessResponse(employeeVM, responseMessage, 200));
}
catch (DbUpdateException dbEx)
{
await transaction.RollbackAsync();
_logger.LogError(dbEx, "Database exception occurred while managing employee");
return StatusCode(500, ApiResponse<object>.ErrorResponse(
"Internal exception occurred",
"Internal database exception has occurred", 500));
}
catch (Exception ex)
{
await transaction.RollbackAsync();
_logger.LogError(ex, "Unhandled exception occurred while managing employee");
return StatusCode(500, ApiResponse<object>.ErrorResponse(
"Internal exception occurred",
"Internal exception has occurred", 500));
}
}
}
[HttpPost("manage-mobile")]
public async Task<IActionResult> CreateUserMoblie([FromBody] MobileUserManageDto model)
{
@ -527,10 +882,205 @@ namespace MarcoBMS.Services.Controllers
}
}
[HttpPost("app/manage")]
public async Task<IActionResult> CreateUserMobileAsync([FromBody] MobileUserManageDto model)
{
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
if (tenantId == Guid.Empty)
{
_logger.LogWarning("Tenant resolution failed in CreateUserMobile"); // structured warning
return StatusCode(403, ApiResponse<object>.ErrorResponse("Unauthorized tenant context", "Unauthorized", 403));
}
if (model is null)
{
_logger.LogWarning("Null payload in CreateUserMobile for Tenant {TenantId}", tenantId); // validation log
return BadRequest(ApiResponse<object>.ErrorResponse("Invalid data", "Invalid data", 400));
}
if (string.IsNullOrWhiteSpace(model.FirstName) || string.IsNullOrWhiteSpace(model.PhoneNumber))
{
_logger.LogWarning("Missing required fields FirstName/Phone for Tenant {TenantId}", tenantId); // validation log
return BadRequest(ApiResponse<object>.ErrorResponse("First name and phone number are required.", "Required fields missing", 400));
}
// Strict Base64 parse
byte[]? imageBytes = null;
if (!string.IsNullOrWhiteSpace(model.ProfileImage))
{
try
{
imageBytes = Convert.FromBase64String(model.ProfileImage);
}
catch (FormatException ex)
{
_logger.LogError(ex, "Invalid base64 image in CreateUserMobile for Tenant {TenantId}", tenantId); // input issue
return BadRequest(ApiResponse<object>.ErrorResponse("Invalid image format.", "Invalid image", 400));
}
}
if (model.Id == null || model.Id == Guid.Empty)
{
var emailExists = await _context.Employees
.AnyAsync(e => e.Email == model.Email && e.OrganizationId == model.OrganizationId);
if (emailExists && !string.IsNullOrWhiteSpace(model.Email))
{
_logger.LogInfo("Employee email already exists in org. Email={Email}, Org={OrgId}", model.Email, model.OrganizationId);
return StatusCode(409, ApiResponse<object>.ErrorResponse("Employee with email already exists", "Employee with this email already exists", 409));
}
// Create path: map only allowed fields
var employee = new Employee
{
Id = Guid.NewGuid(),
TenantId = tenantId,
FirstName = model.FirstName.Trim(),
LastName = model.LastName?.Trim(),
Email = model.Email,
Gender = model.Gender,
PhoneNumber = model.PhoneNumber,
JoiningDate = model.JoiningDate,
JobRoleId = model.JobRoleId,
Photo = imageBytes,
OrganizationId = model.OrganizationId,
HasApplicationAccess = model.HasApplicationAccess,
};
if (!string.IsNullOrWhiteSpace(model.Email) && model.HasApplicationAccess)
{
var existingUser = await _userManager.FindByEmailAsync(model.Email);
if (existingUser == null)
{
existingUser = new ApplicationUser
{
UserName = model.Email,
Email = model.Email,
EmailConfirmed = true
};
var createResult = await _userManager.CreateAsync(existingUser, "User@123");
if (!createResult.Succeeded)
{
_logger.LogWarning("Failed to create identity user for {Email}. Errors={Errors}",
existingUser.Email,
string.Join(", ", createResult.Errors.Select(e => $"{e.Code}:{e.Description}")));
return BadRequest(ApiResponse<object>.ErrorResponse("Failed to create user", createResult.Errors, 400));
}
await SendResetIfApplicableAsync(existingUser, employee.FirstName ?? "User");
employee.ApplicationUserId = existingUser.Id;
}
}
_context.Employees.Add(employee);
await _context.SaveChangesAsync();
var employeeVM = _mapper.Map<EmployeeVM>(employee);
var notification = new
{
LoggedInUserId = loggedInEmployee?.Id,
Keyword = "Employee",
EmployeeId = employee.Id
};
// Consider broadcasting to tenant/organization group instead of Clients.All to avoid cross-tenant leaks:
// await _signalR.Clients.Group($"org:{model.OrganizationId}").SendAsync("NotificationEventHandler", notification);
await _signalR.Clients.All.SendAsync("NotificationEventHandler", notification);
_logger.LogInfo("Employee {EmployeeId} created in Tenant {TenantId}", employee.Id, tenantId); // success
return Ok(ApiResponse<object>.SuccessResponse(employeeVM, "Employee created successfully", 200));
}
else
{
// Update path: fetch scoped to tenant
var employeeId = model.Id.Value;
var existingEmployee = await _context.Employees
.FirstOrDefaultAsync(e => e.Id == employeeId); // tenant-safe lookup
if (existingEmployee is null)
{
_logger.LogWarning("Update attempted for missing Employee {EmployeeId} in Tenant {TenantId}", employeeId, tenantId); // not found
return NotFound(ApiResponse<object>.ErrorResponse("Employee not found", "Not found", 404));
}
// Update allowed fields only
existingEmployee.FirstName = model.FirstName.Trim();
existingEmployee.LastName = model.LastName?.Trim();
existingEmployee.Gender = model.Gender;
existingEmployee.PhoneNumber = model.PhoneNumber;
existingEmployee.JoiningDate = model.JoiningDate;
existingEmployee.JobRoleId = model.JobRoleId;
existingEmployee.OrganizationId = model.OrganizationId;
existingEmployee.HasApplicationAccess = model.HasApplicationAccess;
if (string.IsNullOrWhiteSpace(existingEmployee.Email) && !string.IsNullOrWhiteSpace(model.Email))
{
var emailExists = await _context.Employees
.AnyAsync(e => e.Email == model.Email);
if (emailExists)
{
_logger.LogInfo("Employee email already exists in org. Email={Email}, Org={OrgId}", model.Email, model.OrganizationId);
return StatusCode(409, ApiResponse<object>.ErrorResponse("Employee with email already exists", "Employee with this email already exists", 409));
}
existingEmployee.Email = model.Email;
}
if (model.HasApplicationAccess && !string.IsNullOrWhiteSpace(model.Email) && string.IsNullOrWhiteSpace(existingEmployee.ApplicationUserId))
{
var existingUser = await _userManager.FindByEmailAsync(model.Email);
if (existingUser == null)
{
existingUser = new ApplicationUser
{
UserName = model.Email,
Email = model.Email,
EmailConfirmed = true
};
var createResult = await _userManager.CreateAsync(existingUser, "User@123");
if (!createResult.Succeeded)
{
_logger.LogWarning("Failed to create identity user for {Email}. Errors={Errors}",
existingUser.Email,
string.Join(", ", createResult.Errors.Select(e => $"{e.Code}:{e.Description}")));
return BadRequest(ApiResponse<object>.ErrorResponse("Failed to create user", createResult.Errors, 400));
}
await SendResetIfApplicableAsync(existingUser, existingEmployee.FirstName ?? "User");
existingEmployee.ApplicationUserId = existingUser.Id;
}
}
if (imageBytes != null)
{
existingEmployee.Photo = imageBytes;
}
await _context.SaveChangesAsync();
var employeeVM = _mapper.Map<EmployeeVM>(existingEmployee);
var notification = new
{
LoggedInUserId = loggedInEmployee?.Id,
Keyword = "Employee",
EmployeeId = employeeId
};
// Consider broadcasting to tenant/organization group instead of Clients.All to avoid cross-tenant leaks:
// await _signalR.Clients.Group($"org:{model.OrganizationId}").SendAsync("NotificationEventHandler", notification);
await _signalR.Clients.All.SendAsync("NotificationEventHandler", notification);
_logger.LogInfo("Employee {EmployeeId} updated in Tenant {TenantId}", existingEmployee.Id, tenantId); // success
return Ok(ApiResponse<object>.SuccessResponse(employeeVM, "Employee updated successfully", 200));
}
}
[HttpDelete("{id}")]
public async Task<IActionResult> SuspendEmployee(Guid id, [FromQuery] bool active = false)
{
using var scope = _serviceScope.CreateScope();
using var scope = _serviceScopeFactory.CreateScope();
Guid tenantId = _userHelper.GetTenantId();
var LoggedEmployee = await _userHelper.GetCurrentEmployeeAsync();
@ -561,7 +1111,7 @@ namespace MarcoBMS.Services.Controllers
}
}
}
var attendance = await _context.Attendes.Where(a => a.EmployeeID == employee.Id && (a.OutTime == null || a.Activity == ATTENDANCE_MARK_TYPE.REQUEST_REGULARIZE)).ToListAsync();
var attendance = await _context.Attendes.Where(a => a.EmployeeId == employee.Id && (a.OutTime == null || a.Activity == ATTENDANCE_MARK_TYPE.REQUEST_REGULARIZE)).ToListAsync();
if (attendance.Count != 0)
{
_logger.LogWarning("Employee with ID {EmployeeId} have any pending check-out or regularization requests", employee.Id);
@ -703,5 +1253,14 @@ namespace MarcoBMS.Services.Controllers
return info;
}
// Prepare reset link sender helper
private async Task SendResetIfApplicableAsync(ApplicationUser u, string firstName)
{
var token = await _userManager.GeneratePasswordResetTokenAsync(u);
var resetLink = $"{_configuration["AppSettings:WebFrontendUrl"]}/reset-password?token={WebUtility.UrlEncode(token)}";
await _emailSender.SendResetPasswordEmailOnRegister(u.Email ?? "", firstName, resetLink);
_logger.LogInfo("Reset password email queued. Email={Email}", u.Email ?? "");
}
}
}

View File

@ -13,6 +13,7 @@ using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.CodeAnalysis;
using Microsoft.EntityFrameworkCore;
using Serilog;
using System.Text.Json;
namespace Marco.Pms.Services.Controllers
@ -22,20 +23,27 @@ namespace Marco.Pms.Services.Controllers
[Authorize]
public class ImageController : ControllerBase
{
private readonly IDbContextFactory<ApplicationDbContext> _dbContextFactory;
private readonly ApplicationDbContext _context;
private readonly S3UploadService _s3Service;
private readonly UserHelper _userHelper;
private readonly ILoggingService _logger;
private readonly PermissionServices _permission;
private readonly Guid tenantId;
public ImageController(ApplicationDbContext context, S3UploadService s3Service, UserHelper userHelper, ILoggingService logger, PermissionServices permission)
public ImageController(IDbContextFactory<ApplicationDbContext> dbContextFactory,
ApplicationDbContext context,
S3UploadService s3Service,
UserHelper userHelper,
ILoggingService logger,
PermissionServices permission)
{
_context = context;
_s3Service = s3Service;
_userHelper = userHelper;
_logger = logger;
_dbContextFactory = dbContextFactory ?? throw new ArgumentNullException(nameof(dbContextFactory));
_context = context ?? throw new ArgumentNullException(nameof(context));
_s3Service = s3Service ?? throw new ArgumentNullException(nameof(s3Service));
_userHelper = userHelper ?? throw new ArgumentNullException(nameof(userHelper));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_permission = permission ?? throw new ArgumentNullException(nameof(permission));
tenantId = userHelper.GetTenantId();
_permission = permission;
}
[HttpGet("images/{projectId}")]
@ -98,7 +106,7 @@ namespace Marco.Pms.Services.Controllers
t.WorkItem.WorkArea != null &&
t.WorkItem.WorkArea.Floor != null &&
t.WorkItem.WorkArea.Floor.Building != null &&
t.WorkItem.WorkArea.Floor.Building.ProjectId != projectId &&
t.WorkItem.WorkArea.Floor.Building.ProjectId == projectId &&
t.TenantId == tenantId);
// Step 4: Extract filter values
@ -382,6 +390,150 @@ namespace Marco.Pms.Services.Controllers
return Ok(ApiResponse<object>.SuccessResponse(response, "Images for provided batchId fetched successfully", 200));
}
[HttpGet("filter/{projectId}")]
public async Task<IActionResult> GetFilterObjectAsync(Guid projectId, CancellationToken ct)
{
_logger.LogInfo("GetFilterObject started for ProjectId {ProjectId}", projectId); // start log [memory:1]
// Validate input early
if (projectId == Guid.Empty)
{
Log.Warning("GetFilterObject received empty ProjectId"); // input validation [memory:1]
return BadRequest(ApiResponse<object>.ErrorResponse("Invalid project id", 400));
}
// Load only what is needed for attachments; project scoping will be enforced downstream
// NOTE: Select only the columns required to reduce payload
var taskAttachments = await _context.TaskAttachments
.AsNoTracking()
.Select(ta => new { ta.ReferenceId, ta.DocumentId })
.ToListAsync(ct); // I/O bound work [memory:10]
if (taskAttachments.Count == 0)
{
Log.Information("No task attachments found for ProjectId {ProjectId}", projectId); // early exit [memory:1]
var emptyResponse = new
{
Buildings = Array.Empty<object>(),
Floors = Array.Empty<object>(),
WorkAreas = Array.Empty<object>(),
WorkCategories = Array.Empty<object>(),
Activities = Array.Empty<object>(),
UploadedBys = Array.Empty<object>(),
Services = Array.Empty<object>()
};
return Ok(ApiResponse<object>.SuccessResponse(emptyResponse, "No data found", 200));
}
// HashSets for O(1) membership tests and to dedupe upfront
var referenceIds = new HashSet<Guid>(taskAttachments.Select(x => x.ReferenceId));
var documentIds = new HashSet<Guid>(taskAttachments.Select(x => x.DocumentId));
_logger.LogDebug("Collected {ReferenceCount} referenceIds and {DocumentCount} documentIds", referenceIds.Count, documentIds.Count); // metrics [memory:1]
// Load comments for the references under this tenant
var comments = await _context.TaskComments
.AsNoTracking()
.Where(tc => referenceIds.Contains(tc.Id) && tc.TenantId == tenantId)
.Select(tc => new { tc.Id, tc.TaskAllocationId })
.ToListAsync(ct); // filtered selection [memory:10]
var taskIds = new HashSet<Guid>(comments.Select(c => c.TaskAllocationId));
_logger.LogDebug("Resolved {CommentCount} comments mapping to {TaskIdCount} taskIds", comments.Count, taskIds.Count); // observation [memory:1]
// IMPORTANT: Correct project filter should be == not !=
// Include graph tailored to fields needed in final projection (avoid over-inclusion)
// Avoid Task.Run for async EF (it doesnt add value and can harm thread pool)
var tasks = await _context.TaskAllocations
.AsNoTracking()
.Where(t =>
t.WorkItem != null &&
t.WorkItem.WorkArea != null &&
t.WorkItem.WorkArea.Floor != null &&
t.WorkItem.WorkArea.Floor.Building != null &&
t.WorkItem.WorkArea.Floor.Building.ProjectId == projectId && // fixed filter
(taskIds.Contains(t.Id) || referenceIds.Contains(t.Id)))
.Include(t => t.WorkItem!)
.ThenInclude(wi => wi.ActivityMaster!)
.ThenInclude(a => a.ActivityGroup!)
.ThenInclude(ag => ag.Service)
.Include(t => t.WorkItem!)
.ThenInclude(wi => wi.WorkArea!)
.ThenInclude(wa => wa.Floor!)
.ThenInclude(f => f.Building)
.Include(t => t.WorkItem!)
.ThenInclude(wi => wi.WorkCategoryMaster)
// Only select fields used later to reduce tracked object size in memory
.Select(t => new
{
TaskId = t.Id,
Building = new { Id = t.WorkItem!.WorkArea!.Floor!.Building!.Id, Name = t.WorkItem.WorkArea.Floor.Building.Name },
Floor = new { Id = t.WorkItem.WorkArea.Floor.Id, Name = t.WorkItem.WorkArea.Floor.FloorName },
WorkArea = new { Id = t.WorkItem.WorkArea.Id, Name = t.WorkItem.WorkArea.AreaName },
Activity = t.WorkItem.ActivityMaster == null ? null : new { Id = t.WorkItem.ActivityMaster.Id, Name = t.WorkItem.ActivityMaster.ActivityName },
WorkCategory = t.WorkItem.WorkCategoryMaster == null ? null : new { Id = t.WorkItem.WorkCategoryMaster.Id, Name = t.WorkItem.WorkCategoryMaster.Name },
Service = t.WorkItem.ActivityMaster!.ActivityGroup!.Service == null ? null : new { Id = t.WorkItem.ActivityMaster.ActivityGroup.Service.Id, Name = t.WorkItem.ActivityMaster.ActivityGroup.Service.Name }
})
.ToListAsync(ct); // optimized projection [memory:10]
_logger.LogDebug("Fetched {TaskCount} tasks after filtering and projection", tasks.Count); // metrics [memory:1]
// Documents query in parallel with tasks is okay; here weve already awaited tasks, but both can run together if needed.
// Only fetch uploader fields needed
var documents = await _context.Documents
.AsNoTracking()
.Where(d => documentIds.Contains(d.Id))
.Select(d => new
{
d.Id,
UploadedBy = d.UploadedBy == null ? null : new
{
d.UploadedBy.Id,
d.UploadedBy.FirstName,
d.UploadedBy.LastName
}
})
.ToListAsync(ct); // minimal shape [memory:10]
_logger.LogDebug("Fetched {DocumentCount} documents for UploadedBy resolution", documents.Count); // metrics [memory:1]
// Distinct projections via HashSet to avoid custom equality or anonymous Distinct pitfalls
static List<T> DistinctBy<T, TKey>(IEnumerable<T> source, Func<T, TKey> keySelector)
=> source.GroupBy(keySelector).Select(g => g.First()).ToList();
var buildings = DistinctBy(tasks.Select(t => t.Building), b => b.Id);
var floors = DistinctBy(tasks.Select(t => t.Floor), f => f.Id);
var workAreas = DistinctBy(tasks.Select(t => t.WorkArea), wa => wa.Id);
var activities = DistinctBy(tasks.Where(t => t.Activity != null).Select(t => t.Activity!), a => a.Id);
var workCategories = DistinctBy(tasks.Where(t => t.WorkCategory != null).Select(t => t.WorkCategory!), wc => wc.Id);
var services = DistinctBy(tasks.Where(t => t.Service != null).Select(t => t.Service!), s => s.Id);
var uploadedBys = DistinctBy(
documents.Where(d => d.UploadedBy != null)
.Select(d => new
{
Id = d.UploadedBy!.Id,
Name = string.Join(' ', new[] { d.UploadedBy!.FirstName, d.UploadedBy!.LastName }.Where(x => !string.IsNullOrWhiteSpace(x)))
}),
u => u.Id);
var response = new
{
Buildings = buildings,
Floors = floors,
WorkAreas = workAreas,
WorkCategories = workCategories,
Activities = activities,
UploadedBys = uploadedBys,
Services = services
};
_logger.LogInfo("GetFilterObject succeeded for ProjectId {ProjectId}. Buildings={Buildings}, Floors={Floors}, WorkAreas={WorkAreas}, Activities={Activities}, WorkCategories={WorkCategories}, Services={Services}, UploadedBys={UploadedBys}",
projectId, buildings.Count, floors.Count, workAreas.Count, activities.Count, workCategories.Count, services.Count, uploadedBys.Count); // success log [memory:1]
return Ok(ApiResponse<object>.SuccessResponse(response, "Filter object for image gallery fetched successfully", 200));
}
[HttpGet("{documentId}")]
public async Task<IActionResult> GetImage(Guid documentId)
{

File diff suppressed because it is too large Load Diff

View File

@ -319,12 +319,20 @@ namespace MarcoBMS.Services.Controllers
return StatusCode(response.StatusCode, response);
}
[HttpGet("get/task/team/{projectId}")]
public async Task<IActionResult> GetProjectTeamByServiceAndOrganization(Guid projectId, [FromQuery] Guid? serviceId, [FromQuery] Guid? organizationId)
{
Employee loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
var response = await _projectServices.GetProjectTeamByServiceAndOrganizationAsync(projectId, serviceId, organizationId, tenantId, loggedInEmployee);
return StatusCode(response.StatusCode, response);
}
#endregion
#region =================================================================== Project InfraStructure Get APIs ===================================================================
[HttpGet("infra-details/{projectId}")]
public async Task<IActionResult> GetInfraDetails(Guid projectId)
public async Task<IActionResult> GetInfraDetails(Guid projectId, [FromQuery] Guid? serviceId)
{
// --- Step 1: Input Validation ---
if (!ModelState.IsValid)
@ -336,7 +344,7 @@ namespace MarcoBMS.Services.Controllers
// --- Step 2: Prepare data without I/O ---
Employee loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
var response = await _projectServices.GetInfraDetailsAsync(projectId, tenantId, loggedInEmployee);
var response = await _projectServices.GetInfraDetailsAsync(projectId, serviceId, tenantId, loggedInEmployee);
return StatusCode(response.StatusCode, response);
}
@ -357,6 +365,7 @@ namespace MarcoBMS.Services.Controllers
var response = await _projectServices.GetWorkItemsAsync(workAreaId, serviceId, tenantId, loggedInEmployee);
return StatusCode(response.StatusCode, response);
}
[HttpGet("tasks-employee/{employeeId}")]
public async Task<IActionResult> GetTasksByEmployee(Guid employeeId, [FromQuery] DateTime? fromDate, DateTime? toDate)
{
@ -510,6 +519,7 @@ namespace MarcoBMS.Services.Controllers
return StatusCode(response.StatusCode, response);
}
[HttpPost("assign/service")]
public async Task<IActionResult> AssignServiceToProject([FromBody] AssignServiceDto model)
{
@ -550,6 +560,13 @@ namespace MarcoBMS.Services.Controllers
var response = await _projectServices.GetAssignedOrganizationsToProjectAsync(projectId, tenantId, loggedInEmployee);
return StatusCode(response.StatusCode, response);
}
[HttpGet("get/assigned/organization/dropdown/{projectId}")]
public async Task<IActionResult> GetAssignedOrganizationsToProjectForDropdownAsync(Guid projectId)
{
Employee loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
var response = await _projectServices.GetAssignedOrganizationsToProjectForDropdownAsync(projectId, tenantId, loggedInEmployee);
return StatusCode(response.StatusCode, response);
}
#endregion
}

View File

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

View File

@ -430,37 +430,14 @@ namespace MarcoBMS.Services.Controllers
}
[HttpGet("list")]
public async Task<IActionResult> GetTasksList([FromQuery] Guid projectId, [FromQuery] string? filter, [FromQuery] int pageNumber = 1, [FromQuery] int pageSize = 20,
[FromQuery] string? dateFrom = null, [FromQuery] string? dateTo = null)
public async Task<IActionResult> GetTasksList([FromQuery] Guid projectId, [FromQuery] string? filter, [FromQuery] Guid? serviceId,
[FromQuery] int pageNumber = 1, [FromQuery] int pageSize = 20)
{
_logger.LogInfo("GetTasksList called for projectId: {ProjectId}, dateFrom: {DateFrom}, dateTo: {DateTo}", projectId, dateFrom ?? "", dateTo ?? "");
_logger.LogInfo("GetTasksList called for projectId: {ProjectId}", projectId);
Guid tenantId = GetTenantId();
DateTime fromDate = new DateTime();
DateTime toDate = new DateTime();
// 1. Parse and validate dateFrom
if (dateFrom != null && !DateTime.TryParse(dateFrom, out fromDate))
{
_logger.LogWarning("Invalid starting date provided: {DateFrom}", dateFrom);
return BadRequest(ApiResponse<object>.ErrorResponse("Invalid starting date.", "Invalid starting date.", 400));
}
// 2. Parse and validate dateTo
if (dateTo != null && !DateTime.TryParse(dateTo, out toDate))
{
_logger.LogWarning("Invalid ending date provided: {DateTo}", dateTo);
return BadRequest(ApiResponse<object>.ErrorResponse("Invalid ending date.", "Invalid ending date.", 400));
}
// 3. Set default date range if not provided
fromDate = dateFrom == null ? DateTime.UtcNow.Date : fromDate;
toDate = dateTo == null ? fromDate.AddDays(1) : toDate;
_logger.LogInfo("Fetching task allocations between {FromDate} and {ToDate}", fromDate, toDate);
// 4. Get task allocations in the specified date range
// 1. Get task allocations in the specified date range
var taskAllocationQuery = _context.TaskAllocations
.Include(t => t.Employee)
.Include(t => t.ReportedBy)
@ -480,9 +457,7 @@ namespace MarcoBMS.Services.Controllers
t.WorkItem.WorkArea != null &&
t.WorkItem.WorkArea.Floor != null &&
t.WorkItem.WorkArea.Floor.Building != null &&
t.WorkItem.WorkArea.Floor.Building.ProjectId != projectId &&
t.AssignmentDate.Date >= fromDate.Date &&
t.AssignmentDate.Date <= toDate.Date &&
t.WorkItem.WorkArea.Floor.Building.ProjectId == projectId &&
t.TenantId == tenantId);
var taskFilter = TryDeserializeFilter(filter);
@ -510,10 +485,23 @@ namespace MarcoBMS.Services.Controllers
if (taskFilter.ServiceIds?.Any() ?? false)
{
taskAllocationQuery = taskAllocationQuery.Where(t => t.WorkItem != null &&
t.WorkItem.ActivityMaster != null &&
t.WorkItem.ActivityMaster.ActivityGroup != null &&
taskFilter.ServiceIds.Contains(t.WorkItem.ActivityMaster.ActivityGroup.ServiceId));
t.WorkItem.ActivityMaster != null &&
t.WorkItem.ActivityMaster.ActivityGroup != null &&
taskFilter.ServiceIds.Contains(t.WorkItem.ActivityMaster.ActivityGroup.ServiceId));
}
if (taskFilter.dateFrom.HasValue && taskFilter.dateTo.HasValue)
{
taskAllocationQuery = taskAllocationQuery.Where(t => t.AssignmentDate.Date >= taskFilter.dateFrom.Value.Date &&
t.AssignmentDate.Date <= taskFilter.dateTo.Value.Date);
}
}
if (serviceId.HasValue)
{
taskAllocationQuery = taskAllocationQuery.Where(t => t.WorkItem != null &&
t.WorkItem.ActivityMaster != null &&
t.WorkItem.ActivityMaster.ActivityGroup != null &&
t.WorkItem.ActivityMaster.ActivityGroup.ServiceId == serviceId);
}
@ -528,14 +516,14 @@ namespace MarcoBMS.Services.Controllers
var taskIds = taskAllocations.Select(t => t.Id).ToList();
// 5. Load team members
// 2. Load team members
_logger.LogInfo("Loading task members and related employee data.");
var teamMembers = await _context.TaskMembers
.Include(t => t.Employee)
.Where(t => taskIds.Contains(t.TaskAllocationId))
.ToListAsync();
// 6. Load task comments
// 3. Load task comments
_logger.LogInfo("Fetching comments and attachments.");
var allComments = await _context.TaskComments
.Include(c => c.Employee)
@ -543,14 +531,14 @@ namespace MarcoBMS.Services.Controllers
.ToListAsync();
var commentIds = allComments.Select(c => c.Id).ToList();
// 7. Load all attachments (task and comment)
// 4. Load all attachments (task and comment)
var attachments = await _context.TaskAttachments
.Where(t => taskIds.Contains(t.ReferenceId) || commentIds.Contains(t.ReferenceId))
.ToListAsync();
var documentIds = attachments.Select(t => t.DocumentId).ToList();
// 8. Load actual documents from attachment references
// 5. Load actual documents from attachment references
var documents = await _context.Documents
.Where(d => documentIds.Contains(d.Id))
.ToListAsync();
@ -764,6 +752,97 @@ namespace MarcoBMS.Services.Controllers
return Ok(ApiResponse<object>.SuccessResponse(taskVM, "Success", 200));
}
[HttpGet("filter/{projectId}")]
public async Task<IActionResult> GetTaskFilterObject(Guid projectId)
{
// Get the current tenant from claims/context
Guid tenantId = GetTenantId();
// Log API invocation with the project and tenant for traceability
_logger.LogInfo("Fetching filter objects for ProjectId={ProjectId}, TenantId={TenantId}", projectId, tenantId);
try
{
// AsNoTracking for improved performance—no intention to update these records
// Only fetch & project properties actually required (DTO projection)
var tasks = await _context.TaskAllocations
.Include(t => t.WorkItem)
.ThenInclude(wi => wi!.WorkArea)
.ThenInclude(wa => wa!.Floor)
.ThenInclude(f => f!.Building)
.Include(t => t.WorkItem)
.ThenInclude(wi => wi!.ActivityMaster)
.ThenInclude(a => a!.ActivityGroup)
.ThenInclude(ag => ag!.Service)
.Where(t => t.WorkItem != null &&
t.WorkItem.WorkArea != null &&
t.WorkItem.WorkArea.Floor != null &&
t.WorkItem.WorkArea.Floor.Building != null &&
t.WorkItem.WorkArea.Floor.Building.ProjectId == projectId &&
t.TenantId == tenantId).ToListAsync();
// Distinct by Id (since projection doesn't guarantee uniqueness across different allocations)
var buildings = tasks.Where(t => t.WorkItem != null && t.WorkItem.WorkArea != null && t.WorkItem.WorkArea.Floor != null && t.WorkItem.WorkArea.Floor.Building != null)
.Select(t => t.WorkItem!.WorkArea!.Floor!.Building!)
.Select(b => new
{
Id = b.Id,
Name = b.Name
}).Distinct().ToList();
var floors = tasks.Where(t => t.WorkItem != null && t.WorkItem.WorkArea != null && t.WorkItem.WorkArea.Floor != null)
.Select(t => t.WorkItem!.WorkArea!.Floor!)
.Select(f => new
{
Id = f.Id,
Name = f.FloorName,
BuildingId = f.BuildingId
}).Distinct().ToList();
var activities = tasks.Where(t => t.WorkItem != null &&
t.WorkItem.ActivityMaster != null &&
t.WorkItem.ActivityMaster.ActivityGroup != null &&
t.WorkItem.ActivityMaster.ActivityGroup.Service != null)
.Select(t => t.WorkItem!.ActivityMaster!)
.Select(a => new
{
Id = a.Id,
Name = a.ActivityName
}).Distinct().ToList();
var services = tasks.Where(t => t.WorkItem != null &&
t.WorkItem.ActivityMaster != null &&
t.WorkItem.ActivityMaster.ActivityGroup != null &&
t.WorkItem.ActivityMaster.ActivityGroup.Service != null)
.Select(t => t.WorkItem!.ActivityMaster!.ActivityGroup!.Service!)
.Select(s => new
{
Id = s.Id,
Name = s.Name
}).Distinct().ToList();
var response = new
{
Buildings = buildings,
Floors = floors,
Activities = activities,
Services = services
};
_logger.LogInfo("Successfully fetched filter objects for ProjectId={ProjectId}, TenantId={TenantId}", projectId, tenantId);
// Use DTO in API response for clarity and easier frontend usage
return Ok(ApiResponse<object>.SuccessResponse(response, "Filter object for task fetched successfully", 200));
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to fetch filter objects for ProjectId={ProjectId}, TenantId={TenantId}", projectId, tenantId);
// Return a standard error result
return StatusCode(500, ApiResponse<object>.ErrorResponse("Failed to fetch filter object.", 500));
}
}
/// <summary>
/// Approves a reported task after validation, updates status, and stores attachments/comments.
/// </summary>

View File

@ -6,6 +6,7 @@ using Marco.Pms.Model.Employees;
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;
using Marco.Pms.Model.Roles;
using Marco.Pms.Model.TenantModels;
@ -473,6 +474,27 @@ namespace Marco.Pms.Services.Controllers
await using var transaction = await _context.Database.BeginTransactionAsync();
try
{
// Get last SPRID and increment for new organization
var lastOrganization = await _context.Organizations.OrderByDescending(sp => sp.SPRID).FirstOrDefaultAsync();
double lastSPRID = lastOrganization?.SPRID ?? 5400;
// Map DTO to entity and set defaults
Organization organization = new Organization
{
Name = model.OrganizationName,
Email = model.Email,
ContactPerson = $"{model.FirstName} {model.LastName}",
Address = model.BillingAddress,
ContactNumber = model.ContactNumber,
SPRID = lastSPRID + 1,
logoImage = model.logoImage,
CreatedAt = DateTime.UtcNow,
CreatedById = loggedInEmployee.Id,
IsActive = true
};
_context.Organizations.Add(organization);
// Create the primary Tenant entity
var tenant = _mapper.Map<Tenant>(model);
@ -480,6 +502,7 @@ namespace Marco.Pms.Services.Controllers
tenant.TenantStatusId = tenantActiveStatus;
tenant.CreatedById = loggedInEmployee.Id;
tenant.IsSuperTenant = false;
tenant.OrganizationId = organization.Id;
_context.Tenants.Add(tenant);
@ -526,7 +549,11 @@ namespace Marco.Pms.Services.Controllers
ApplicationUserId = applicationUser.Id,
JobRole = adminJobRole, // Link to the newly created role
CurrentAddress = model.BillingAddress,
TenantId = tenant.Id
IsActive = true,
IsSystem = false,
IsPrimary = true,
OrganizationId = organization.Id,
HasApplicationAccess = true
};
_context.Employees.Add(employeeUser);
@ -559,6 +586,21 @@ namespace Marco.Pms.Services.Controllers
{
ApplicationRoleId = applicationRole.Id,
FeaturePermissionId = PermissionsMaster.ViewMasters
},
new RolePermissionMappings
{
ApplicationRoleId = applicationRole.Id,
FeaturePermissionId = PermissionsMaster.ViewOrganization
},
new RolePermissionMappings
{
ApplicationRoleId = applicationRole.Id,
FeaturePermissionId = PermissionsMaster.AddOrganization
},
new RolePermissionMappings
{
ApplicationRoleId = applicationRole.Id,
FeaturePermissionId = PermissionsMaster.EditOrganization
}
};
_context.RolePermissionMappings.AddRange(rolePermissionMappigs);
@ -579,6 +621,8 @@ namespace Marco.Pms.Services.Controllers
ProjectAddress = model.BillingAddress,
StartDate = model.OnBoardingDate,
EndDate = DateTime.MaxValue,
PromoterId = organization.Id,
PMCId = organization.Id,
ContactPerson = tenant.ContactName,
TenantId = tenant.Id
};
@ -595,6 +639,18 @@ namespace Marco.Pms.Services.Controllers
};
_context.ProjectAllocations.Add(projectAllocation);
// Map organization services
if (model.ServiceIds?.Any() ?? false)
{
var serviceOrgMappings = model.ServiceIds.Select(s => new OrgServiceMapping
{
ServiceId = s,
OrganizationId = organization.Id
}).ToList();
_context.OrgServiceMappings.AddRange(serviceOrgMappings);
}
// All entities are now added to the context. Save them all in a single database operation.
await _context.SaveChangesAsync();
@ -957,10 +1013,6 @@ namespace Marco.Pms.Services.Controllers
try
{
_ = Task.Run(async () =>
{
await ClearPermissionForTenant();
});
var features = await _featureDetailsHelper.GetFeatureDetails(subscriptionPlan.FeaturesId);
if (features == null)
{
@ -1013,7 +1065,7 @@ namespace Marco.Pms.Services.Controllers
// Get root employee and role for this tenant
var rootEmployee = await _context.Employees
.Include(e => e.ApplicationUser)
.FirstOrDefaultAsync(e => e.ApplicationUser != null && (e.ApplicationUser.IsRootUser ?? false) && e.TenantId == model.TenantId);
.FirstOrDefaultAsync(e => e.ApplicationUser != null && (e.ApplicationUser.IsRootUser ?? false) && e.OrganizationId == tenant.OrganizationId);
if (rootEmployee == null)
{
@ -1071,6 +1123,9 @@ namespace Marco.Pms.Services.Controllers
_logger.LogInfo("Removed {Count} role permission mappings for role {RoleId}", deleteMappings.Count, roleId);
}
var _cache = scope.ServiceProvider.GetRequiredService<CacheUpdateHelper>();
await _cache.ClearAllEmployeesFromCacheByTenantId(tenant.Id);
var _masteData = scope.ServiceProvider.GetRequiredService<MasterDataService>();
if (features.Modules?.ProjectManagement?.Enabled ?? false)
@ -1269,10 +1324,6 @@ namespace Marco.Pms.Services.Controllers
_logger.LogInfo("Subscription plan changed: Tenant={TenantId}, NewPlan={PlanId}",
model.TenantId, model.PlanId);
_ = Task.Run(async () =>
{
await ClearPermissionForTenant();
});
// 8. Update tenant permissions based on subscription features.
var features = await _featureDetailsHelper.GetFeatureDetails(subscriptionPlan.FeaturesId);
@ -1307,7 +1358,7 @@ namespace Marco.Pms.Services.Controllers
// 8c. Find root employee & role for this tenant.
var rootEmployee = await context.Employees
.Include(e => e.ApplicationUser)
.FirstOrDefaultAsync(e => e.ApplicationUser != null && (e.ApplicationUser.IsRootUser ?? false) && e.TenantId == model.TenantId);
.FirstOrDefaultAsync(e => e.ApplicationUser != null && (e.ApplicationUser.IsRootUser ?? false) && e.OrganizationId == tenant.OrganizationId);
if (rootEmployee == null)
{
@ -1318,7 +1369,8 @@ namespace Marco.Pms.Services.Controllers
var rootRoleId = await context.EmployeeRoleMappings
.AsNoTracking()
.Where(er => er.EmployeeId == rootEmployee.Id && er.TenantId == model.TenantId)
.Include(er => er.Role)
.Where(er => er.EmployeeId == rootEmployee.Id && er.TenantId == model.TenantId && er.Role != null && er.Role.Role == "Super User")
.Select(er => er.RoleId)
.FirstOrDefaultAsync();
@ -1383,6 +1435,9 @@ namespace Marco.Pms.Services.Controllers
_logger.LogInfo("Permissions revoked: {Count} for Role={RoleId}", mappingsToRemove.Count, rootRoleId);
}
var _cache = scope.ServiceProvider.GetRequiredService<CacheUpdateHelper>();
await _cache.ClearAllEmployeesFromCacheByTenantId(tenant.Id);
var _masteData = scope.ServiceProvider.GetRequiredService<MasterDataService>();
if (features.Modules?.ProjectManagement?.Enabled ?? false)
@ -1767,19 +1822,6 @@ namespace Marco.Pms.Services.Controllers
return ApiResponse<SubscriptionPlanVM>.SuccessResponse(VM, "Success", 200);
}
private async Task ClearPermissionForTenant()
{
await using var _context = await _dbContextFactory.CreateDbContextAsync();
using var scope = _serviceScopeFactory.CreateScope();
var _cache = scope.ServiceProvider.GetRequiredService<CacheUpdateHelper>();
var _cacheLogger = scope.ServiceProvider.GetRequiredService<ILoggingService>();
var employeeIds = await _context.Employees.Where(e => e.TenantId == tenantId).Select(e => e.Id).ToListAsync();
await _cache.ClearAllEmployeesFromCacheByEmployeeIds(employeeIds, tenantId);
_cacheLogger.LogInfo("{EmployeeCount} number of employee deleted", employeeIds.Count);
}
#endregion
}
}

View File

@ -10,6 +10,7 @@ using Marco.Pms.Model.MongoDBModels.Expenses;
using Marco.Pms.Model.MongoDBModels.Masters;
using Marco.Pms.Model.MongoDBModels.Project;
using Marco.Pms.Model.MongoDBModels.Utility;
using Marco.Pms.Model.OrganizationModel;
using Marco.Pms.Model.Projects;
using Marco.Pms.Model.Utilities;
using MarcoBMS.Services.Service;
@ -69,6 +70,45 @@ namespace Marco.Pms.Services.Helpers
.Where(s => s.Id == project.ProjectStatusId)
.Select(s => new { s.Id, s.Status }) // Projection
.FirstOrDefaultAsync();
});
var promotorTask = Task.Run(async () =>
{
using var context = _dbContextFactory.CreateDbContext();
return await context.Organizations
.AsNoTracking()
.Where(o => o.Id == project.PromoterId)
.Select(o => new OrganizationMongoDB
{
Id = o.Id.ToString(),
Name = o.Name,
ContactPerson = o.ContactPerson,
Email = o.Email,
Address = o.Address,
ContactNumber = o.ContactNumber,
SPRID = o.SPRID
}) // Projection
.FirstOrDefaultAsync();
});
var pmcTask = Task.Run(async () =>
{
using var context = _dbContextFactory.CreateDbContext();
return await context.Organizations
.AsNoTracking()
.Where(o => o.Id == project.PMCId)
.Select(o => new OrganizationMongoDB
{
Id = o.Id.ToString(),
Name = o.Name,
ContactPerson = o.ContactPerson,
Email = o.Email,
Address = o.Address,
ContactNumber = o.ContactNumber,
SPRID = o.SPRID
}) // Projection
.FirstOrDefaultAsync();
});
var teamSizeTask = Task.Run(async () =>
@ -120,12 +160,15 @@ namespace Marco.Pms.Services.Helpers
});
// Wait for all parallel database operations to complete.
await Task.WhenAll(statusTask, teamSizeTask, infrastructureTask);
await Task.WhenAll(statusTask, teamSizeTask, infrastructureTask, promotorTask, pmcTask);
// Get the results from the completed tasks.
var status = await statusTask;
var teamSize = await teamSizeTask;
var (allBuildings, allFloors, allWorkAreas, workSummariesByWorkAreaId) = await infrastructureTask;
var status = statusTask.Result;
var teamSize = teamSizeTask.Result;
var (allBuildings, allFloors, allWorkAreas, workSummariesByWorkAreaId) = infrastructureTask.Result;
var promotor = promotorTask.Result;
var pmc = pmcTask.Result;
// --- Step 2: Process the fetched data and build the MongoDB model ---
@ -216,6 +259,8 @@ namespace Marco.Pms.Services.Helpers
projectDetails.Buildings = buildingMongoList;
projectDetails.PlannedWork = totalPlannedWork;
projectDetails.CompletedWork = totalCompletedWork;
projectDetails.Promoter = promotor;
projectDetails.PMC = pmc;
try
{
@ -234,6 +279,8 @@ namespace Marco.Pms.Services.Helpers
return; // Nothing to do
}
var projectStatusIds = projects.Select(p => p.ProjectStatusId).Distinct().ToList();
var promotorIds = projects.Select(p => p.PromoterId).Distinct().ToList();
var pmcsIds = projects.Select(p => p.PMCId).Distinct().ToList();
// --- Step 1: Fetch all required data in maximum parallel ---
// Each task uses its own DbContext and selects only the required columns (projection).
@ -248,6 +295,25 @@ namespace Marco.Pms.Services.Helpers
.ToDictionaryAsync(s => s.Id);
});
var organizationTask = Task.Run(async () =>
{
using var context = _dbContextFactory.CreateDbContext();
return await context.Organizations
.AsNoTracking()
.Where(o => promotorIds.Contains(o.Id) || pmcsIds.Contains(o.Id))
.Select(o => new OrganizationMongoDB
{
Id = o.Id.ToString(),
Name = o.Name,
ContactPerson = o.ContactPerson,
Email = o.Email,
Address = o.Address,
ContactNumber = o.ContactNumber,
SPRID = o.SPRID
}) // Projection
.ToListAsync();
});
var teamSizeTask = Task.Run(async () =>
{
using var context = _dbContextFactory.CreateDbContext();
@ -322,20 +388,22 @@ namespace Marco.Pms.Services.Helpers
});
// Await the remaining parallel tasks.
await Task.WhenAll(statusTask, teamSizeTask, workAreasTask, workSummaryTask);
await Task.WhenAll(statusTask, teamSizeTask, workAreasTask, workSummaryTask, organizationTask);
// --- Step 2: Process the fetched data and build the MongoDB models ---
var allStatuses = await statusTask;
var teamSizesByProjectId = await teamSizeTask;
var allWorkAreas = await workAreasTask;
var workSummariesByWorkAreaId = await workSummaryTask;
var allStatuses = statusTask.Result;
var teamSizesByProjectId = teamSizeTask.Result;
var allWorkAreas = workAreasTask.Result;
var workSummariesByWorkAreaId = workSummaryTask.Result;
var organizations = organizationTask.Result;
// Create fast in-memory lookups for hierarchical data
var buildingsByProjectId = allBuildings.ToLookup(b => b.ProjectId);
var floorsByBuildingId = allFloors.ToLookup(f => f.BuildingId);
var workAreasByFloorId = allWorkAreas.ToLookup(wa => wa.FloorId);
var projectDetailsList = new List<ProjectMongoDB>(projects.Count);
foreach (var project in projects)
{
@ -427,6 +495,8 @@ namespace Marco.Pms.Services.Helpers
projectDetails.Buildings = buildingMongoList;
projectDetails.PlannedWork = totalPlannedWork;
projectDetails.CompletedWork = totalCompletedWork;
projectDetails.Promoter = organizations.FirstOrDefault(o => o.Id == project.PromoterId.ToString());
projectDetails.PMC = organizations.FirstOrDefault(o => o.Id == project.PMCId.ToString());
projectDetailsList.Add(projectDetails);
}
@ -443,11 +513,32 @@ namespace Marco.Pms.Services.Helpers
}
public async Task<bool> UpdateProjectDetailsOnly(Project project)
{
StatusMaster projectStatus = await _context.StatusMasters
var projectStatusTask = Task.Run(async () =>
{
await using var context = await _dbContextFactory.CreateDbContextAsync();
return await context.StatusMasters
.FirstOrDefaultAsync(s => s.Id == project.ProjectStatusId) ?? new StatusMaster();
});
var promotorTask = Task.Run(async () =>
{
await using var context = await _dbContextFactory.CreateDbContextAsync();
return await context.Organizations.FirstOrDefaultAsync(o => o.Id == project.PromoterId) ?? new Organization();
});
var pmcTask = Task.Run(async () =>
{
await using var context = await _dbContextFactory.CreateDbContextAsync();
return await context.Organizations.FirstOrDefaultAsync(o => o.Id == project.PMCId) ?? new Organization();
});
await Task.WhenAll(projectStatusTask, promotorTask, pmcTask);
var projectStatus = projectStatusTask.Result;
var promotor = promotorTask.Result;
var pmc = pmcTask.Result;
try
{
bool response = await _projectCache.UpdateProjectDetailsOnlyToCache(project, projectStatus);
bool response = await _projectCache.UpdateProjectDetailsOnlyToCache(project, projectStatus, promotor, pmc);
return response;
}
catch (Exception ex)
@ -865,6 +956,28 @@ namespace Marco.Pms.Services.Helpers
_logger.LogError(ex, "Error occured while deleting all employees from Cache");
}
}
public async Task ClearAllEmployeesFromCacheByOnlyEmployeeId(Guid employeeId)
{
try
{
var response = await _employeeCache.ClearAllEmployeesFromCacheByOnlyEmployeeId(employeeId);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error occured while deleting all employees from Cache");
}
}
public async Task ClearAllEmployeesFromCacheByTenantId(Guid tenantId)
{
try
{
var response = await _employeeCache.ClearAllEmployeesFromCacheByTenantId(tenantId);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error occured while deleting all employees from Cache");
}
}
public async Task ClearAllEmployees()
{
try

View File

@ -1,4 +1,5 @@

using AutoMapper;
using Marco.Pms.DataAccess.Data;
using Marco.Pms.Model.Employees;
using Marco.Pms.Model.Mapper;
@ -13,10 +14,12 @@ namespace MarcoBMS.Services.Helpers
{
private readonly ApplicationDbContext _context;
private readonly ILoggingService _logger;
public EmployeeHelper(ApplicationDbContext context, ILoggingService logger)
private readonly IMapper _mapper;
public EmployeeHelper(ApplicationDbContext context, ILoggingService logger, IMapper mapper)
{
_context = context;
_logger = logger;
_mapper = mapper;
}
public async Task<Employee> GetEmployeeByID(Guid EmployeeID)
{
@ -72,38 +75,36 @@ namespace MarcoBMS.Services.Helpers
}
}
public async Task<List<EmployeeVM>> GetEmployeeByProjectId(Guid tenantId, Guid? projectId, bool ShowInActive)
public async Task<List<EmployeeVM>> GetEmployeeByProjectId(Guid organizationId, Guid tenantId, Guid? projectId, bool ShowInActive)
{
try
{
List<EmployeeVM> result = new List<EmployeeVM>();
List<Employee> employees = new List<Employee>();
if (projectId.HasValue)
{
var employeeIds = await _context.ProjectAllocations
.Where(pa => projectId == pa.ProjectId && pa.IsActive && pa.TenantId == tenantId)
.Select(pa => pa.EmployeeId)
.Distinct()
.ToListAsync();
var employees = await _context.Employees
.Include(fp => fp.JobRole)
.Where(e => employeeIds.Contains(e.Id) && e.JobRole != null && e.IsActive && e.TenantId == tenantId)
employees = await _context.ProjectAllocations
.Include(pa => pa.Employee)
.ThenInclude(e => e!.JobRole)
.Where(pa => projectId == pa.ProjectId && pa.IsActive && pa.TenantId == tenantId && pa.Employee != null && pa.Employee.IsActive)
.Select(pa => pa.Employee!)
.Distinct()
.ToListAsync();
result = employees.Select(e => e.ToEmployeeVMFromEmployee()).ToList();
}
else if (ShowInActive)
{
result = await _context.Employees.Where(c => c.TenantId == tenantId && c.IsActive == false).Include(fp => fp.JobRole)
.Select(c => c.ToEmployeeVMFromEmployee()).ToListAsync();
employees = await _context.Employees
.Include(fp => fp.JobRole)
.Where(c => c.OrganizationId == organizationId && c.IsActive == false)
.ToListAsync();
}
else
{
result = await _context.Employees.Where(c => c.TenantId == tenantId && c.IsActive == true).Include(fp => fp.JobRole)
.Select(c => c.ToEmployeeVMFromEmployee()).ToListAsync();
employees = await _context.Employees
.Include(fp => fp.JobRole)
.Where(c => c.OrganizationId == organizationId && c.IsActive == true)
.ToListAsync();
}
var result = employees.Select(e => _mapper.Map<EmployeeVM>(e)).Distinct().ToList();
return result;
}
catch (Exception ex)

View File

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

View File

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

View File

@ -26,12 +26,17 @@ namespace MarcoBMS.Services.Helpers
var tenant = _httpContextAccessor.HttpContext?.User.FindFirst("TenantId")?.Value;
return (tenant != null ? Guid.Parse(tenant) : Guid.Empty);
}
public Guid GetCurrentOrganizationId()
{
var tenant = _httpContextAccessor.HttpContext?.User.FindFirst("OrganizationId")?.Value;
return (tenant != null ? Guid.Parse(tenant) : Guid.Empty);
}
public async Task<Tenant?> GetCurrentTenant()
{
var tenantId = _httpContextAccessor.HttpContext?.User.FindFirst("TenantId")?.Value;
if (tenantId != null)
{
return await _context.Tenants.FirstOrDefaultAsync(t => t.Id == Guid.Parse(tenantId));
return await _context.Tenants.AsNoTracking().FirstOrDefaultAsync(t => t.Id == Guid.Parse(tenantId));
}
return null;
}
@ -49,7 +54,7 @@ namespace MarcoBMS.Services.Helpers
{
var user = await GetCurrentUserAsync();
if (user == null) return new Employee { };
var Employee = await _context.Employees.Include(e => e.JobRole).FirstOrDefaultAsync(e => e.ApplicationUserId == user.Id && e.IsActive);
var Employee = await _context.Employees.AsNoTracking().Include(a => a.ApplicationUser).Include(e => e.JobRole).FirstOrDefaultAsync(e => e.ApplicationUserId == user.Id && e.IsActive);
return Employee ?? new Employee { };
}

View File

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

View File

@ -6,6 +6,7 @@ using Marco.Pms.Model.Dtos.Activities;
using Marco.Pms.Model.Dtos.AppMenu;
using Marco.Pms.Model.Dtos.Directory;
using Marco.Pms.Model.Dtos.DocumentManager;
using Marco.Pms.Model.Dtos.Employees;
using Marco.Pms.Model.Dtos.Expenses;
using Marco.Pms.Model.Dtos.Master;
using Marco.Pms.Model.Dtos.Organization;
@ -46,6 +47,13 @@ namespace Marco.Pms.Services.MappingProfiles
CreateMap<CreateOrganizationDto, Organization>();
CreateMap<Organization, OrganizationVM>();
CreateMap<Organization, BasicOrganizationVm>();
CreateMap<Organization, OrganizationDetailsVM>();
CreateMap<OrganizationMongoDB, BasicOrganizationVm>()
.ForMember(
dest => dest.Id,
// Explicitly and safely convert string Id to Guid Id
opt => opt.MapFrom(src => new Guid(src.Id))
);
#endregion
@ -120,6 +128,7 @@ namespace Marco.Pms.Services.MappingProfiles
CreateMap<Project, ProjectListVM>();
CreateMap<Project, ProjectDto>();
CreateMap<ProjectMongoDB, ProjectListVM>();
CreateMap<ProjectServiceMapping, ProjectServiceMappingVM>();
CreateMap<ProjectMongoDB, ProjectVM>()
.ForMember(
dest => dest.Id,
@ -176,7 +185,13 @@ namespace Marco.Pms.Services.MappingProfiles
#region ======================================================= Employee =======================================================
CreateMap<Employee, EmployeeVM>();
CreateMap<Employee, EmployeeVM>()
.ForMember(
dest => dest.JobRole,
opt => opt.MapFrom(src => src.JobRole != null ? src.JobRole.Name : ""));
CreateMap<CreateUserDto, Employee>();
CreateMap<MobileUserManageDto, Employee>();
CreateMap<Employee, BasicEmployeeVM>()
.ForMember(
dest => dest.JobRoleName,
@ -364,6 +379,27 @@ namespace Marco.Pms.Services.MappingProfiles
#endregion
#region ======================================================= Contact Category Master =======================================================
CreateMap<CreateContactCategoryDto, ContactCategoryMaster>();
CreateMap<UpdateContactCategoryDto, ContactCategoryMaster>();
CreateMap<ContactCategoryMaster, ContactCategoryVM>();
#endregion
#region ======================================================= Contact Tag Master =======================================================
CreateMap<CreateContactTagDto, ContactTagMaster>();
CreateMap<UpdateContactTagDto, ContactTagMaster>();
CreateMap<ContactTagMaster, ContactTagVM>();
#endregion
#region ======================================================= Expenses Status Master =======================================================
#endregion
#region ======================================================= Expenses Status Master =======================================================
#endregion
#region ======================================================= Expenses Status Master =======================================================
#endregion
#region ======================================================= Expenses Status Master =======================================================
#endregion
#region ======================================================= Expenses Status Master =======================================================
#endregion
#endregion
#region ======================================================= Document =======================================================

View File

@ -956,10 +956,14 @@ namespace Marco.Pms.Services.Service
try
{
var contact = _mapper.Map<Contact>(createContact);
if (string.IsNullOrWhiteSpace(createContact.Name))
if (string.IsNullOrWhiteSpace(createContact.Description))
{
contact.Description = string.Empty;
}
if (string.IsNullOrWhiteSpace(createContact.Designation))
{
contact.Designation = string.Empty;
}
contact.CreatedAt = DateTime.UtcNow;
contact.CreatedById = loggedInEmployeeId;
contact.TenantId = tenantId;
@ -2031,9 +2035,11 @@ namespace Marco.Pms.Services.Service
try
{
var bucketIds = await _context.ContactBucketMappings.Where(cb => cb.ContactId == noteDto.ContactId).Select(cb => cb.BucketId).ToListAsync();
var hasContactAccess = await _context.EmployeeBucketMappings.AnyAsync(eb => bucketIds.Contains(eb.BucketId) && eb.EmployeeId == loggedInEmployee.Id);
if (!hasContactAccess)
var (hasAdminPermission, hasManagerPermission, hasUserPermission) = await CheckPermissionsAsync(loggedInEmployee.Id);
var bucketIds = await _context.ContactBucketMappings.AsNoTracking().Where(cb => cb.ContactId == noteDto.ContactId).Select(cb => cb.BucketId).ToListAsync();
var hasContactAccess = await _context.EmployeeBucketMappings.AsNoTracking().AnyAsync(eb => bucketIds.Contains(eb.BucketId) && eb.EmployeeId == loggedInEmployee.Id);
if (!hasAdminPermission && !hasContactAccess)
{
_logger.LogWarning("Employee {EmployeeId} does not have permission to delete contact {ContactId}",
loggedInEmployee.Id, noteDto.ContactId);
@ -2265,9 +2271,11 @@ namespace Marco.Pms.Services.Service
return ApiResponse<object>.ErrorResponse("Note not found", "Note not found", 404);
}
var bucketIds = await _context.ContactBucketMappings.Where(cb => cb.ContactId == note.ContactId).Select(cb => cb.BucketId).ToListAsync();
var hasContactAccess = await _context.EmployeeBucketMappings.AnyAsync(eb => bucketIds.Contains(eb.BucketId) && eb.EmployeeId == loggedInEmployee.Id);
if (hasContactAccess)
var (hasAdminPermission, hasManagerPermission, hasUserPermission) = await CheckPermissionsAsync(loggedInEmployee.Id);
var bucketIds = await _context.ContactBucketMappings.AsNoTracking().Where(cb => cb.ContactId == note.ContactId).Select(cb => cb.BucketId).ToListAsync();
var hasContactAccess = await _context.EmployeeBucketMappings.AsNoTracking().AnyAsync(eb => bucketIds.Contains(eb.BucketId) && eb.EmployeeId == loggedInEmployee.Id);
if (!hasAdminPermission && !hasContactAccess)
{
_logger.LogWarning("Employee {EmployeeId} does not have permission to delete contact {ContactId}",
loggedInEmployee.Id, note.ContactId);
@ -2611,6 +2619,7 @@ namespace Marco.Pms.Services.Service
var bucketTask = bucketContext.Buckets
.Include(b => b.CreatedBy)
.ThenInclude(e => e!.JobRole)
.AsNoTracking()
.FirstOrDefaultAsync(b => b.Id == bucketDto.Id && b.TenantId == tenantId);
// Await all parallel tasks

View File

@ -23,6 +23,7 @@ using MarcoBMS.Services.Service;
using Microsoft.CodeAnalysis;
using Microsoft.EntityFrameworkCore;
using System.Text.Json;
using System.Text.RegularExpressions;
using Document = Marco.Pms.Model.DocumentManager.Document;
namespace Marco.Pms.Services.Service
@ -119,8 +120,12 @@ namespace Marco.Pms.Services.Service
// 2. --- Deserialize Filter and Apply ---
ExpensesFilter? expenseFilter = TryDeserializeFilter(filter);
var (totalPages, totalCount, cacheList) = await _cache.GetExpenseListAsync(tenantId, loggedInEmployeeId, hasViewAllPermissionTask.Result, hasViewSelfPermissionTask.Result,
pageNumber, pageSize, expenseFilter, searchString);
//var (totalPages, totalCount, cacheList) = await _cache.GetExpenseListAsync(tenantId, loggedInEmployeeId, hasViewAllPermissionTask.Result, hasViewSelfPermissionTask.Result,
// pageNumber, pageSize, expenseFilter, searchString);
List<ExpenseDetailsMongoDB>? cacheList = null;
var totalPages = 0;
var totalCount = 0;
// 3. --- Build Base Query and Apply Permissions ---
// Start with a base IQueryable. Filters will be chained onto this.
@ -268,7 +273,8 @@ namespace Marco.Pms.Services.Service
{
try
{
var expenseDetails = await _cache.GetExpenseDetailsById(id, tenantId);
//ExpenseDetailsMongoDB? expenseDetails = await _cache.GetExpenseDetailsById(id, tenantId);
ExpenseDetailsMongoDB? expenseDetails = null;
if (expenseDetails == null)
{
var expense = await _context.Expenses.AsNoTracking().FirstOrDefaultAsync(e => e.Id == id && e.TenantId == tenantId);
@ -313,6 +319,13 @@ namespace Marco.Pms.Services.Service
{
status.PermissionIds = permissionStatusMappings.Where(ps => ps.StatusId == status.Id).Select(ps => ps.PermissionIds).FirstOrDefault();
}
int index = vm.NextStatus.FindIndex(ns => ns.DisplayName == "Reject");
if (index > -1)
{
var item = vm.NextStatus[index];
vm.NextStatus.RemoveAt(index);
vm.NextStatus.Insert(0, item);
}
}
vm.ExpensesReimburse = _mapper.Map<ExpensesReimburseVM>(expensesReimburse);
@ -482,6 +495,15 @@ namespace Marco.Pms.Services.Service
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
return await dbContext.PaymentModeMatser.AsNoTracking().FirstOrDefaultAsync(pm => pm.Id == dto.PaymentModeId);
});
var expenseUIdTask = Task.Run(async () =>
{
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
var result = await dbContext.Expenses
.Where(e => !string.IsNullOrWhiteSpace(e.ExpenseUId)).ToListAsync();
return result
.Select(e => ExtractNumber(e.ExpenseUId))
.OrderByDescending(id => id).FirstOrDefault();
});
var statusMappingTask = Task.Run(async () =>
{
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
@ -501,10 +523,7 @@ namespace Marco.Pms.Services.Service
// Await all prerequisite checks at once.
await Task.WhenAll(
hasUploadPermissionTask, hasProjectPermissionTask,
projectTask, expenseTypeTask, paymentModeTask, statusMappingTask, paidByTask
);
await Task.WhenAll(hasUploadPermissionTask, hasProjectPermissionTask, projectTask, expenseTypeTask, paymentModeTask, statusMappingTask, paidByTask, expenseUIdTask);
// 2. Aggregate and Check Results
if (!await hasUploadPermissionTask || !await hasProjectPermissionTask)
@ -514,11 +533,12 @@ namespace Marco.Pms.Services.Service
}
var validationErrors = new List<string>();
var project = await projectTask;
var expenseType = await expenseTypeTask;
var paymentMode = await paymentModeTask;
var statusMapping = await statusMappingTask;
var paidBy = await paidByTask;
var project = projectTask.Result;
var expenseType = expenseTypeTask.Result;
var paymentMode = paymentModeTask.Result;
var statusMapping = statusMappingTask.Result;
var paidBy = paidByTask.Result;
var lastExpenseUId = expenseUIdTask.Result;
if (project == null) validationErrors.Add("Project not found.");
if (paidBy == null) validationErrors.Add("Paid by employee not found");
@ -534,9 +554,10 @@ namespace Marco.Pms.Services.Service
_logger.LogWarning("Expense creation failed due to validation errors: {ValidationErrors}", errorMessage);
return ApiResponse<object>.ErrorResponse("Invalid input data.", errorMessage, 400);
}
var currentexpenseUId = (lastExpenseUId + 1).ToString("D5");
// 3. Entity Creation
var expense = _mapper.Map<Expenses>(dto);
expense.ExpenseUId = $"EX-{currentexpenseUId}";
expense.CreatedById = loggedInEmployee.Id;
expense.CreatedAt = DateTime.UtcNow;
expense.TenantId = tenantId;
@ -1079,6 +1100,13 @@ namespace Marco.Pms.Services.Service
#endregion
#region =================================================================== Helper Functions ===================================================================
private int ExtractNumber(string id)
{
// Extract trailing number; handles EX_0001, EX-0001, EX0001
var m = Regex.Match(id ?? string.Empty, @"(\d+)$");
return m.Success ? int.Parse(m.Value) : int.MinValue; // put invalid IDs at the bottom
}
private static object ExceptionMapper(Exception ex)
{
return new

File diff suppressed because it is too large Load Diff

View File

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

File diff suppressed because it is too large Load Diff

View File

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