Compare commits
43 Commits
main
...
OnFieldWor
Author | SHA1 | Date | |
---|---|---|---|
663d94093c | |||
dc83aa72a6 | |||
ef597b2bb7 | |||
4acc61f03a | |||
c47b0da7b8 | |||
d180d5d60e | |||
60eefa78c0 | |||
b1af96b923 | |||
629c4541d6 | |||
e93408c00d | |||
16e509ccbd | |||
8a4a056c2d | |||
f6e8a0d5e2 | |||
0561e356d8 | |||
e2ee3f325c | |||
6441103e30 | |||
d380dfebd2 | |||
658fa8cd23 | |||
3afdad29b2 | |||
8f463ce90d | |||
590476a8aa | |||
548e714ea9 | |||
91f4305995 | |||
7e15c517ac | |||
9332d9cc0b | |||
541ed28bd2 | |||
0e1d20156f | |||
87c5de87a1 | |||
5eda1773b7 | |||
b3f54962ab | |||
040e7df32b | |||
0066e20c43 | |||
7659eadd00 | |||
824381bb49 | |||
207a44acd7 | |||
7775f58d69 | |||
91be729b41 | |||
0bd57d29d8 | |||
b442bb4bbc | |||
ca3e47c1e6 | |||
061512d501 | |||
71cc442054 | |||
bab03a8e47 |
6209
Marco.Pms.DataAccess/Migrations/20251003093145_Added_ExpenceUID_In_Expense_Table.Designer.cs
generated
Normal file
6209
Marco.Pms.DataAccess/Migrations/20251003093145_Added_ExpenceUID_In_Expense_Table.Designer.cs
generated
Normal file
File diff suppressed because one or more lines are too long
@ -0,0 +1,29 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Marco.Pms.DataAccess.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class Added_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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
6226
Marco.Pms.DataAccess/Migrations/20251008121556_Added_Requested_In_Attendance_Table.Designer.cs
generated
Normal file
6226
Marco.Pms.DataAccess/Migrations/20251008121556_Added_Requested_In_Attendance_Table.Designer.cs
generated
Normal file
File diff suppressed because one or more lines are too long
@ -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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -172,6 +172,9 @@ namespace Marco.Pms.DataAccess.Migrations
|
|||||||
b.Property<int>("Activity")
|
b.Property<int>("Activity")
|
||||||
.HasColumnType("int");
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("ApprovedAt")
|
||||||
|
.HasColumnType("datetime(6)");
|
||||||
|
|
||||||
b.Property<Guid?>("ApprovedById")
|
b.Property<Guid?>("ApprovedById")
|
||||||
.HasColumnType("char(36)");
|
.HasColumnType("char(36)");
|
||||||
|
|
||||||
@ -200,6 +203,12 @@ namespace Marco.Pms.DataAccess.Migrations
|
|||||||
b.Property<Guid>("ProjectID")
|
b.Property<Guid>("ProjectID")
|
||||||
.HasColumnType("char(36)");
|
.HasColumnType("char(36)");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("RequestedAt")
|
||||||
|
.HasColumnType("datetime(6)");
|
||||||
|
|
||||||
|
b.Property<Guid?>("RequestedById")
|
||||||
|
.HasColumnType("char(36)");
|
||||||
|
|
||||||
b.Property<Guid>("TenantId")
|
b.Property<Guid>("TenantId")
|
||||||
.HasColumnType("char(36)");
|
.HasColumnType("char(36)");
|
||||||
|
|
||||||
@ -209,6 +218,8 @@ namespace Marco.Pms.DataAccess.Migrations
|
|||||||
|
|
||||||
b.HasIndex("EmployeeId");
|
b.HasIndex("EmployeeId");
|
||||||
|
|
||||||
|
b.HasIndex("RequestedById");
|
||||||
|
|
||||||
b.HasIndex("TenantId");
|
b.HasIndex("TenantId");
|
||||||
|
|
||||||
b.ToTable("Attendes");
|
b.ToTable("Attendes");
|
||||||
@ -1832,6 +1843,10 @@ namespace Marco.Pms.DataAccess.Migrations
|
|||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasColumnType("longtext");
|
.HasColumnType("longtext");
|
||||||
|
|
||||||
|
b.Property<string>("ExpenseUId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("longtext");
|
||||||
|
|
||||||
b.Property<Guid>("ExpensesTypeId")
|
b.Property<Guid>("ExpensesTypeId")
|
||||||
.HasColumnType("char(36)");
|
.HasColumnType("char(36)");
|
||||||
|
|
||||||
@ -4711,6 +4726,10 @@ namespace Marco.Pms.DataAccess.Migrations
|
|||||||
.OnDelete(DeleteBehavior.Cascade)
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
.IsRequired();
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("Marco.Pms.Model.Employees.Employee", "RequestedBy")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("RequestedById");
|
||||||
|
|
||||||
b.HasOne("Marco.Pms.Model.TenantModels.Tenant", "Tenant")
|
b.HasOne("Marco.Pms.Model.TenantModels.Tenant", "Tenant")
|
||||||
.WithMany()
|
.WithMany()
|
||||||
.HasForeignKey("TenantId")
|
.HasForeignKey("TenantId")
|
||||||
@ -4721,6 +4740,8 @@ namespace Marco.Pms.DataAccess.Migrations
|
|||||||
|
|
||||||
b.Navigation("Employee");
|
b.Navigation("Employee");
|
||||||
|
|
||||||
|
b.Navigation("RequestedBy");
|
||||||
|
|
||||||
b.Navigation("Tenant");
|
b.Navigation("Tenant");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -19,6 +19,8 @@ namespace Marco.Pms.Model.AttendanceModule
|
|||||||
public Guid ProjectID { get; set; }
|
public Guid ProjectID { get; set; }
|
||||||
|
|
||||||
public DateTime AttendanceDate { get; set; }
|
public DateTime AttendanceDate { get; set; }
|
||||||
|
public DateTime? RequestedAt { get; set; }
|
||||||
|
public DateTime? ApprovedAt { get; set; }
|
||||||
public DateTime? InTime { get; set; }
|
public DateTime? InTime { get; set; }
|
||||||
public DateTime? OutTime { get; set; }
|
public DateTime? OutTime { get; set; }
|
||||||
public bool IsApproved { get; set; }
|
public bool IsApproved { get; set; }
|
||||||
@ -29,5 +31,10 @@ namespace Marco.Pms.Model.AttendanceModule
|
|||||||
[ForeignKey("ApprovedById")]
|
[ForeignKey("ApprovedById")]
|
||||||
[ValidateNever]
|
[ValidateNever]
|
||||||
public Employee? Approver { get; set; }
|
public Employee? Approver { get; set; }
|
||||||
|
public Guid? RequestedById { get; set; }
|
||||||
|
|
||||||
|
[ForeignKey("RequestedById")]
|
||||||
|
[ValidateNever]
|
||||||
|
public Employee? RequestedBy { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,6 +5,5 @@
|
|||||||
public required Guid EmployeeId { get; set; }
|
public required Guid EmployeeId { get; set; }
|
||||||
public required string MPIN { get; set; }
|
public required string MPIN { get; set; }
|
||||||
public required string MPINToken { get; set; }
|
public required string MPINToken { get; set; }
|
||||||
public required string FcmToken { get; set; }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -19,7 +19,7 @@
|
|||||||
public string? EmergencyPhoneNumber { get; set; }
|
public string? EmergencyPhoneNumber { get; set; }
|
||||||
public string? EmergencyContactPerson { get; set; }
|
public string? EmergencyContactPerson { get; set; }
|
||||||
public Guid JobRoleId { get; set; }
|
public Guid JobRoleId { get; set; }
|
||||||
public required Guid OrganizationId { get; set; }
|
public Guid? OrganizationId { get; set; }
|
||||||
public required bool HasApplicationAccess { get; set; }
|
public required bool HasApplicationAccess { get; set; }
|
||||||
}
|
}
|
||||||
public class MobileUserManageDto
|
public class MobileUserManageDto
|
||||||
@ -33,7 +33,7 @@
|
|||||||
public required string Gender { get; set; }
|
public required string Gender { get; set; }
|
||||||
public Guid JobRoleId { get; set; }
|
public Guid JobRoleId { get; set; }
|
||||||
public string? ProfileImage { get; set; }
|
public string? ProfileImage { get; set; }
|
||||||
public required Guid OrganizationId { get; set; }
|
public Guid? OrganizationId { get; set; }
|
||||||
public required bool HasApplicationAccess { get; set; }
|
public required bool HasApplicationAccess { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -27,7 +27,7 @@ namespace Marco.Pms.Model.Dtos.Project
|
|||||||
[DisplayName("Project Status")]
|
[DisplayName("Project Status")]
|
||||||
[Required(ErrorMessage = "Project Status is required!")]
|
[Required(ErrorMessage = "Project Status is required!")]
|
||||||
public required Guid ProjectStatusId { get; set; }
|
public required Guid ProjectStatusId { get; set; }
|
||||||
public required Guid PromoterId { get; set; }
|
public Guid? PromoterId { get; set; }
|
||||||
public required Guid PMCId { get; set; }
|
public Guid? PMCId { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -27,7 +27,7 @@ namespace Marco.Pms.Model.Dtos.Project
|
|||||||
[DisplayName("Project Status")]
|
[DisplayName("Project Status")]
|
||||||
[Required(ErrorMessage = "Project Status is required!")]
|
[Required(ErrorMessage = "Project Status is required!")]
|
||||||
public required Guid ProjectStatusId { get; set; }
|
public required Guid ProjectStatusId { get; set; }
|
||||||
public required Guid PromoterId { get; set; }
|
public Guid? PromoterId { get; set; }
|
||||||
public required Guid PMCId { get; set; }
|
public Guid? PMCId { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -54,6 +54,7 @@ namespace Marco.Pms.Model.Expenses
|
|||||||
public DateTime CreatedAt { get; set; }
|
public DateTime CreatedAt { get; set; }
|
||||||
public string? TransactionId { get; set; }
|
public string? TransactionId { get; set; }
|
||||||
public string Description { get; set; } = string.Empty;
|
public string Description { get; set; } = string.Empty;
|
||||||
|
public string ExpenseUId { get; set; } = string.Empty;
|
||||||
public string? Location { get; set; }
|
public string? Location { get; set; }
|
||||||
public string? GSTNumber { get; set; }
|
public string? GSTNumber { get; set; }
|
||||||
public string SupplerName { get; set; } = string.Empty;
|
public string SupplerName { get; set; } = string.Empty;
|
||||||
|
@ -6,6 +6,7 @@
|
|||||||
public List<Guid>? StatusIds { get; set; }
|
public List<Guid>? StatusIds { get; set; }
|
||||||
public List<Guid>? CreatedByIds { get; set; }
|
public List<Guid>? CreatedByIds { get; set; }
|
||||||
public List<Guid>? PaidById { get; set; }
|
public List<Guid>? PaidById { get; set; }
|
||||||
|
public List<Guid>? ExpenseTypeIds { get; set; }
|
||||||
public bool IsTransactionDate { get; set; } = false;
|
public bool IsTransactionDate { get; set; } = false;
|
||||||
public DateTime? StartDate { get; set; }
|
public DateTime? StartDate { get; set; }
|
||||||
public DateTime? EndDate { get; set; }
|
public DateTime? EndDate { get; set; }
|
||||||
|
@ -5,6 +5,7 @@
|
|||||||
public List<Guid>? BuildingIds { get; set; }
|
public List<Guid>? BuildingIds { get; set; }
|
||||||
public List<Guid>? FloorIds { get; set; }
|
public List<Guid>? FloorIds { get; set; }
|
||||||
public List<Guid>? ActivityIds { get; set; }
|
public List<Guid>? ActivityIds { get; set; }
|
||||||
|
public List<Guid>? ServiceIds { get; set; }
|
||||||
public DateTime? dateFrom { get; set; }
|
public DateTime? dateFrom { get; set; }
|
||||||
public DateTime? dateTo { get; set; }
|
public DateTime? dateTo { get; set; }
|
||||||
}
|
}
|
||||||
|
@ -20,6 +20,7 @@ namespace Marco.Pms.Model.MongoDBModels.Expenses
|
|||||||
public DateTime ExpireAt { get; set; } = DateTime.UtcNow.Date.AddDays(1);
|
public DateTime ExpireAt { get; set; } = DateTime.UtcNow.Date.AddDays(1);
|
||||||
public string SupplerName { get; set; } = string.Empty;
|
public string SupplerName { get; set; } = string.Empty;
|
||||||
public double Amount { get; set; }
|
public double Amount { get; set; }
|
||||||
|
public string? ExpenseUId { get; set; }
|
||||||
public ExpensesStatusMasterMongoDB Status { get; set; } = new ExpensesStatusMasterMongoDB();
|
public ExpensesStatusMasterMongoDB Status { get; set; } = new ExpensesStatusMasterMongoDB();
|
||||||
public List<ExpensesStatusMasterMongoDB> NextStatus { get; set; } = new List<ExpensesStatusMasterMongoDB>();
|
public List<ExpensesStatusMasterMongoDB> NextStatus { get; set; } = new List<ExpensesStatusMasterMongoDB>();
|
||||||
public bool PreApproved { get; set; } = false;
|
public bool PreApproved { get; set; } = false;
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
using Marco.Pms.Model.Dtos.Attendance;
|
using Marco.Pms.Model.Dtos.Attendance;
|
||||||
|
using Marco.Pms.Model.ViewModels.Activities;
|
||||||
|
|
||||||
namespace Marco.Pms.Model.ViewModels.AttendanceVM
|
namespace Marco.Pms.Model.ViewModels.AttendanceVM
|
||||||
{
|
{
|
||||||
@ -6,15 +7,20 @@ namespace Marco.Pms.Model.ViewModels.AttendanceVM
|
|||||||
{
|
{
|
||||||
public Guid Id { get; set; }
|
public Guid Id { get; set; }
|
||||||
public Guid EmployeeId { get; set; }
|
public Guid EmployeeId { get; set; }
|
||||||
|
public Guid ProjectId { get; set; }
|
||||||
public string? FirstName { get; set; }
|
public string? FirstName { get; set; }
|
||||||
public string? LastName { get; set; }
|
public string? LastName { get; set; }
|
||||||
public string? EmployeeAvatar { get; set; }
|
public string? EmployeeAvatar { get; set; }
|
||||||
public string? OrganizationName { get; set; }
|
public string? OrganizationName { get; set; }
|
||||||
|
public string? ProjectName { get; set; }
|
||||||
public DateTime? CheckInTime { get; set; }
|
public DateTime? CheckInTime { get; set; }
|
||||||
public DateTime? CheckOutTime { get; set; }
|
public DateTime? CheckOutTime { get; set; }
|
||||||
|
public DateTime? RequestedAt { get; set; }
|
||||||
|
public DateTime? ApprovedAt { get; set; }
|
||||||
public string? JobRoleName { get; set; }
|
public string? JobRoleName { get; set; }
|
||||||
public ATTENDANCE_MARK_TYPE Activity { get; set; }
|
public ATTENDANCE_MARK_TYPE Activity { get; set; }
|
||||||
|
public BasicEmployeeVM? Approver { get; set; }
|
||||||
|
public BasicEmployeeVM? RequestedBy { get; set; }
|
||||||
public Guid? DocumentId { get; set; }
|
public Guid? DocumentId { get; set; }
|
||||||
public string? ThumbPreSignedUrl { get; set; }
|
public string? ThumbPreSignedUrl { get; set; }
|
||||||
public string? PreSignedUrl { get; set; }
|
public string? PreSignedUrl { get; set; }
|
||||||
|
@ -26,6 +26,7 @@ namespace Marco.Pms.Model.ViewModels.Expenses
|
|||||||
public string? TransactionId { get; set; }
|
public string? TransactionId { get; set; }
|
||||||
public string Description { get; set; } = string.Empty;
|
public string Description { get; set; } = string.Empty;
|
||||||
public string? Location { get; set; }
|
public string? Location { get; set; }
|
||||||
|
public string? ExpenseUId { get; set; }
|
||||||
public List<BasicDocumentVM> Documents { get; set; } = new List<BasicDocumentVM>();
|
public List<BasicDocumentVM> Documents { get; set; } = new List<BasicDocumentVM>();
|
||||||
public List<ExpenseLogVM> ExpenseLogs { get; set; } = new List<ExpenseLogVM>();
|
public List<ExpenseLogVM> ExpenseLogs { get; set; } = new List<ExpenseLogVM>();
|
||||||
public string? GSTNumber { get; set; }
|
public string? GSTNumber { get; set; }
|
||||||
|
@ -18,6 +18,7 @@ namespace Marco.Pms.Model.ViewModels.Expanses
|
|||||||
public DateTime TransactionDate { get; set; }
|
public DateTime TransactionDate { get; set; }
|
||||||
public DateTime CreatedAt { get; set; }
|
public DateTime CreatedAt { get; set; }
|
||||||
public string SupplerName { get; set; } = string.Empty;
|
public string SupplerName { get; set; } = string.Empty;
|
||||||
|
public string? ExpenseUId { get; set; }
|
||||||
public string Description { get; set; } = string.Empty;
|
public string Description { get; set; } = string.Empty;
|
||||||
public string TransactionId { get; set; } = string.Empty;
|
public string TransactionId { get; set; } = string.Empty;
|
||||||
public double Amount { get; set; }
|
public double Amount { get; set; }
|
||||||
|
@ -470,6 +470,19 @@ namespace Marco.Pms.Services.Controllers
|
|||||||
|
|
||||||
foreach (var item in menu.Items)
|
foreach (var item in menu.Items)
|
||||||
{
|
{
|
||||||
|
if (item.Text == "Projects")
|
||||||
|
{
|
||||||
|
allowedItems.Add(new MenuItem
|
||||||
|
{
|
||||||
|
Text = "Projects",
|
||||||
|
Icon = "bx bx-building-house",
|
||||||
|
Available = true,
|
||||||
|
Link = "/projects",
|
||||||
|
PermissionIds = new List<string>(),
|
||||||
|
Submenu = new List<SubMenuItem>()
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
// --- Item permission check ---
|
// --- Item permission check ---
|
||||||
if (!item.PermissionIds.Any())
|
if (!item.PermissionIds.Any())
|
||||||
{
|
{
|
||||||
@ -608,6 +621,20 @@ namespace Marco.Pms.Services.Controllers
|
|||||||
var featureIds = await generalHelper.GetFeatureIdsByTenentIdAsync(tenantId);
|
var featureIds = await generalHelper.GetFeatureIdsByTenentIdAsync(tenantId);
|
||||||
_logger.LogInfo("Enabled features for TenantId: {TenantId} -> {FeatureIds}", tenantId, string.Join(",", featureIds));
|
_logger.LogInfo("Enabled features for TenantId: {TenantId} -> {FeatureIds}", tenantId, string.Join(",", featureIds));
|
||||||
|
|
||||||
|
if (!(featureIds?.Any() ?? false))
|
||||||
|
{
|
||||||
|
featureIds = new List<Guid>
|
||||||
|
{
|
||||||
|
new Guid("a4e25142-449b-4334-a6e5-22f70e4732d7"), // Expense Management feature
|
||||||
|
new Guid("81ab8a87-8ccd-4015-a917-0627cee6a100"), // Employee Management feature
|
||||||
|
new Guid("52c9cf54-1eb2-44d2-81bb-524cf29c0a94"), // Attendance Management feature
|
||||||
|
new Guid("a8cf4331-8f04-4961-8360-a3f7c3cc7462"), // Document Management feature
|
||||||
|
new Guid("be3b3afc-6ccf-4566-b9b6-aafcb65546be"), // Masters Management feature
|
||||||
|
new Guid("39e66f81-efc6-446c-95bd-46bff6cfb606"), // Directory Management feature
|
||||||
|
new Guid("6d4c82d6-dbce-48ab-b8b8-f785f4d8c914") // Organization Management feature
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// Aggregate menus based on enabled features
|
// Aggregate menus based on enabled features
|
||||||
var response = featureIds
|
var response = featureIds
|
||||||
.Where(id => featureMenus.ContainsKey(id))
|
.Where(id => featureMenus.ContainsKey(id))
|
||||||
|
@ -1,11 +1,12 @@
|
|||||||
using Marco.Pms.DataAccess.Data;
|
using AutoMapper;
|
||||||
|
using Marco.Pms.DataAccess.Data;
|
||||||
using Marco.Pms.Model.AttendanceModule;
|
using Marco.Pms.Model.AttendanceModule;
|
||||||
using Marco.Pms.Model.Dtos.Attendance;
|
using Marco.Pms.Model.Dtos.Attendance;
|
||||||
using Marco.Pms.Model.Employees;
|
using Marco.Pms.Model.Employees;
|
||||||
using Marco.Pms.Model.Entitlements;
|
using Marco.Pms.Model.Entitlements;
|
||||||
using Marco.Pms.Model.Mapper;
|
using Marco.Pms.Model.Mapper;
|
||||||
using Marco.Pms.Model.Projects;
|
|
||||||
using Marco.Pms.Model.Utilities;
|
using Marco.Pms.Model.Utilities;
|
||||||
|
using Marco.Pms.Model.ViewModels.Activities;
|
||||||
using Marco.Pms.Model.ViewModels.AttendanceVM;
|
using Marco.Pms.Model.ViewModels.AttendanceVM;
|
||||||
using Marco.Pms.Services.Hubs;
|
using Marco.Pms.Services.Hubs;
|
||||||
using Marco.Pms.Services.Service;
|
using Marco.Pms.Services.Service;
|
||||||
@ -28,48 +29,42 @@ namespace MarcoBMS.Services.Controllers
|
|||||||
public class AttendanceController : ControllerBase
|
public class AttendanceController : ControllerBase
|
||||||
{
|
{
|
||||||
private readonly ApplicationDbContext _context;
|
private readonly ApplicationDbContext _context;
|
||||||
private readonly EmployeeHelper _employeeHelper;
|
private readonly IServiceScopeFactory _serviceScopeFactory;
|
||||||
private readonly IProjectServices _projectServices;
|
|
||||||
private readonly UserHelper _userHelper;
|
private readonly UserHelper _userHelper;
|
||||||
private readonly S3UploadService _s3Service;
|
|
||||||
private readonly PermissionServices _permission;
|
private readonly PermissionServices _permission;
|
||||||
private readonly ILoggingService _logger;
|
private readonly ILoggingService _logger;
|
||||||
private readonly IHubContext<MarcoHub> _signalR;
|
private readonly Guid tenantId;
|
||||||
private readonly IFirebaseService _firebase;
|
private readonly IMapper _mapper;
|
||||||
|
|
||||||
public AttendanceController(
|
public AttendanceController(
|
||||||
ApplicationDbContext context, EmployeeHelper employeeHelper, IProjectServices projectServices, UserHelper userHelper,
|
ApplicationDbContext context,
|
||||||
S3UploadService s3Service, ILoggingService logger, PermissionServices permission, IHubContext<MarcoHub> signalR, IFirebaseService firebase)
|
UserHelper userHelper,
|
||||||
|
IServiceScopeFactory serviceScopeFactory,
|
||||||
|
ILoggingService logger,
|
||||||
|
PermissionServices permission,
|
||||||
|
IMapper mapper)
|
||||||
{
|
{
|
||||||
_context = context;
|
_context = context;
|
||||||
_employeeHelper = employeeHelper;
|
_serviceScopeFactory = serviceScopeFactory;
|
||||||
_projectServices = projectServices;
|
|
||||||
_userHelper = userHelper;
|
_userHelper = userHelper;
|
||||||
_s3Service = s3Service;
|
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_permission = permission;
|
_permission = permission;
|
||||||
_signalR = signalR;
|
_mapper = mapper;
|
||||||
_firebase = firebase;
|
tenantId = userHelper.GetTenantId();
|
||||||
}
|
|
||||||
|
|
||||||
private Guid GetTenantId()
|
|
||||||
{
|
|
||||||
return _userHelper.GetTenantId();
|
|
||||||
//var tenant = User.FindFirst("TenantId")?.Value;
|
|
||||||
//return (tenant != null ? Convert.ToInt32(tenant) : 1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("log/attendance/{attendanceid}")]
|
[HttpGet("log/attendance/{attendanceid}")]
|
||||||
|
|
||||||
public async Task<IActionResult> GetAttendanceLogById(Guid attendanceid)
|
public async Task<IActionResult> GetAttendanceLogById(Guid attendanceid)
|
||||||
{
|
{
|
||||||
Guid TenantId = GetTenantId();
|
using var scope = _serviceScopeFactory.CreateScope();
|
||||||
|
var _s3Service = scope.ServiceProvider.GetRequiredService<S3UploadService>();
|
||||||
|
|
||||||
List<AttendanceLog> lstAttendance = await _context.AttendanceLogs
|
List<AttendanceLog> lstAttendance = await _context.AttendanceLogs
|
||||||
.Include(a => a.Document)
|
.Include(a => a.Document)
|
||||||
.Include(a => a.Employee)
|
.Include(a => a.Employee)
|
||||||
.Include(a => a.UpdatedByEmployee)
|
.Include(a => a.UpdatedByEmployee)
|
||||||
.Where(c => c.AttendanceId == attendanceid && c.TenantId == TenantId)
|
.Where(c => c.AttendanceId == attendanceid && c.TenantId == tenantId)
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
|
|
||||||
List<AttendanceLogVM> attendanceLogVMs = new List<AttendanceLogVM>();
|
List<AttendanceLogVM> attendanceLogVMs = new List<AttendanceLogVM>();
|
||||||
@ -85,30 +80,42 @@ namespace MarcoBMS.Services.Controllers
|
|||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("log/employee/{employeeId}")]
|
[HttpGet("log/employee/{employeeId}")]
|
||||||
public async Task<IActionResult> GetAttendanceLogByEmployeeId(Guid employeeId, [FromQuery] string? dateFrom = null, [FromQuery] string? dateTo = null)
|
public async Task<IActionResult> GetAttendanceLogByEmployeeId(Guid employeeId, [FromQuery] DateTime? dateFrom = null, [FromQuery] DateTime? dateTo = null)
|
||||||
{
|
{
|
||||||
Guid TenantId = GetTenantId();
|
|
||||||
DateTime fromDate = new DateTime();
|
|
||||||
DateTime toDate = new DateTime();
|
|
||||||
|
|
||||||
if (dateFrom != null && DateTime.TryParse(dateFrom, out fromDate) == false)
|
|
||||||
{
|
|
||||||
_logger.LogWarning("User sent Invalid from Date while featching attendance logs");
|
|
||||||
return BadRequest(ApiResponse<object>.ErrorResponse("Invalid Date", "Invalid Date", 400));
|
|
||||||
}
|
|
||||||
if (dateTo != null && DateTime.TryParse(dateTo, out toDate) == false)
|
|
||||||
{
|
|
||||||
_logger.LogWarning("User sent Invalid to Date while featching attendance logs");
|
|
||||||
return BadRequest(ApiResponse<object>.ErrorResponse("Invalid Date", "Invalid Date", 400));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (employeeId == Guid.Empty)
|
if (employeeId == Guid.Empty)
|
||||||
{
|
{
|
||||||
_logger.LogWarning("The employee Id sent by user is empty");
|
_logger.LogWarning("The employee Id sent by user is empty");
|
||||||
return BadRequest(ApiResponse<object>.ErrorResponse("Employee ID is required and must not be Empty.", "Employee ID is required and must not be empty.", 400));
|
return BadRequest(ApiResponse<object>.ErrorResponse("Employee ID is required and must not be Empty.", "Employee ID is required and must not be empty.", 400));
|
||||||
}
|
}
|
||||||
List<Attendance> attendances = await _context.Attendes.Where(c => c.EmployeeId == employeeId && c.TenantId == TenantId && c.AttendanceDate.Date >= fromDate && c.AttendanceDate.Date <= toDate).ToListAsync();
|
|
||||||
Employee? employee = await _context.Employees.Include(e => e.JobRole).FirstOrDefaultAsync(e => e.Id == employeeId && e.TenantId == TenantId && e.IsActive);
|
Employee? employee = await _context.Employees.Include(e => e.JobRole).FirstOrDefaultAsync(e => e.Id == employeeId && e.TenantId == tenantId);
|
||||||
|
if (employee == null)
|
||||||
|
{
|
||||||
|
_logger.LogWarning("Employee {EmployeeId} not found", employeeId);
|
||||||
|
return NotFound(ApiResponse<object>.ErrorResponse("Employee not found", "Employee not found in database", 404));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!dateFrom.HasValue)
|
||||||
|
{
|
||||||
|
dateFrom = DateTime.UtcNow;
|
||||||
|
}
|
||||||
|
if (!dateTo.HasValue)
|
||||||
|
{
|
||||||
|
var days = 0 - 7;
|
||||||
|
dateTo = dateFrom.Value.AddDays(days);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Attendance> attendances = await _context.Attendes
|
||||||
|
.Include(a => a.RequestedBy)
|
||||||
|
.ThenInclude(e => e!.JobRole)
|
||||||
|
.Include(a => a.RequestedBy)
|
||||||
|
.ThenInclude(e => e!.JobRole)
|
||||||
|
.Where(c => c.EmployeeId == employeeId && c.TenantId == tenantId && c.AttendanceDate.Date >= dateFrom && c.AttendanceDate.Date <= dateTo).ToListAsync();
|
||||||
|
|
||||||
|
var projectIds = attendances.Select(a => a.ProjectID).Distinct().ToList();
|
||||||
|
|
||||||
|
var projects = await _context.Projects.Where(p => projectIds.Contains(p.Id) && p.TenantId == tenantId).ToListAsync();
|
||||||
|
|
||||||
List<EmployeeAttendanceVM> results = new List<EmployeeAttendanceVM>();
|
List<EmployeeAttendanceVM> results = new List<EmployeeAttendanceVM>();
|
||||||
|
|
||||||
if (employee != null)
|
if (employee != null)
|
||||||
@ -121,11 +128,17 @@ namespace MarcoBMS.Services.Controllers
|
|||||||
EmployeeId = employee.Id,
|
EmployeeId = employee.Id,
|
||||||
FirstName = employee.FirstName,
|
FirstName = employee.FirstName,
|
||||||
LastName = employee.LastName,
|
LastName = employee.LastName,
|
||||||
|
ProjectId = attendance.ProjectID,
|
||||||
|
ProjectName = projects.Where(p => p.Id == attendance.ProjectID).Select(p => p.Name).FirstOrDefault(),
|
||||||
CheckInTime = attendance.InTime,
|
CheckInTime = attendance.InTime,
|
||||||
CheckOutTime = attendance.OutTime,
|
CheckOutTime = attendance.OutTime,
|
||||||
JobRoleName = employee.JobRole != null ? employee.JobRole.Name : "",
|
JobRoleName = employee.JobRole != null ? employee.JobRole.Name : "",
|
||||||
Activity = attendance.Activity,
|
Activity = attendance.Activity,
|
||||||
EmployeeAvatar = null
|
EmployeeAvatar = null,
|
||||||
|
RequestedAt = attendance.RequestedAt,
|
||||||
|
RequestedBy = _mapper.Map<BasicEmployeeVM>(attendance.RequestedBy),
|
||||||
|
ApprovedAt = attendance.ApprovedAt,
|
||||||
|
Approver = _mapper.Map<BasicEmployeeVM>(attendance.Approver)
|
||||||
};
|
};
|
||||||
results.Add(result);
|
results.Add(result);
|
||||||
}
|
}
|
||||||
@ -146,27 +159,12 @@ namespace MarcoBMS.Services.Controllers
|
|||||||
|
|
||||||
[HttpGet("project/log")]
|
[HttpGet("project/log")]
|
||||||
|
|
||||||
public async Task<IActionResult> EmployeeAttendanceByDateRange([FromQuery] Guid projectId, [FromQuery] Guid? organizationId, [FromQuery] string? dateFrom = null, [FromQuery] string? dateTo = null)
|
public async Task<IActionResult> EmployeeAttendanceByDateRange([FromQuery] Guid? projectId, [FromQuery] Guid? organizationId, [FromQuery] string? dateFrom = null, [FromQuery] string? dateTo = null)
|
||||||
{
|
{
|
||||||
Guid tenantId = GetTenantId();
|
|
||||||
var LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
|
var LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
|
||||||
|
|
||||||
var project = await _context.Projects.AsNoTracking().FirstOrDefaultAsync(p => p.Id == projectId && p.TenantId == tenantId);
|
|
||||||
if (project == null)
|
|
||||||
{
|
|
||||||
_logger.LogWarning("Project {ProjectId} not found in database", projectId);
|
|
||||||
return NotFound(ApiResponse<object>.ErrorResponse("Project not found."));
|
|
||||||
}
|
|
||||||
|
|
||||||
var hasTeamAttendancePermission = await _permission.HasPermission(PermissionsMaster.TeamAttendance, LoggedInEmployee.Id);
|
var hasTeamAttendancePermission = await _permission.HasPermission(PermissionsMaster.TeamAttendance, LoggedInEmployee.Id);
|
||||||
var hasSelfAttendancePermission = await _permission.HasPermission(PermissionsMaster.SelfAttendance, LoggedInEmployee.Id);
|
var hasSelfAttendancePermission = await _permission.HasPermission(PermissionsMaster.SelfAttendance, LoggedInEmployee.Id);
|
||||||
var hasProjectPermission = await _permission.HasProjectPermission(LoggedInEmployee, projectId);
|
|
||||||
|
|
||||||
if (!hasProjectPermission)
|
|
||||||
{
|
|
||||||
_logger.LogWarning("Employee {EmployeeId} tries to access attendance of project {ProjectId}, but don't have access", LoggedInEmployee.Id, projectId);
|
|
||||||
return Unauthorized(ApiResponse<object>.ErrorResponse("Unauthorized access", "Unauthorized access", 404));
|
|
||||||
}
|
|
||||||
|
|
||||||
DateTime fromDate = new DateTime();
|
DateTime fromDate = new DateTime();
|
||||||
DateTime toDate = new DateTime();
|
DateTime toDate = new DateTime();
|
||||||
@ -182,26 +180,46 @@ namespace MarcoBMS.Services.Controllers
|
|||||||
return BadRequest(ApiResponse<object>.ErrorResponse("Invalid Date", "Invalid Date", 400));
|
return BadRequest(ApiResponse<object>.ErrorResponse("Invalid Date", "Invalid Date", 400));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (projectId == Guid.Empty)
|
|
||||||
{
|
|
||||||
_logger.LogWarning("The project Id sent by user is less than or equal to zero");
|
|
||||||
return BadRequest(ApiResponse<object>.ErrorResponse("Project ID is required and must be greater than zero.", "Project ID is required and must be greater than zero.", 400));
|
|
||||||
}
|
|
||||||
|
|
||||||
var result = new List<EmployeeAttendanceVM>();
|
var result = new List<EmployeeAttendanceVM>();
|
||||||
//Attendance? attendance = null;
|
//Attendance? attendance = null;
|
||||||
ProjectAllocation? teamMember = null;
|
|
||||||
|
|
||||||
if (dateFrom == null) fromDate = DateTime.UtcNow.Date;
|
if (dateFrom == null) fromDate = DateTime.UtcNow.Date;
|
||||||
if (dateTo == null && dateFrom != null) toDate = fromDate.AddDays(-1);
|
if (dateTo == null && dateFrom != null) toDate = fromDate.AddDays(-1);
|
||||||
|
|
||||||
|
var lstAttendanceQuery = _context.Attendes
|
||||||
|
.Include(a => a.Employee)
|
||||||
|
.ThenInclude(e => e!.JobRole)
|
||||||
|
.Include(a => a.Employee)
|
||||||
|
.ThenInclude(e => e!.JobRole)
|
||||||
|
.Include(a => a.Employee)
|
||||||
|
.ThenInclude(e => e!.Organization)
|
||||||
|
.Include(a => a.Employee)
|
||||||
|
.ThenInclude(e => e!.JobRole)
|
||||||
|
.Where(a =>
|
||||||
|
a.AttendanceDate.Date >= fromDate.Date &&
|
||||||
|
a.AttendanceDate.Date <= toDate.Date &&
|
||||||
|
a.TenantId == tenantId &&
|
||||||
|
a.Employee != null &&
|
||||||
|
a.Employee.Organization != null &&
|
||||||
|
a.Employee.JobRole != null);
|
||||||
|
|
||||||
|
if (organizationId.HasValue)
|
||||||
|
{
|
||||||
|
lstAttendanceQuery = lstAttendanceQuery.Where(a => a.Employee != null && a.Employee.OrganizationId == organizationId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (projectId.HasValue)
|
||||||
|
{
|
||||||
|
lstAttendanceQuery = lstAttendanceQuery.Where(a => a.ProjectID == projectId);
|
||||||
|
}
|
||||||
|
|
||||||
if (hasTeamAttendancePermission)
|
if (hasTeamAttendancePermission)
|
||||||
{
|
{
|
||||||
List<Attendance> lstAttendance = await _context.Attendes.Where(c => c.ProjectID == projectId && c.AttendanceDate.Date >= fromDate.Date && c.AttendanceDate.Date <= toDate.Date && c.TenantId == tenantId).ToListAsync();
|
List<Attendance> lstAttendance = await lstAttendanceQuery.ToListAsync();
|
||||||
|
|
||||||
|
var projectIds = lstAttendance.Select(a => a.ProjectID).ToList();
|
||||||
|
var projects = await _context.Projects.Where(p => projectIds.Contains(p.Id) && p.TenantId == tenantId).ToListAsync();
|
||||||
|
|
||||||
List<ProjectAllocation> projectteam = await _projectServices.GetTeamByProject(tenantId, projectId, organizationId, true);
|
|
||||||
var jobRole = await _context.JobRoles.ToListAsync();
|
|
||||||
foreach (Attendance? attendance in lstAttendance)
|
foreach (Attendance? attendance in lstAttendance)
|
||||||
{
|
{
|
||||||
var result1 = new EmployeeAttendanceVM()
|
var result1 = new EmployeeAttendanceVM()
|
||||||
@ -209,77 +227,61 @@ namespace MarcoBMS.Services.Controllers
|
|||||||
Id = attendance.Id,
|
Id = attendance.Id,
|
||||||
CheckInTime = attendance.InTime,
|
CheckInTime = attendance.InTime,
|
||||||
CheckOutTime = attendance.OutTime,
|
CheckOutTime = attendance.OutTime,
|
||||||
Activity = attendance.Activity
|
Activity = attendance.Activity,
|
||||||
|
EmployeeId = attendance.EmployeeId,
|
||||||
|
FirstName = attendance.Employee?.FirstName,
|
||||||
|
LastName = attendance.Employee?.LastName,
|
||||||
|
JobRoleName = attendance.Employee?.JobRole?.Name,
|
||||||
|
ProjectId = attendance.ProjectID,
|
||||||
|
ProjectName = projects.Where(p => p.Id == attendance.ProjectID).Select(p => p.Name).FirstOrDefault(),
|
||||||
|
OrganizationName = attendance.Employee?.Organization?.Name,
|
||||||
|
RequestedAt = attendance.RequestedAt,
|
||||||
|
RequestedBy = _mapper.Map<BasicEmployeeVM>(attendance.RequestedBy),
|
||||||
|
ApprovedAt = attendance.ApprovedAt,
|
||||||
|
Approver = _mapper.Map<BasicEmployeeVM>(attendance.Approver)
|
||||||
};
|
};
|
||||||
teamMember = projectteam.Find(x => x.EmployeeId == attendance.EmployeeId);
|
|
||||||
if (teamMember != null)
|
|
||||||
{
|
|
||||||
result1.EmployeeAvatar = null;
|
|
||||||
result1.EmployeeId = teamMember.EmployeeId;
|
|
||||||
if (teamMember.Employee != null)
|
|
||||||
{
|
|
||||||
result1.FirstName = teamMember.Employee.FirstName;
|
|
||||||
result1.LastName = teamMember.Employee.LastName;
|
|
||||||
result1.JobRoleName = teamMember.Employee.JobRole != null ? teamMember.Employee.JobRole.Name : null;
|
|
||||||
result1.OrganizationName = teamMember.Employee.Organization?.Name;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
result1.FirstName = null;
|
|
||||||
result1.LastName = null;
|
|
||||||
result1.JobRoleName = null;
|
|
||||||
result1.OrganizationName = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
result.Add(result1);
|
result.Add(result1);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
else if (hasSelfAttendancePermission)
|
else if (hasSelfAttendancePermission)
|
||||||
{
|
{
|
||||||
List<Attendance> lstAttendances = await _context.Attendes
|
|
||||||
.Where(c => c.ProjectID == projectId && c.EmployeeId == LoggedInEmployee.Id && c.AttendanceDate.Date >= fromDate.Date && c.AttendanceDate.Date <= toDate.Date && c.TenantId == tenantId)
|
|
||||||
.ToListAsync();
|
|
||||||
|
|
||||||
var projectAllocationQuery = _context.ProjectAllocations
|
var lstAttendances = await lstAttendanceQuery.Where(a => a.EmployeeId == LoggedInEmployee.Id).ToListAsync();
|
||||||
.Include(pa => pa.Employee)
|
|
||||||
.ThenInclude(e => e!.Organization)
|
|
||||||
.Where(pa => pa.ProjectId == projectId && pa.EmployeeId == LoggedInEmployee.Id && pa.TenantId == tenantId && pa.IsActive);
|
|
||||||
|
|
||||||
if (organizationId.HasValue)
|
var projectIds = lstAttendances.Select(a => a.ProjectID).ToList();
|
||||||
{
|
var projects = await _context.Projects.Where(p => projectIds.Contains(p.Id) && p.TenantId == tenantId).ToListAsync();
|
||||||
projectAllocationQuery = projectAllocationQuery.Where(pa => pa.Employee != null && pa.Employee.OrganizationId == organizationId);
|
|
||||||
}
|
|
||||||
|
|
||||||
var projectAllocation = await projectAllocationQuery.FirstOrDefaultAsync();
|
|
||||||
|
|
||||||
foreach (var attendance in lstAttendances)
|
foreach (var attendance in lstAttendances)
|
||||||
{
|
|
||||||
if (projectAllocation != null)
|
|
||||||
{
|
{
|
||||||
EmployeeAttendanceVM result1 = new EmployeeAttendanceVM
|
EmployeeAttendanceVM result1 = new EmployeeAttendanceVM
|
||||||
{
|
{
|
||||||
Id = attendance.Id,
|
Id = attendance.Id,
|
||||||
EmployeeAvatar = null,
|
EmployeeAvatar = null,
|
||||||
EmployeeId = projectAllocation.EmployeeId,
|
EmployeeId = attendance.EmployeeId,
|
||||||
FirstName = projectAllocation.Employee?.FirstName,
|
FirstName = attendance.Employee?.FirstName,
|
||||||
LastName = projectAllocation.Employee?.LastName,
|
LastName = attendance.Employee?.LastName,
|
||||||
JobRoleName = projectAllocation.Employee?.JobRole?.Name,
|
JobRoleName = attendance.Employee?.JobRole?.Name,
|
||||||
OrganizationName = projectAllocation.Employee?.Organization?.Name,
|
ProjectId = attendance.ProjectID,
|
||||||
|
ProjectName = projects.Where(p => p.Id == attendance.ProjectID).Select(p => p.Name).FirstOrDefault(),
|
||||||
|
OrganizationName = attendance.Employee?.Organization?.Name,
|
||||||
CheckInTime = attendance.InTime,
|
CheckInTime = attendance.InTime,
|
||||||
CheckOutTime = attendance.OutTime,
|
CheckOutTime = attendance.OutTime,
|
||||||
Activity = attendance.Activity
|
Activity = attendance.Activity,
|
||||||
|
RequestedAt = attendance.RequestedAt,
|
||||||
|
RequestedBy = _mapper.Map<BasicEmployeeVM>(attendance.RequestedBy),
|
||||||
|
ApprovedAt = attendance.ApprovedAt,
|
||||||
|
Approver = _mapper.Map<BasicEmployeeVM>(attendance.Approver)
|
||||||
};
|
};
|
||||||
result.Add(result1);
|
result.Add(result1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
_logger.LogInfo("{count} Attendance records fetched successfully", result.Count);
|
_logger.LogInfo("{count} Attendance records fetched successfully", result.Count);
|
||||||
return Ok(ApiResponse<object>.SuccessResponse(result, System.String.Format("{0} Attendance records fetched successfully", result.Count), 200));
|
return Ok(ApiResponse<object>.SuccessResponse(result, System.String.Format("{0} Attendance records fetched successfully", result.Count), 200));
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("project/team")]
|
[HttpGet("project/team")]
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Retrieves employee attendance records for a specified project and date.
|
/// Retrieves employee attendance records for a specified project and date.
|
||||||
/// The result is filtered based on the logged-in employee's permissions (Team or Self).
|
/// The result is filtered based on the logged-in employee's permissions (Team or Self).
|
||||||
@ -289,13 +291,12 @@ namespace MarcoBMS.Services.Controllers
|
|||||||
/// <param name="includeInactive">Optional. Includes inactive employees in the team list if true.</param>
|
/// <param name="includeInactive">Optional. Includes inactive employees in the team list if true.</param>
|
||||||
/// <param name="date">Optional. The date for which to fetch attendance, in "yyyy-MM-dd" format. Defaults to the current UTC date.</param>
|
/// <param name="date">Optional. The date for which to fetch attendance, in "yyyy-MM-dd" format. Defaults to the current UTC date.</param>
|
||||||
/// <returns>An IActionResult containing a list of employee attendance records or an error response.</returns>
|
/// <returns>An IActionResult containing a list of employee attendance records or an error response.</returns>
|
||||||
public async Task<IActionResult> EmployeeAttendanceByProjectAsync([FromQuery] Guid projectId, [FromQuery] Guid? organizationId, [FromQuery] bool includeInactive, [FromQuery] string? date = null)
|
public async Task<IActionResult> EmployeeAttendanceByProjectAsync([FromQuery] Guid? projectId, [FromQuery] Guid? organizationId, [FromQuery] bool includeInactive, [FromQuery] string? date = null)
|
||||||
{
|
{
|
||||||
var tenantId = GetTenantId();
|
|
||||||
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
|
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
|
||||||
|
|
||||||
// --- 1. Initial Validation and Permission Checks ---
|
// --- 1. Initial Validation and Permission Checks ---
|
||||||
_logger.LogInfo("Fetching attendance for ProjectId: {ProjectId}, TenantId: {TenantId}", projectId, tenantId);
|
_logger.LogInfo("Fetching attendance for ProjectId: {ProjectId}, TenantId: {TenantId}", projectId ?? Guid.Empty, tenantId);
|
||||||
|
|
||||||
// Validate date format
|
// Validate date format
|
||||||
if (!DateTime.TryParse(date, out var forDate))
|
if (!DateTime.TryParse(date, out var forDate))
|
||||||
@ -303,20 +304,6 @@ namespace MarcoBMS.Services.Controllers
|
|||||||
forDate = DateTime.UtcNow.Date; // Default to today's date
|
forDate = DateTime.UtcNow.Date; // Default to today's date
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if the project exists and if the employee has access
|
|
||||||
var project = await _context.Projects.AsNoTracking().FirstOrDefaultAsync(p => p.Id == projectId && p.TenantId == tenantId);
|
|
||||||
if (project == null)
|
|
||||||
{
|
|
||||||
_logger.LogWarning("Project {ProjectId} not found in database", projectId);
|
|
||||||
return NotFound(ApiResponse<object>.ErrorResponse("Project not found."));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!await _permission.HasProjectPermission(loggedInEmployee, projectId))
|
|
||||||
{
|
|
||||||
_logger.LogWarning("Unauthorized access attempt by EmployeeId: {EmployeeId} for ProjectId: {ProjectId}", loggedInEmployee.Id, projectId);
|
|
||||||
return Unauthorized(ApiResponse<object>.ErrorResponse("You do not have permission to access this project."));
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- 2. Delegate to Specific Logic Based on Permissions ---
|
// --- 2. Delegate to Specific Logic Based on Permissions ---
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@ -325,13 +312,17 @@ namespace MarcoBMS.Services.Controllers
|
|||||||
|
|
||||||
if (hasTeamAttendancePermission)
|
if (hasTeamAttendancePermission)
|
||||||
{
|
{
|
||||||
|
if (!organizationId.HasValue)
|
||||||
|
{
|
||||||
|
organizationId = loggedInEmployee.OrganizationId;
|
||||||
|
}
|
||||||
_logger.LogInfo("EmployeeId: {EmployeeId} has Team Attendance permission. Fetching team attendance.", loggedInEmployee.Id);
|
_logger.LogInfo("EmployeeId: {EmployeeId} has Team Attendance permission. Fetching team attendance.", loggedInEmployee.Id);
|
||||||
result = await GetTeamAttendanceAsync(tenantId, projectId, organizationId, forDate, includeInactive);
|
result = await GetTeamAttendanceAsync(tenantId, projectId, organizationId.Value, forDate, includeInactive);
|
||||||
}
|
}
|
||||||
else if (await _permission.HasPermission(PermissionsMaster.SelfAttendance, loggedInEmployee.Id))
|
else if (await _permission.HasPermission(PermissionsMaster.SelfAttendance, loggedInEmployee.Id))
|
||||||
{
|
{
|
||||||
_logger.LogInfo("EmployeeId: {EmployeeId} has Self Attendance permission. Fetching self attendance.", loggedInEmployee.Id);
|
_logger.LogInfo("EmployeeId: {EmployeeId} has Self Attendance permission. Fetching self attendance.", loggedInEmployee.Id);
|
||||||
result = await GetSelfAttendanceAsync(tenantId, projectId, loggedInEmployee.Id, organizationId, forDate);
|
result = await GetSelfAttendanceAsync(tenantId, projectId, loggedInEmployee.Id, forDate);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@ -339,41 +330,49 @@ namespace MarcoBMS.Services.Controllers
|
|||||||
return StatusCode(403, ApiResponse<object>.ErrorResponse("You do not have permission to view attendance.", new { }, 403));
|
return StatusCode(403, ApiResponse<object>.ErrorResponse("You do not have permission to view attendance.", new { }, 403));
|
||||||
}
|
}
|
||||||
|
|
||||||
_logger.LogInfo("Successfully fetched {Count} attendance records for ProjectId: {ProjectId}", result.Count, projectId);
|
_logger.LogInfo("Successfully fetched {Count} attendance records for ProjectId: {ProjectId}", result.Count, projectId ?? Guid.Empty);
|
||||||
return Ok(ApiResponse<object>.SuccessResponse(result, $"{result.Count} attendance records fetched successfully."));
|
return Ok(ApiResponse<object>.SuccessResponse(result, $"{result.Count} attendance records fetched successfully."));
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
_logger.LogError(ex, "An error occurred while fetching attendance for ProjectId: {ProjectId}", projectId);
|
_logger.LogError(ex, "An error occurred while fetching attendance for ProjectId: {ProjectId}", projectId ?? Guid.Empty);
|
||||||
return StatusCode(500, ApiResponse<object>.ErrorResponse("An internal server error occurred."));
|
return StatusCode(500, ApiResponse<object>.ErrorResponse("An internal server error occurred."));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
[HttpGet("regularize")]
|
[HttpGet("regularize")]
|
||||||
public async Task<IActionResult> GetRequestRegularizeAttendance([FromQuery] Guid projectId, [FromQuery] Guid? organizationId, [FromQuery] bool IncludeInActive)
|
public async Task<IActionResult> GetRequestRegularizeAttendance([FromQuery] Guid? projectId, [FromQuery] Guid? organizationId, [FromQuery] bool IncludeInActive)
|
||||||
{
|
{
|
||||||
Guid TenantId = GetTenantId();
|
|
||||||
Employee LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
|
Employee LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
|
||||||
var result = new List<EmployeeAttendanceVM>();
|
var result = new List<EmployeeAttendanceVM>();
|
||||||
var hasProjectPermission = await _permission.HasProjectPermission(LoggedInEmployee, projectId);
|
|
||||||
|
|
||||||
if (!hasProjectPermission)
|
var lstAttendanceQuery = _context.Attendes
|
||||||
|
.Include(a => a.RequestedBy)
|
||||||
|
.ThenInclude(e => e!.JobRole)
|
||||||
|
.Include(a => a.Employee)
|
||||||
|
.ThenInclude(e => e!.Organization)
|
||||||
|
.Include(a => a.Employee)
|
||||||
|
.ThenInclude(e => e!.JobRole)
|
||||||
|
|
||||||
|
.Where(c => c.Activity == ATTENDANCE_MARK_TYPE.REQUEST_REGULARIZE && c.Employee != null && c.Employee.JobRole != null && c.TenantId == tenantId);
|
||||||
|
|
||||||
|
if (organizationId.HasValue)
|
||||||
{
|
{
|
||||||
_logger.LogWarning("Employee {EmployeeId} tries to access attendance of project {ProjectId}, but don't have access", LoggedInEmployee.Id, projectId);
|
lstAttendanceQuery = lstAttendanceQuery.Where(a => a.Employee != null && a.Employee.OrganizationId == organizationId);
|
||||||
return Unauthorized(ApiResponse<object>.ErrorResponse("Unauthorized access", "Unauthorized access", 404));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
List<Attendance> lstAttendance = await _context.Attendes.Where(c => c.ProjectID == projectId && c.Activity == ATTENDANCE_MARK_TYPE.REQUEST_REGULARIZE && c.TenantId == TenantId).ToListAsync();
|
if (projectId.HasValue)
|
||||||
|
{
|
||||||
|
lstAttendanceQuery = lstAttendanceQuery.Where(a => a.ProjectID == projectId);
|
||||||
|
}
|
||||||
|
|
||||||
List<ProjectAllocation> projectteam = await _projectServices.GetTeamByProject(TenantId, projectId, organizationId, true);
|
List<Attendance> lstAttendance = await lstAttendanceQuery.ToListAsync();
|
||||||
var idList = projectteam.Select(p => p.EmployeeId).ToList();
|
|
||||||
var jobRole = await _context.JobRoles.ToListAsync();
|
var projectIds = lstAttendance.Select(a => a.ProjectID).ToList();
|
||||||
|
var projects = await _context.Projects.Where(p => projectIds.Contains(p.Id) && p.TenantId == tenantId).ToListAsync();
|
||||||
|
|
||||||
foreach (Attendance attende in lstAttendance)
|
foreach (Attendance attende in lstAttendance)
|
||||||
{
|
|
||||||
var teamMember = projectteam.Find(m => m.EmployeeId == attende.EmployeeId);
|
|
||||||
if (teamMember != null && teamMember.Employee != null && teamMember.Employee.JobRole != null)
|
|
||||||
{
|
{
|
||||||
var result1 = new EmployeeAttendanceVM()
|
var result1 = new EmployeeAttendanceVM()
|
||||||
{
|
{
|
||||||
@ -383,13 +382,16 @@ namespace MarcoBMS.Services.Controllers
|
|||||||
Activity = attende.Activity,
|
Activity = attende.Activity,
|
||||||
EmployeeAvatar = null,
|
EmployeeAvatar = null,
|
||||||
EmployeeId = attende.EmployeeId,
|
EmployeeId = attende.EmployeeId,
|
||||||
FirstName = teamMember.Employee.FirstName,
|
ProjectId = attende.ProjectID,
|
||||||
LastName = teamMember.Employee.LastName,
|
FirstName = attende.Employee?.FirstName,
|
||||||
JobRoleName = teamMember.Employee.JobRole.Name,
|
ProjectName = projects.Where(p => p.Id == attende.ProjectID).Select(p => p.Name).FirstOrDefault(),
|
||||||
OrganizationName = teamMember.Employee.Organization?.Name
|
LastName = attende.Employee?.LastName,
|
||||||
|
JobRoleName = attende.Employee?.JobRole?.Name,
|
||||||
|
OrganizationName = attende.Employee?.Organization?.Name,
|
||||||
|
RequestedAt = attende.RequestedAt,
|
||||||
|
RequestedBy = _mapper.Map<BasicEmployeeVM>(attende.RequestedBy)
|
||||||
};
|
};
|
||||||
result.Add(result1);
|
result.Add(result1);
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -416,13 +418,16 @@ namespace MarcoBMS.Services.Controllers
|
|||||||
return BadRequest(ApiResponse<object>.ErrorResponse("Invalid data", errors, 400));
|
return BadRequest(ApiResponse<object>.ErrorResponse("Invalid data", errors, 400));
|
||||||
}
|
}
|
||||||
|
|
||||||
Guid TenantId = GetTenantId();
|
using var scope = _serviceScopeFactory.CreateScope();
|
||||||
|
var _signalR = scope.ServiceProvider.GetRequiredService<IHubContext<MarcoHub>>();
|
||||||
|
var _employeeHelper = scope.ServiceProvider.GetRequiredService<EmployeeHelper>();
|
||||||
|
|
||||||
var currentEmployee = await _userHelper.GetCurrentEmployeeAsync();
|
var currentEmployee = await _userHelper.GetCurrentEmployeeAsync();
|
||||||
|
|
||||||
using var transaction = await _context.Database.BeginTransactionAsync();
|
using var transaction = await _context.Database.BeginTransactionAsync();
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
Attendance? attendance = await _context.Attendes.FirstOrDefaultAsync(a => a.Id == recordAttendanceDot.Id && a.TenantId == TenantId); ;
|
Attendance? attendance = await _context.Attendes.FirstOrDefaultAsync(a => a.Id == recordAttendanceDot.Id && a.TenantId == tenantId); ;
|
||||||
|
|
||||||
if (recordAttendanceDot.MarkTime == null)
|
if (recordAttendanceDot.MarkTime == null)
|
||||||
{
|
{
|
||||||
@ -460,10 +465,12 @@ namespace MarcoBMS.Services.Controllers
|
|||||||
{
|
{
|
||||||
DateTime date = attendance.AttendanceDate;
|
DateTime date = attendance.AttendanceDate;
|
||||||
finalDateTime = GetDateFromTimeStamp(date.Date, recordAttendanceDot.MarkTime);
|
finalDateTime = GetDateFromTimeStamp(date.Date, recordAttendanceDot.MarkTime);
|
||||||
if (attendance.InTime < finalDateTime)
|
if (attendance.InTime <= finalDateTime)
|
||||||
{
|
{
|
||||||
attendance.OutTime = finalDateTime;
|
attendance.OutTime = finalDateTime;
|
||||||
attendance.Activity = ATTENDANCE_MARK_TYPE.REQUEST_REGULARIZE;
|
attendance.Activity = ATTENDANCE_MARK_TYPE.REQUEST_REGULARIZE;
|
||||||
|
attendance.RequestedAt = DateTime.UtcNow;
|
||||||
|
attendance.RequestedById = currentEmployee.Id;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@ -477,12 +484,15 @@ namespace MarcoBMS.Services.Controllers
|
|||||||
attendance.IsApproved = true;
|
attendance.IsApproved = true;
|
||||||
attendance.Activity = ATTENDANCE_MARK_TYPE.REGULARIZE;
|
attendance.Activity = ATTENDANCE_MARK_TYPE.REGULARIZE;
|
||||||
attendance.ApprovedById = currentEmployee.Id;
|
attendance.ApprovedById = currentEmployee.Id;
|
||||||
|
attendance.ApprovedAt = DateTime.UtcNow;
|
||||||
// do nothing
|
// do nothing
|
||||||
}
|
}
|
||||||
else if (recordAttendanceDot.Action == ATTENDANCE_MARK_TYPE.REGULARIZE_REJECT)
|
else if (recordAttendanceDot.Action == ATTENDANCE_MARK_TYPE.REGULARIZE_REJECT)
|
||||||
{
|
{
|
||||||
attendance.IsApproved = false;
|
attendance.IsApproved = false;
|
||||||
attendance.Activity = ATTENDANCE_MARK_TYPE.REGULARIZE_REJECT;
|
attendance.Activity = ATTENDANCE_MARK_TYPE.REGULARIZE_REJECT;
|
||||||
|
attendance.ApprovedById = currentEmployee.Id;
|
||||||
|
attendance.ApprovedAt = DateTime.UtcNow;
|
||||||
// do nothing
|
// do nothing
|
||||||
}
|
}
|
||||||
attendance.Date = DateTime.UtcNow;
|
attendance.Date = DateTime.UtcNow;
|
||||||
@ -493,7 +503,7 @@ namespace MarcoBMS.Services.Controllers
|
|||||||
else
|
else
|
||||||
{
|
{
|
||||||
attendance = new Attendance();
|
attendance = new Attendance();
|
||||||
attendance.TenantId = TenantId;
|
attendance.TenantId = tenantId;
|
||||||
attendance.AttendanceDate = recordAttendanceDot.Date;
|
attendance.AttendanceDate = recordAttendanceDot.Date;
|
||||||
// attendance.Activity = recordAttendanceDot.Action;
|
// attendance.Activity = recordAttendanceDot.Action;
|
||||||
attendance.Comment = recordAttendanceDot.Comment;
|
attendance.Comment = recordAttendanceDot.Comment;
|
||||||
@ -525,7 +535,7 @@ namespace MarcoBMS.Services.Controllers
|
|||||||
Latitude = recordAttendanceDot.Latitude,
|
Latitude = recordAttendanceDot.Latitude,
|
||||||
Longitude = recordAttendanceDot.Longitude,
|
Longitude = recordAttendanceDot.Longitude,
|
||||||
|
|
||||||
TenantId = TenantId,
|
TenantId = tenantId,
|
||||||
UpdatedBy = currentEmployee.Id,
|
UpdatedBy = currentEmployee.Id,
|
||||||
UpdatedOn = recordAttendanceDot.Date
|
UpdatedOn = recordAttendanceDot.Date
|
||||||
};
|
};
|
||||||
@ -549,6 +559,7 @@ namespace MarcoBMS.Services.Controllers
|
|||||||
CheckOutTime = attendance.OutTime,
|
CheckOutTime = attendance.OutTime,
|
||||||
EmployeeAvatar = null,
|
EmployeeAvatar = null,
|
||||||
EmployeeId = recordAttendanceDot.EmployeeID,
|
EmployeeId = recordAttendanceDot.EmployeeID,
|
||||||
|
ProjectId = attendance.ProjectID,
|
||||||
FirstName = employee.FirstName,
|
FirstName = employee.FirstName,
|
||||||
LastName = employee.LastName,
|
LastName = employee.LastName,
|
||||||
Id = attendance.Id,
|
Id = attendance.Id,
|
||||||
@ -569,10 +580,10 @@ namespace MarcoBMS.Services.Controllers
|
|||||||
// --- Push Notification Section ---
|
// --- Push Notification Section ---
|
||||||
// This section attempts to send a test push notification to the user's device.
|
// This section attempts to send a test push notification to the user's device.
|
||||||
// It's designed to fail gracefully and handle invalid Firebase Cloud Messaging (FCM) tokens.
|
// It's designed to fail gracefully and handle invalid Firebase Cloud Messaging (FCM) tokens.
|
||||||
|
var _firebase = scope.ServiceProvider.GetRequiredService<IFirebaseService>();
|
||||||
var name = $"{vm.FirstName} {vm.LastName}";
|
var name = $"{vm.FirstName} {vm.LastName}";
|
||||||
|
|
||||||
await _firebase.SendAttendanceMessageAsync(attendance.ProjectID, name, recordAttendanceDot.Action, attendance.EmployeeId, TenantId);
|
await _firebase.SendAttendanceMessageAsync(attendance.ProjectID, name, recordAttendanceDot.Action, attendance.EmployeeId, tenantId);
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -608,7 +619,12 @@ namespace MarcoBMS.Services.Controllers
|
|||||||
return BadRequest(ApiResponse<object>.ErrorResponse("Invalid data", errors, 400));
|
return BadRequest(ApiResponse<object>.ErrorResponse("Invalid data", errors, 400));
|
||||||
}
|
}
|
||||||
|
|
||||||
Guid tenantId = GetTenantId();
|
using var scope = _serviceScopeFactory.CreateScope();
|
||||||
|
var _s3Service = scope.ServiceProvider.GetRequiredService<S3UploadService>();
|
||||||
|
var _employeeHelper = scope.ServiceProvider.GetRequiredService<EmployeeHelper>();
|
||||||
|
var _firebase = scope.ServiceProvider.GetRequiredService<IFirebaseService>();
|
||||||
|
var _signalR = scope.ServiceProvider.GetRequiredService<IHubContext<MarcoHub>>();
|
||||||
|
|
||||||
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
|
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
|
||||||
var batchId = Guid.NewGuid();
|
var batchId = Guid.NewGuid();
|
||||||
|
|
||||||
@ -672,6 +688,8 @@ namespace MarcoBMS.Services.Controllers
|
|||||||
{
|
{
|
||||||
attendance.OutTime = finalDateTime;
|
attendance.OutTime = finalDateTime;
|
||||||
attendance.Activity = ATTENDANCE_MARK_TYPE.REQUEST_REGULARIZE;
|
attendance.Activity = ATTENDANCE_MARK_TYPE.REQUEST_REGULARIZE;
|
||||||
|
attendance.RequestedAt = DateTime.UtcNow;
|
||||||
|
attendance.RequestedById = loggedInEmployee.Id;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@ -682,10 +700,14 @@ namespace MarcoBMS.Services.Controllers
|
|||||||
case ATTENDANCE_MARK_TYPE.REGULARIZE:
|
case ATTENDANCE_MARK_TYPE.REGULARIZE:
|
||||||
attendance.IsApproved = true;
|
attendance.IsApproved = true;
|
||||||
attendance.Activity = ATTENDANCE_MARK_TYPE.REGULARIZE;
|
attendance.Activity = ATTENDANCE_MARK_TYPE.REGULARIZE;
|
||||||
|
attendance.ApprovedAt = DateTime.UtcNow;
|
||||||
|
attendance.ApprovedById = loggedInEmployee.Id;
|
||||||
break;
|
break;
|
||||||
case ATTENDANCE_MARK_TYPE.REGULARIZE_REJECT:
|
case ATTENDANCE_MARK_TYPE.REGULARIZE_REJECT:
|
||||||
attendance.IsApproved = false;
|
attendance.IsApproved = false;
|
||||||
attendance.Activity = ATTENDANCE_MARK_TYPE.REGULARIZE_REJECT;
|
attendance.Activity = ATTENDANCE_MARK_TYPE.REGULARIZE_REJECT;
|
||||||
|
attendance.ApprovedAt = DateTime.UtcNow;
|
||||||
|
attendance.ApprovedById = loggedInEmployee.Id;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -751,6 +773,7 @@ namespace MarcoBMS.Services.Controllers
|
|||||||
{
|
{
|
||||||
Id = attendance.Id,
|
Id = attendance.Id,
|
||||||
EmployeeId = employee.Id,
|
EmployeeId = employee.Id,
|
||||||
|
ProjectId = attendance.ProjectID,
|
||||||
FirstName = employee.FirstName,
|
FirstName = employee.FirstName,
|
||||||
LastName = employee.LastName,
|
LastName = employee.LastName,
|
||||||
CheckInTime = attendance.InTime,
|
CheckInTime = attendance.InTime,
|
||||||
@ -813,56 +836,54 @@ namespace MarcoBMS.Services.Controllers
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Fetches attendance for an entire project team using a single, optimized database query.
|
/// Fetches attendance for an entire project team using a single, optimized database query.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private async Task<List<EmployeeAttendanceVM>> GetTeamAttendanceAsync(Guid tenantId, Guid projectId, Guid? organizationId, DateTime forDate, bool includeInactive)
|
private async Task<List<EmployeeAttendanceVM>> GetTeamAttendanceAsync(Guid tenantId, Guid? projectId, Guid organizationId, DateTime forDate, bool includeInactive)
|
||||||
{
|
{
|
||||||
// This single query joins ProjectAllocations with Employees and performs a LEFT JOIN with Attendances.
|
// This single query joins ProjectAllocations with Employees and performs a LEFT JOIN with Attendances.
|
||||||
// This is far more efficient than fetching collections and joining them in memory.
|
// This is far more efficient than fetching collections and joining them in memory.
|
||||||
var query = _context.ProjectAllocations
|
var query = _context.Employees
|
||||||
.Include(pa => pa.Employee)
|
.Include(e => e!.Organization)
|
||||||
.ThenInclude(e => e!.Organization)
|
.Include(e => e!.JobRole)
|
||||||
.Include(pa => pa.Employee)
|
.Where(e => e.OrganizationId == organizationId && e.Organization != null && e.JobRole != null && e.IsActive);
|
||||||
.ThenInclude(e => e!.JobRole)
|
|
||||||
.Where(pa => pa.TenantId == tenantId && pa.ProjectId == projectId);
|
|
||||||
|
|
||||||
// Apply filters based on optional parameters
|
|
||||||
if (!includeInactive)
|
var lstAttendanceQuery = _context.Attendes.Where(c => c.AttendanceDate.Date == forDate && c.TenantId == tenantId);
|
||||||
|
|
||||||
|
if (projectId.HasValue)
|
||||||
{
|
{
|
||||||
query = query.Where(pa => pa.IsActive);
|
lstAttendanceQuery = lstAttendanceQuery.Where(a => a.ProjectID == projectId);
|
||||||
}
|
|
||||||
if (organizationId.HasValue)
|
|
||||||
{
|
|
||||||
query = query.Where(pa => pa.Employee != null && pa.Employee.OrganizationId == organizationId);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
List<Attendance> lstAttendance = await _context.Attendes.Where(c => c.ProjectID == projectId && c.AttendanceDate.Date == forDate && c.TenantId == tenantId).ToListAsync();
|
List<Attendance> lstAttendance = await lstAttendanceQuery.ToListAsync();
|
||||||
|
|
||||||
var teamAttendance = await query
|
var employees = await query
|
||||||
.AsNoTracking()
|
.AsNoTracking()
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
|
|
||||||
var response = teamAttendance
|
var projectIds = lstAttendance.Select(a => a.ProjectID).ToList();
|
||||||
.Select(teamMember =>
|
var projects = await _context.Projects.Where(p => projectIds.Contains(p.Id) && p.TenantId == tenantId).ToListAsync();
|
||||||
|
|
||||||
|
var response = employees
|
||||||
|
.Select(employee =>
|
||||||
{
|
{
|
||||||
var result1 = new EmployeeAttendanceVM()
|
var result1 = new EmployeeAttendanceVM()
|
||||||
{
|
{
|
||||||
EmployeeAvatar = null,
|
EmployeeAvatar = null,
|
||||||
EmployeeId = teamMember.EmployeeId,
|
EmployeeId = employee.Id,
|
||||||
FirstName = teamMember.Employee?.FirstName,
|
FirstName = employee.FirstName,
|
||||||
LastName = teamMember.Employee?.LastName,
|
LastName = employee.LastName,
|
||||||
OrganizationName = teamMember.Employee?.Organization?.Name,
|
OrganizationName = employee.Organization!.Name,
|
||||||
JobRoleName = teamMember.Employee?.JobRole?.Name,
|
JobRoleName = employee.JobRole!.Name,
|
||||||
};
|
};
|
||||||
|
|
||||||
//var member = emp.Where(e => e.Id == teamMember.EmployeeId);
|
var attendance = lstAttendance.Find(x => x.EmployeeId == employee.Id) ?? new Attendance();
|
||||||
|
|
||||||
|
|
||||||
var attendance = lstAttendance.Find(x => x.EmployeeId == teamMember.EmployeeId) ?? new Attendance();
|
|
||||||
if (attendance != null)
|
if (attendance != null)
|
||||||
{
|
{
|
||||||
result1.Id = attendance.Id;
|
result1.Id = attendance.Id;
|
||||||
|
result1.ProjectId = attendance.ProjectID;
|
||||||
result1.CheckInTime = attendance.InTime;
|
result1.CheckInTime = attendance.InTime;
|
||||||
result1.CheckOutTime = attendance.OutTime;
|
result1.CheckOutTime = attendance.OutTime;
|
||||||
result1.Activity = attendance.Activity;
|
result1.Activity = attendance.Activity;
|
||||||
|
result1.ProjectName = projects.Where(p => p.Id == attendance.ProjectID).Select(p => p.Name).FirstOrDefault();
|
||||||
}
|
}
|
||||||
return result1;
|
return result1;
|
||||||
})
|
})
|
||||||
@ -875,43 +896,51 @@ namespace MarcoBMS.Services.Controllers
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Fetches a single attendance record for the logged-in employee.
|
/// Fetches a single attendance record for the logged-in employee.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private async Task<List<EmployeeAttendanceVM>> GetSelfAttendanceAsync(Guid tenantId, Guid projectId, Guid employeeId, Guid? organizationId, DateTime forDate)
|
private async Task<List<EmployeeAttendanceVM>> GetSelfAttendanceAsync(Guid tenantId, Guid? projectId, Guid employeeId, DateTime forDate)
|
||||||
{
|
{
|
||||||
List<EmployeeAttendanceVM> result = new List<EmployeeAttendanceVM>();
|
List<EmployeeAttendanceVM> result = new List<EmployeeAttendanceVM>();
|
||||||
|
|
||||||
// This query fetches the employee's project allocation and their attendance in a single trip.
|
// This query fetches the employee's project allocation and their attendance in a single trip.
|
||||||
Attendance lstAttendance = await _context.Attendes
|
var lstAttendanceQuery = _context.Attendes
|
||||||
.FirstOrDefaultAsync(c => c.ProjectID == projectId && c.EmployeeId == employeeId && c.AttendanceDate.Date == forDate && c.TenantId == tenantId) ?? new Attendance();
|
.Where(c => c.EmployeeId == employeeId && c.AttendanceDate.Date == forDate && c.TenantId == tenantId);
|
||||||
|
|
||||||
var projectAllocationQuery = _context.ProjectAllocations
|
if (projectId.HasValue)
|
||||||
.Include(pa => pa.Employee)
|
|
||||||
.ThenInclude(e => e!.Organization)
|
|
||||||
.Where(pa => pa.ProjectId == projectId && pa.EmployeeId == employeeId && pa.TenantId == tenantId && pa.IsActive);
|
|
||||||
|
|
||||||
if (organizationId.HasValue)
|
|
||||||
{
|
{
|
||||||
projectAllocationQuery = projectAllocationQuery.Where(pa => pa.Employee != null && pa.Employee.OrganizationId == organizationId);
|
lstAttendanceQuery = lstAttendanceQuery.Where(a => a.ProjectID == projectId);
|
||||||
}
|
}
|
||||||
|
|
||||||
var projectAllocation = await projectAllocationQuery.FirstOrDefaultAsync();
|
List<Attendance> lstAttendances = await lstAttendanceQuery.ToListAsync() ?? new List<Attendance>();
|
||||||
|
|
||||||
if (projectAllocation != null)
|
var projectIds = lstAttendances.Select(a => a.ProjectID).ToList();
|
||||||
|
var projects = await _context.Projects.Where(p => projectIds.Contains(p.Id) && p.TenantId == tenantId).ToListAsync();
|
||||||
|
|
||||||
|
var employee = await _context.Employees
|
||||||
|
.Include(e => e.Organization)
|
||||||
|
.Include(e => e.JobRole)
|
||||||
|
.FirstOrDefaultAsync(e => e.Id == employeeId && e.IsActive);
|
||||||
|
|
||||||
|
if (employee != null && employee.JobRole != null && employee.Organization != null)
|
||||||
|
{
|
||||||
|
foreach (var lstAttendance in lstAttendances)
|
||||||
{
|
{
|
||||||
EmployeeAttendanceVM result1 = new EmployeeAttendanceVM
|
EmployeeAttendanceVM result1 = new EmployeeAttendanceVM
|
||||||
{
|
{
|
||||||
Id = lstAttendance.Id,
|
Id = lstAttendance.Id,
|
||||||
EmployeeAvatar = null,
|
EmployeeAvatar = null,
|
||||||
EmployeeId = projectAllocation.EmployeeId,
|
ProjectId = lstAttendance.ProjectID,
|
||||||
FirstName = projectAllocation.Employee?.FirstName,
|
EmployeeId = employee.Id,
|
||||||
OrganizationName = projectAllocation.Employee?.Organization?.Name,
|
FirstName = employee.FirstName,
|
||||||
LastName = projectAllocation.Employee?.LastName,
|
OrganizationName = employee.Organization.Name,
|
||||||
JobRoleName = projectAllocation.Employee?.JobRole?.Name,
|
ProjectName = projects.Where(p => p.Id == lstAttendance.ProjectID).Select(p => p.Name).FirstOrDefault(),
|
||||||
|
LastName = employee.LastName,
|
||||||
|
JobRoleName = employee.JobRole.Name,
|
||||||
CheckInTime = lstAttendance.InTime,
|
CheckInTime = lstAttendance.InTime,
|
||||||
CheckOutTime = lstAttendance.OutTime,
|
CheckOutTime = lstAttendance.OutTime,
|
||||||
Activity = lstAttendance.Activity
|
Activity = lstAttendance.Activity
|
||||||
};
|
};
|
||||||
result.Add(result1);
|
result.Add(result1);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
@ -59,7 +59,7 @@ namespace MarcoBMS.Services.Controllers
|
|||||||
{
|
{
|
||||||
|
|
||||||
var user = await _context.ApplicationUsers
|
var user = await _context.ApplicationUsers
|
||||||
.FirstOrDefaultAsync(u => u.Email == loginDto.Username || u.PhoneNumber == loginDto.Username);
|
.FirstOrDefaultAsync(u => u.Email == loginDto.Username || u.UserName == loginDto.Username);
|
||||||
|
|
||||||
if (user == null)
|
if (user == null)
|
||||||
{
|
{
|
||||||
@ -103,9 +103,13 @@ namespace MarcoBMS.Services.Controllers
|
|||||||
return NotFound(ApiResponse<object>.ErrorResponse("Username not found", "Username not found", 404));
|
return NotFound(ApiResponse<object>.ErrorResponse("Username not found", "Username not found", 404));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var tenants = await _context.Tenants.Where(t => t.OrganizationId == emp.OrganizationId).ToListAsync();
|
||||||
|
|
||||||
|
var tenant = tenants.OrderBy(t => t.OnBoardingDate).FirstOrDefault();
|
||||||
|
|
||||||
// Generate tokens
|
// Generate tokens
|
||||||
var token = _refreshTokenService.GenerateJwtToken(user.UserName, emp.TenantId ?? Guid.Empty, emp.OrganizationId, _jwtSettings);
|
var token = _refreshTokenService.GenerateJwtToken(user.UserName, tenant?.Id ?? Guid.Empty, emp.OrganizationId, _jwtSettings);
|
||||||
var refreshToken = await _refreshTokenService.CreateRefreshToken(user.Id, emp.TenantId.ToString(), emp.OrganizationId, _jwtSettings);
|
var refreshToken = await _refreshTokenService.CreateRefreshToken(user.Id, tenant?.Id.ToString(), emp.OrganizationId, _jwtSettings);
|
||||||
|
|
||||||
_logger.LogInfo("User login successful - UserId: {UserId}", user.Id);
|
_logger.LogInfo("User login successful - UserId: {UserId}", user.Id);
|
||||||
return Ok(ApiResponse<object>.SuccessResponse(new
|
return Ok(ApiResponse<object>.SuccessResponse(new
|
||||||
@ -201,12 +205,17 @@ namespace MarcoBMS.Services.Controllers
|
|||||||
}
|
}
|
||||||
_logger.LogInfo("Successfully found employee details for tenant ID: {TenantId}", emp.TenantId ?? Guid.Empty);
|
_logger.LogInfo("Successfully found employee details for tenant ID: {TenantId}", emp.TenantId ?? Guid.Empty);
|
||||||
|
|
||||||
|
|
||||||
|
var tenants = await _context.Tenants.Where(t => t.OrganizationId == emp.OrganizationId).ToListAsync();
|
||||||
|
|
||||||
|
var tenant = tenants.OrderBy(t => t.OnBoardingDate).FirstOrDefault();
|
||||||
|
|
||||||
// Generate JWT token
|
// Generate JWT token
|
||||||
var token = _refreshTokenService.GenerateJwtToken(user.UserName, emp.TenantId ?? Guid.Empty, emp.OrganizationId, _jwtSettings);
|
var token = _refreshTokenService.GenerateJwtToken(user.UserName, tenant?.Id ?? Guid.Empty, emp.OrganizationId, _jwtSettings);
|
||||||
|
|
||||||
// Generate a new refresh token and store it in the database.
|
// Generate a new refresh token and store it in the database.
|
||||||
_logger.LogInfo("Generating and storing Refresh Token for user: {Username}", user.UserName);
|
_logger.LogInfo("Generating and storing Refresh Token for user: {Username}", user.UserName);
|
||||||
var refreshToken = await _refreshTokenService.CreateRefreshToken(user.Id, emp.TenantId.ToString(), emp.OrganizationId, _jwtSettings);
|
var refreshToken = await _refreshTokenService.CreateRefreshToken(user.Id, tenant?.Id.ToString(), emp.OrganizationId, _jwtSettings);
|
||||||
|
|
||||||
// Fetch MPIN Token
|
// Fetch MPIN Token
|
||||||
var mpinToken = await _context.MPINDetails.FirstOrDefaultAsync(p => p.UserId == Guid.Parse(user.Id));
|
var mpinToken = await _context.MPINDetails.FirstOrDefaultAsync(p => p.UserId == Guid.Parse(user.Id));
|
||||||
@ -264,31 +273,32 @@ namespace MarcoBMS.Services.Controllers
|
|||||||
}
|
}
|
||||||
|
|
||||||
string? tokenType = claimsPrincipal.FindFirst("token_type")?.Value;
|
string? tokenType = claimsPrincipal.FindFirst("token_type")?.Value;
|
||||||
string? tokenTenantId = claimsPrincipal.FindFirst("TenantId")?.Value;
|
|
||||||
string? tokenUserId = claimsPrincipal.FindFirst(ClaimTypes.NameIdentifier)?.Value;
|
string? tokenUserId = claimsPrincipal.FindFirst(ClaimTypes.NameIdentifier)?.Value;
|
||||||
|
|
||||||
// Validate essential claims
|
// Validate essential claims
|
||||||
if (string.IsNullOrWhiteSpace(tokenType) || string.IsNullOrWhiteSpace(tokenTenantId) || string.IsNullOrWhiteSpace(tokenUserId))
|
if (string.IsNullOrWhiteSpace(tokenType) || string.IsNullOrWhiteSpace(tokenUserId))
|
||||||
{
|
{
|
||||||
_logger.LogWarning("MPIN token claims are incomplete");
|
_logger.LogWarning("MPIN token claims are incomplete");
|
||||||
return Unauthorized(ApiResponse<object>.ErrorResponse("Invalid token claims", "MPIN token does not match your identity", 401));
|
return Unauthorized(ApiResponse<object>.ErrorResponse("Invalid token claims", "MPIN token does not match your identity", 401));
|
||||||
}
|
}
|
||||||
|
|
||||||
Guid tenantId = Guid.Parse(tokenTenantId);
|
|
||||||
|
|
||||||
// Fetch employee by ID and tenant
|
// Fetch employee by ID and tenant
|
||||||
var requestEmployee = await _context.Employees
|
var requestEmployee = await _context.Employees
|
||||||
.Include(e => e.ApplicationUser)
|
.Include(e => e.ApplicationUser)
|
||||||
.FirstOrDefaultAsync(e => e.Id == verifyMPIN.EmployeeId && e.TenantId == tenantId && e.ApplicationUserId == tokenUserId && e.IsActive);
|
.FirstOrDefaultAsync(e => e.Id == verifyMPIN.EmployeeId && e.HasApplicationAccess && e.ApplicationUserId == tokenUserId && e.IsActive);
|
||||||
|
|
||||||
if (requestEmployee == null || string.IsNullOrWhiteSpace(requestEmployee.ApplicationUserId))
|
if (requestEmployee == null || string.IsNullOrWhiteSpace(requestEmployee.ApplicationUserId))
|
||||||
{
|
{
|
||||||
_logger.LogWarning("Employee not found or invalid for verification - EmployeeId: {EmployeeId}", verifyMPIN.EmployeeId);
|
_logger.LogWarning("Employee not found or invalid for verification - EmployeeId: {EmployeeId}", verifyMPIN.EmployeeId);
|
||||||
return BadRequest(ApiResponse<object>.ErrorResponse("Invalid request", "Provided invalid employee information", 400));
|
return BadRequest(ApiResponse<object>.ErrorResponse("Invalid request", "Provided invalid employee information", 400));
|
||||||
}
|
}
|
||||||
|
var tenants = await _context.Tenants.Where(t => t.OrganizationId == requestEmployee.OrganizationId).ToListAsync();
|
||||||
|
|
||||||
|
var tenant = tenants.OrderBy(t => t.OnBoardingDate).FirstOrDefault();
|
||||||
|
Guid tenantId = tenant?.Id ?? Guid.Empty;
|
||||||
|
|
||||||
// Validate that the token belongs to the same employee making the request
|
// Validate that the token belongs to the same employee making the request
|
||||||
if (requestEmployee.ApplicationUserId != tokenUserId || tokenType != "mpin")
|
if (requestEmployee.ApplicationUserId != tokenUserId || tokenType != "mpin" || tenantId == Guid.Empty)
|
||||||
{
|
{
|
||||||
_logger.LogWarning("Token identity does not match employee info - EmployeeId: {EmployeeId}", requestEmployee.Id);
|
_logger.LogWarning("Token identity does not match employee info - EmployeeId: {EmployeeId}", requestEmployee.Id);
|
||||||
return Unauthorized(ApiResponse<object>.ErrorResponse("Unauthorized", "MPIN token does not match your identity", 401));
|
return Unauthorized(ApiResponse<object>.ErrorResponse("Unauthorized", "MPIN token does not match your identity", 401));
|
||||||
@ -319,36 +329,6 @@ namespace MarcoBMS.Services.Controllers
|
|||||||
return Unauthorized(ApiResponse<object>.ErrorResponse("MPIN mismatch", "MPIN did not match", 401));
|
return Unauthorized(ApiResponse<object>.ErrorResponse("MPIN mismatch", "MPIN did not match", 401));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(verifyMPIN.FcmToken))
|
|
||||||
{
|
|
||||||
var existingFCMTokenMapping = await _context.FCMTokenMappings.Where(ft => ft.FcmToken == verifyMPIN.FcmToken).ToListAsync();
|
|
||||||
|
|
||||||
if (existingFCMTokenMapping.Any())
|
|
||||||
{
|
|
||||||
_context.FCMTokenMappings.RemoveRange(existingFCMTokenMapping);
|
|
||||||
}
|
|
||||||
|
|
||||||
var fcmTokenMapping = new FCMTokenMapping
|
|
||||||
{
|
|
||||||
EmployeeId = requestEmployee.Id,
|
|
||||||
FcmToken = verifyMPIN.FcmToken,
|
|
||||||
ExpiredAt = DateTime.UtcNow.AddDays(6),
|
|
||||||
TenantId = tenantId
|
|
||||||
};
|
|
||||||
_context.FCMTokenMappings.Add(fcmTokenMapping);
|
|
||||||
_logger.LogInfo("New FCM Token registering for employee {EmployeeId}", requestEmployee.Id);
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await _context.SaveChangesAsync();
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "Exception occured while saving FCM Token for employee {EmployeeId}", requestEmployee.Id);
|
|
||||||
return StatusCode(500, ApiResponse<object>.ErrorResponse("Internal Error", ex.Message, 500));
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate new tokens
|
// Generate new tokens
|
||||||
var jwtToken = _refreshTokenService.GenerateJwtToken(requestEmployee.Email, tenantId, requestEmployee.OrganizationId, _jwtSettings);
|
var jwtToken = _refreshTokenService.GenerateJwtToken(requestEmployee.Email, tenantId, requestEmployee.OrganizationId, _jwtSettings);
|
||||||
var refreshToken = await _refreshTokenService.CreateRefreshToken(requestEmployee.ApplicationUserId, tenantId.ToString(), requestEmployee.OrganizationId, _jwtSettings);
|
var refreshToken = await _refreshTokenService.CreateRefreshToken(requestEmployee.ApplicationUserId, tenantId.ToString(), requestEmployee.OrganizationId, _jwtSettings);
|
||||||
@ -431,7 +411,9 @@ namespace MarcoBMS.Services.Controllers
|
|||||||
//var accessToken = _refreshTokenService.GenerateJwtTokenWithOrganization(requestEmployee.ApplicationUser?.UserName, requestEmployee.OrganizationId, _jwtSettings);
|
//var accessToken = _refreshTokenService.GenerateJwtTokenWithOrganization(requestEmployee.ApplicationUser?.UserName, requestEmployee.OrganizationId, _jwtSettings);
|
||||||
//var refreshToken = await _refreshTokenService.CreateRefreshTokenWithOrganization(requestEmployee.ApplicationUserId, requestEmployee.OrganizationId, _jwtSettings);
|
//var refreshToken = await _refreshTokenService.CreateRefreshTokenWithOrganization(requestEmployee.ApplicationUserId, requestEmployee.OrganizationId, _jwtSettings);
|
||||||
|
|
||||||
var tenant = await _context.Tenants.FirstOrDefaultAsync(t => t.OrganizationId == requestEmployee.OrganizationId);
|
var tenants = await _context.Tenants.Where(t => t.OrganizationId == requestEmployee.OrganizationId).ToListAsync();
|
||||||
|
|
||||||
|
var tenant = tenants.OrderBy(t => t.OnBoardingDate).FirstOrDefault();
|
||||||
|
|
||||||
var accessToken = _refreshTokenService.GenerateJwtToken(requestEmployee.ApplicationUser?.UserName,
|
var accessToken = _refreshTokenService.GenerateJwtToken(requestEmployee.ApplicationUser?.UserName,
|
||||||
tenant?.Id ?? Guid.Empty, requestEmployee.OrganizationId, _jwtSettings);
|
tenant?.Id ?? Guid.Empty, requestEmployee.OrganizationId, _jwtSettings);
|
||||||
|
@ -2,6 +2,8 @@
|
|||||||
using Marco.Pms.Model.Activities;
|
using Marco.Pms.Model.Activities;
|
||||||
using Marco.Pms.Model.Dtos.Attendance;
|
using Marco.Pms.Model.Dtos.Attendance;
|
||||||
using Marco.Pms.Model.Employees;
|
using Marco.Pms.Model.Employees;
|
||||||
|
using Marco.Pms.Model.Entitlements;
|
||||||
|
using Marco.Pms.Model.Expenses;
|
||||||
using Marco.Pms.Model.Projects;
|
using Marco.Pms.Model.Projects;
|
||||||
using Marco.Pms.Model.Utilities;
|
using Marco.Pms.Model.Utilities;
|
||||||
using Marco.Pms.Model.ViewModels.DashBoard;
|
using Marco.Pms.Model.ViewModels.DashBoard;
|
||||||
@ -12,6 +14,7 @@ using MarcoBMS.Services.Service;
|
|||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using System.Globalization;
|
||||||
|
|
||||||
namespace Marco.Pms.Services.Controllers
|
namespace Marco.Pms.Services.Controllers
|
||||||
{
|
{
|
||||||
@ -25,19 +28,35 @@ namespace Marco.Pms.Services.Controllers
|
|||||||
private readonly IProjectServices _projectServices;
|
private readonly IProjectServices _projectServices;
|
||||||
private readonly ILoggingService _logger;
|
private readonly ILoggingService _logger;
|
||||||
private readonly PermissionServices _permissionServices;
|
private readonly PermissionServices _permissionServices;
|
||||||
|
private readonly IServiceScopeFactory _serviceScopeFactory;
|
||||||
public static readonly Guid ActiveId = Guid.Parse("b74da4c2-d07e-46f2-9919-e75e49b12731");
|
public static readonly Guid ActiveId = Guid.Parse("b74da4c2-d07e-46f2-9919-e75e49b12731");
|
||||||
public DashboardController(ApplicationDbContext context, UserHelper userHelper, IProjectServices projectServices, ILoggingService logger, PermissionServices permissionServices)
|
private static readonly Guid Draft = Guid.Parse("297e0d8f-f668-41b5-bfea-e03b354251c8");
|
||||||
|
private static readonly Guid Review = Guid.Parse("6537018f-f4e9-4cb3-a210-6c3b2da999d7");
|
||||||
|
private static readonly Guid Approve = Guid.Parse("4068007f-c92f-4f37-a907-bc15fe57d4d8");
|
||||||
|
private static readonly Guid ProcessPending = Guid.Parse("f18c5cfd-7815-4341-8da2-2c2d65778e27");
|
||||||
|
private static readonly Guid Processed = Guid.Parse("61578360-3a49-4c34-8604-7b35a3787b95");
|
||||||
|
private static readonly Guid RejectedByReviewer = Guid.Parse("965eda62-7907-4963-b4a1-657fb0b2724b");
|
||||||
|
private static readonly Guid RejectedByApprover = Guid.Parse("d1ee5eec-24b6-4364-8673-a8f859c60729");
|
||||||
|
private readonly Guid tenantId;
|
||||||
|
|
||||||
|
public DashboardController(ApplicationDbContext context,
|
||||||
|
UserHelper userHelper,
|
||||||
|
IProjectServices projectServices,
|
||||||
|
IServiceScopeFactory serviceScopeFactory,
|
||||||
|
ILoggingService logger,
|
||||||
|
PermissionServices permissionServices)
|
||||||
{
|
{
|
||||||
_context = context;
|
_context = context;
|
||||||
_userHelper = userHelper;
|
_userHelper = userHelper;
|
||||||
_projectServices = projectServices;
|
_projectServices = projectServices;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
|
_serviceScopeFactory = serviceScopeFactory;
|
||||||
_permissionServices = permissionServices;
|
_permissionServices = permissionServices;
|
||||||
|
tenantId = userHelper.GetTenantId();
|
||||||
}
|
}
|
||||||
[HttpGet("progression")]
|
[HttpGet("progression")]
|
||||||
public async Task<IActionResult> GetGraph([FromQuery] double days, [FromQuery] string FromDate, [FromQuery] Guid? projectId)
|
public async Task<IActionResult> GetGraph([FromQuery] double days, [FromQuery] string FromDate, [FromQuery] Guid? projectId)
|
||||||
{
|
{
|
||||||
var tenantId = _userHelper.GetTenantId();
|
|
||||||
var LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
|
var LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
|
||||||
|
|
||||||
DateTime fromDate = new DateTime();
|
DateTime fromDate = new DateTime();
|
||||||
@ -149,7 +168,6 @@ namespace Marco.Pms.Services.Controllers
|
|||||||
[HttpGet("projects")]
|
[HttpGet("projects")]
|
||||||
public async Task<IActionResult> GetProjectCount()
|
public async Task<IActionResult> GetProjectCount()
|
||||||
{
|
{
|
||||||
var tenantId = _userHelper.GetTenantId();
|
|
||||||
var LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
|
var LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
|
||||||
|
|
||||||
var projects = await _context.Projects.Where(p => p.TenantId == tenantId).ToListAsync();
|
var projects = await _context.Projects.Where(p => p.TenantId == tenantId).ToListAsync();
|
||||||
@ -176,7 +194,6 @@ namespace Marco.Pms.Services.Controllers
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var tenantId = _userHelper.GetTenantId();
|
|
||||||
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
|
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
|
||||||
|
|
||||||
_logger.LogInfo("GetTotalEmployees called by user {UserId} for ProjectId: {ProjectId}", loggedInEmployee.Id, projectId ?? Guid.Empty);
|
_logger.LogInfo("GetTotalEmployees called by user {UserId} for ProjectId: {ProjectId}", loggedInEmployee.Id, projectId ?? Guid.Empty);
|
||||||
@ -269,7 +286,6 @@ namespace Marco.Pms.Services.Controllers
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var tenantId = _userHelper.GetTenantId();
|
|
||||||
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
|
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
|
||||||
|
|
||||||
_logger.LogInfo("GetTotalTasks called by user {UserId} for ProjectId: {ProjectId}", loggedInEmployee.Id, projectId ?? Guid.Empty);
|
_logger.LogInfo("GetTotalTasks called by user {UserId} for ProjectId: {ProjectId}", loggedInEmployee.Id, projectId ?? Guid.Empty);
|
||||||
@ -348,10 +364,10 @@ namespace Marco.Pms.Services.Controllers
|
|||||||
return StatusCode(500, ApiResponse<object>.ErrorResponse("An internal server error occurred.", null, 500));
|
return StatusCode(500, ApiResponse<object>.ErrorResponse("An internal server error occurred.", null, 500));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("pending-attendance")]
|
[HttpGet("pending-attendance")]
|
||||||
public async Task<IActionResult> GetPendingAttendance()
|
public async Task<IActionResult> GetPendingAttendance()
|
||||||
{
|
{
|
||||||
Guid tenantId = _userHelper.GetTenantId();
|
|
||||||
var LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
|
var LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
|
||||||
|
|
||||||
var attendance = await _context.Attendes.Where(a => a.EmployeeId == LoggedInEmployee.Id && a.TenantId == tenantId).ToListAsync();
|
var attendance = await _context.Attendes.Where(a => a.EmployeeId == LoggedInEmployee.Id && a.TenantId == tenantId).ToListAsync();
|
||||||
@ -374,7 +390,6 @@ namespace Marco.Pms.Services.Controllers
|
|||||||
[HttpGet("project-attendance/{projectId}")]
|
[HttpGet("project-attendance/{projectId}")]
|
||||||
public async Task<IActionResult> GetProjectAttendance(Guid projectId, [FromQuery] string? date)
|
public async Task<IActionResult> GetProjectAttendance(Guid projectId, [FromQuery] string? date)
|
||||||
{
|
{
|
||||||
Guid tenantId = _userHelper.GetTenantId();
|
|
||||||
var LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
|
var LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
|
||||||
|
|
||||||
DateTime currentDate = DateTime.UtcNow;
|
DateTime currentDate = DateTime.UtcNow;
|
||||||
@ -428,7 +443,6 @@ namespace Marco.Pms.Services.Controllers
|
|||||||
[HttpGet("activities/{projectId}")]
|
[HttpGet("activities/{projectId}")]
|
||||||
public async Task<IActionResult> GetActivities(Guid projectId, [FromQuery] string? date)
|
public async Task<IActionResult> GetActivities(Guid projectId, [FromQuery] string? date)
|
||||||
{
|
{
|
||||||
Guid tenantId = _userHelper.GetTenantId();
|
|
||||||
var LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
|
var LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
|
||||||
|
|
||||||
DateTime currentDate = DateTime.UtcNow;
|
DateTime currentDate = DateTime.UtcNow;
|
||||||
@ -600,5 +614,317 @@ namespace Marco.Pms.Services.Controllers
|
|||||||
|
|
||||||
return Ok(ApiResponse<object>.SuccessResponse(sortedResult, $"{sortedResult.Count} records fetched for attendance overview", 200));
|
return Ok(ApiResponse<object>.SuccessResponse(sortedResult, $"{sortedResult.Count} records fetched for attendance overview", 200));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpGet("expense/monthly")]
|
||||||
|
public async Task<IActionResult> GetExpenseReportByProjectsAsync([FromQuery] Guid? projectId, [FromQuery] Guid? categoryId, [FromQuery] int months)
|
||||||
|
{
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Read-only base filter with tenant scope and non-draft
|
||||||
|
var baseQuery = _context.Expenses
|
||||||
|
.AsNoTracking()
|
||||||
|
.Where(e =>
|
||||||
|
e.TenantId == tenantId
|
||||||
|
&& e.IsActive
|
||||||
|
&& e.StatusId != Draft); // [Server Filters]
|
||||||
|
|
||||||
|
if (months != 0)
|
||||||
|
{
|
||||||
|
months = 0 - months;
|
||||||
|
var end = DateTime.UtcNow.Date;
|
||||||
|
var start = end.AddMonths(months); // inclusive EOD
|
||||||
|
baseQuery = baseQuery.Where(e => e.TransactionDate >= start
|
||||||
|
&& e.TransactionDate <= end);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (projectId.HasValue)
|
||||||
|
baseQuery = baseQuery.Where(e => e.ProjectId == projectId);
|
||||||
|
|
||||||
|
if (categoryId.HasValue)
|
||||||
|
baseQuery = baseQuery.Where(e => e.ExpensesTypeId == categoryId);
|
||||||
|
|
||||||
|
// Single server-side group/aggregate by project
|
||||||
|
var report = await baseQuery
|
||||||
|
.AsNoTracking()
|
||||||
|
.GroupBy(e => new { e.TransactionDate.Year, e.TransactionDate.Month })
|
||||||
|
.Select(g => new
|
||||||
|
{
|
||||||
|
Year = g.Key.Year,
|
||||||
|
Month = g.Key.Month,
|
||||||
|
Total = g.Sum(x => x.Amount),
|
||||||
|
Count = g.Count()
|
||||||
|
})
|
||||||
|
.OrderBy(x => x.Year).ThenBy(x => x.Month)
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
var culture = CultureInfo.GetCultureInfo("en-IN"); // pick desired locale
|
||||||
|
|
||||||
|
var response = report
|
||||||
|
.Select(x => new
|
||||||
|
{
|
||||||
|
MonthName = culture.DateTimeFormat.GetMonthName(x.Month), // e.g., "January"
|
||||||
|
Year = x.Year,
|
||||||
|
Total = x.Total,
|
||||||
|
Count = x.Count
|
||||||
|
}).ToList();
|
||||||
|
|
||||||
|
_logger.LogInfo(
|
||||||
|
"GetExpenseReportByProjects completed. TenantId={TenantId}, Rows={Rows}",
|
||||||
|
tenantId, report.Count); // [Completion Log]
|
||||||
|
|
||||||
|
return Ok(ApiResponse<object>.SuccessResponse(response, "Expense report by project fetched successfully", 200)); // [Success Response]
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
_logger.LogWarning("GetExpenseReportByProjects canceled by client. TenantId={TenantId}", tenantId); // [Cancel Log]
|
||||||
|
return StatusCode(499, ApiResponse<object>.ErrorResponse("Client has canceled the opration", "Client has canceled the opration", 499)); // [Cancel Response]
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex,
|
||||||
|
"GetExpenseReportByProjects failed. TenantId={TenantId}",
|
||||||
|
tenantId); // [Error Log]
|
||||||
|
return StatusCode(500,
|
||||||
|
ApiResponse<object>.ErrorResponse("An error occurred while fetching the expense report.", 500)); // [Error Response]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("expense/type")]
|
||||||
|
public async Task<IActionResult> GetExpenseReportByExpenseTypeAsync([FromQuery] Guid? projectId, [FromQuery] DateTime startDate, [FromQuery] DateTime endDate)
|
||||||
|
{
|
||||||
|
// Structured log: entering action with filters
|
||||||
|
_logger.LogDebug(
|
||||||
|
"GetExpenseReportByExpenseType started. TenantId={TenantId}, ProjectId={ProjectId}, StartDate={StartDate}, EndDate={EndDate}",
|
||||||
|
tenantId, projectId ?? Guid.Empty, startDate, endDate); // [Start Log] [memory:4][memory:1]
|
||||||
|
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Compose base query: push filters to DB, avoid client evaluation
|
||||||
|
IQueryable<Expenses> baseQuery = _context.Expenses
|
||||||
|
.AsNoTracking() // Reduce tracking overhead for read-only endpoint
|
||||||
|
.Where(e => e.TenantId == tenantId
|
||||||
|
&& e.IsActive
|
||||||
|
&& e.StatusId != Draft
|
||||||
|
&& e.TransactionDate >= startDate
|
||||||
|
&& e.TransactionDate <= endDate.AddDays(1).AddTicks(-1));
|
||||||
|
|
||||||
|
if (projectId.HasValue)
|
||||||
|
baseQuery = baseQuery.Where(e => e.ProjectId == projectId.Value); // [Filter] [memory:7]
|
||||||
|
|
||||||
|
// Project to a minimal shape before grouping to avoid loading navigation graphs
|
||||||
|
// Group by expense type name; adjust to the correct key if ExpensesCategory is an enum or navigation
|
||||||
|
var query = baseQuery
|
||||||
|
.Where(e => e.ExpensesType != null)
|
||||||
|
.Select(e => new
|
||||||
|
{
|
||||||
|
ExpenseTypeName = e.ExpensesType!.Name, // If enum, use e.ExpensesCategory.ToString()
|
||||||
|
Amount = e.Amount,
|
||||||
|
StatusId = e.StatusId
|
||||||
|
})
|
||||||
|
.GroupBy(x => x.ExpenseTypeName)
|
||||||
|
.Select(g => new
|
||||||
|
{
|
||||||
|
ProjectName = g.Key, // Original code used g.Key!.Name; here the grouping key is already a string
|
||||||
|
TotalApprovedAmount = g.Where(x => x.StatusId == Processed
|
||||||
|
|| x.StatusId == ProcessPending).Sum(x => x.Amount),
|
||||||
|
TotalPendingAmount = g.Where(x => x.StatusId != Processed
|
||||||
|
&& x.StatusId != RejectedByReviewer
|
||||||
|
&& x.StatusId != RejectedByApprover)
|
||||||
|
.Sum(x => x.Amount),
|
||||||
|
TotalRejectedAmount = g.Where(x => x.StatusId == RejectedByReviewer
|
||||||
|
|| x.StatusId == RejectedByApprover)
|
||||||
|
.Sum(x => x.Amount),
|
||||||
|
TotalProcessedAmount = g.Where(x => x.StatusId == Processed)
|
||||||
|
.Sum(x => x.Amount)
|
||||||
|
})
|
||||||
|
.OrderBy(r => r.ProjectName); // Server-side order [memory:7]
|
||||||
|
|
||||||
|
var report = await query.ToListAsync(); // Single round-trip [memory:7]
|
||||||
|
|
||||||
|
var response = new
|
||||||
|
{
|
||||||
|
Report = report,
|
||||||
|
TotalAmount = report.Sum(r => r.TotalApprovedAmount)
|
||||||
|
};
|
||||||
|
|
||||||
|
_logger.LogInfo(
|
||||||
|
"GetExpenseReportByExpenseType completed. TenantId={TenantId}, Filters: ProjectId={ProjectId}, StartDate={StartDate}, EndDate={EndDate}, Rows={RowCount}, TotalAmount={TotalAmount}",
|
||||||
|
tenantId, projectId ?? Guid.Empty, startDate, endDate, report.Count, response.TotalAmount); // [Completion Log] [memory:4]
|
||||||
|
|
||||||
|
return Ok(ApiResponse<object>.SuccessResponse(response, "Expense report by expense type fetched successfully", 200)); // [Success Response] [memory:1]
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
_logger.LogWarning("GetExpenseReportByExpenseType canceled by client. TenantId={TenantId}", tenantId); // [Cancel Log] [memory:4]
|
||||||
|
return StatusCode(499, ApiResponse<object>.ErrorResponse("Client has canceled the opration", "Client has canceled the opration", 499)); // [Cancel Response] [memory:1]
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex,
|
||||||
|
"GetExpenseReportByExpenseType failed. TenantId={TenantId}, ProjectId={ProjectId}, StartDate={StartDate}, EndDate={EndDate}",
|
||||||
|
tenantId, projectId ?? Guid.Empty, startDate, endDate); // [Error Log] [memory:4]
|
||||||
|
return StatusCode(StatusCodes.Status500InternalServerError,
|
||||||
|
ApiResponse<object>.ErrorResponse("An error occurred while fetching the expense report.", 500)); // [Error Response] [memory:1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("expense/pendings")]
|
||||||
|
public async Task<IActionResult> GetPendingExpenseListAsync([FromQuery] Guid? projectId)
|
||||||
|
{
|
||||||
|
// Start log with correlation fields
|
||||||
|
_logger.LogDebug(
|
||||||
|
"GetPendingExpenseListAsync started. Project={ProjectId} TenantId={TenantId}", projectId ?? Guid.Empty, tenantId); // [Start Log]
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Resolve current employee once; avoid using scoped services inside Task.Run
|
||||||
|
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); // [User Context]
|
||||||
|
|
||||||
|
// Resolve permission service from current scope once
|
||||||
|
using var scope = _serviceScopeFactory.CreateScope();
|
||||||
|
|
||||||
|
// Fire permission checks concurrently without Task.Run; these are async I/O methods
|
||||||
|
|
||||||
|
var hasReviewPermissionTask = Task.Run(async () =>
|
||||||
|
{
|
||||||
|
var _permission = scope.ServiceProvider.GetRequiredService<PermissionServices>();
|
||||||
|
return await _permission.HasPermission(PermissionsMaster.ExpenseReview, loggedInEmployee.Id);
|
||||||
|
});
|
||||||
|
|
||||||
|
var hasApprovePermissionTask = Task.Run(async () =>
|
||||||
|
{
|
||||||
|
var _permission = scope.ServiceProvider.GetRequiredService<PermissionServices>();
|
||||||
|
return await _permission.HasPermission(PermissionsMaster.ExpenseApprove, loggedInEmployee.Id);
|
||||||
|
});
|
||||||
|
|
||||||
|
var hasProcessPermissionTask = Task.Run(async () =>
|
||||||
|
{
|
||||||
|
var _permission = scope.ServiceProvider.GetRequiredService<PermissionServices>();
|
||||||
|
return await _permission.HasPermission(PermissionsMaster.ExpenseProcess, loggedInEmployee.Id);
|
||||||
|
});
|
||||||
|
|
||||||
|
var hasManagePermissionTask = Task.Run(async () =>
|
||||||
|
{
|
||||||
|
var _permission = scope.ServiceProvider.GetRequiredService<PermissionServices>();
|
||||||
|
return await _permission.HasPermission(PermissionsMaster.ExpenseManage, loggedInEmployee.Id);
|
||||||
|
});
|
||||||
|
|
||||||
|
await Task.WhenAll(hasReviewPermissionTask, hasApprovePermissionTask, hasProcessPermissionTask, hasManagePermissionTask); // [Parallel Await]
|
||||||
|
|
||||||
|
var hasReviewPermission = hasReviewPermissionTask.Result;
|
||||||
|
var hasApprovePermission = hasApprovePermissionTask.Result;
|
||||||
|
var hasProcessPermission = hasProcessPermissionTask.Result;
|
||||||
|
var hasManagePermission = hasManagePermissionTask.Result;
|
||||||
|
|
||||||
|
_logger.LogInfo(
|
||||||
|
"Permissions resolved: Review={Review}, Approve={Approve}, Process={Process}",
|
||||||
|
hasReviewPermission, hasApprovePermission, hasProcessPermission); // [Permissions Log]
|
||||||
|
|
||||||
|
// Build base query: read-only, tenant-scoped
|
||||||
|
var baseQuery = _context.Expenses
|
||||||
|
.Include(e => e.Status)
|
||||||
|
.AsNoTracking() // Reduce tracking overhead for read-only list
|
||||||
|
.Where(e => e.IsActive && e.TenantId == tenantId && e.StatusId != Processed && e.Status != null); // [Base Filter]
|
||||||
|
|
||||||
|
// Project to DTO in SQL to avoid heavy Include graph.
|
||||||
|
if (projectId.HasValue)
|
||||||
|
baseQuery = baseQuery.Where(e => e.ProjectId == projectId);
|
||||||
|
|
||||||
|
// Prefer ProjectTo when profiles exist; otherwise project minimal fields
|
||||||
|
var expenses = await baseQuery
|
||||||
|
.ToListAsync(); // Single round-trip; no Include needed for this shape
|
||||||
|
|
||||||
|
var draftExpenses = expenses.Where(e => e.StatusId == Draft && e.CreatedById == loggedInEmployee.Id).ToList();
|
||||||
|
var reviewExpenses = expenses.Where(e => (hasReviewPermission || e.CreatedById == loggedInEmployee.Id) && e.StatusId == Review).ToList();
|
||||||
|
var approveExpenses = expenses.Where(e => (hasApprovePermission || e.CreatedById == loggedInEmployee.Id) && e.StatusId == Approve).ToList();
|
||||||
|
var processPendingExpenses = expenses.Where(e => (hasProcessPermission || e.CreatedById == loggedInEmployee.Id) && e.StatusId == ProcessPending).ToList();
|
||||||
|
var submitedExpenses = expenses.Where(e => e.StatusId != Draft && e.CreatedById == loggedInEmployee.Id).ToList();
|
||||||
|
var totalAmount = expenses.Where(e => e.StatusId != Draft).Sum(e => e.Amount);
|
||||||
|
|
||||||
|
if (hasManagePermission)
|
||||||
|
{
|
||||||
|
var response = new
|
||||||
|
{
|
||||||
|
Draft = new
|
||||||
|
{
|
||||||
|
Count = draftExpenses.Count,
|
||||||
|
TotalAmount = draftExpenses.Sum(e => e.Amount)
|
||||||
|
},
|
||||||
|
ReviewPending = new
|
||||||
|
{
|
||||||
|
Count = reviewExpenses.Count,
|
||||||
|
TotalAmount = reviewExpenses.Sum(e => e.Amount)
|
||||||
|
},
|
||||||
|
ApprovePending = new
|
||||||
|
{
|
||||||
|
Count = approveExpenses.Count,
|
||||||
|
TotalAmount = approveExpenses.Sum(e => e.Amount)
|
||||||
|
},
|
||||||
|
ProcessPending = new
|
||||||
|
{
|
||||||
|
Count = processPendingExpenses.Count,
|
||||||
|
TotalAmount = processPendingExpenses.Sum(e => e.Amount)
|
||||||
|
},
|
||||||
|
Submited = new
|
||||||
|
{
|
||||||
|
Count = submitedExpenses.Count,
|
||||||
|
TotalAmount = submitedExpenses.Sum(e => e.Amount)
|
||||||
|
},
|
||||||
|
TotalAmount = totalAmount
|
||||||
|
};
|
||||||
|
_logger.LogInfo(
|
||||||
|
"GetPendingExpenseListAsync completed. TenantId={TenantId}",
|
||||||
|
tenantId); // [Completion Log]
|
||||||
|
|
||||||
|
return Ok(ApiResponse<object>.SuccessResponse(response, "Pending Expenses fetched successfully", 200)); // [Success Response]
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var response = new
|
||||||
|
{
|
||||||
|
Draft = new
|
||||||
|
{
|
||||||
|
Count = draftExpenses.Count
|
||||||
|
},
|
||||||
|
ReviewPending = new
|
||||||
|
{
|
||||||
|
Count = reviewExpenses.Count
|
||||||
|
},
|
||||||
|
ApprovePending = new
|
||||||
|
{
|
||||||
|
Count = approveExpenses.Count
|
||||||
|
},
|
||||||
|
ProcessPending = new
|
||||||
|
{
|
||||||
|
Count = processPendingExpenses.Count
|
||||||
|
},
|
||||||
|
Submited = new
|
||||||
|
{
|
||||||
|
Count = submitedExpenses.Count
|
||||||
|
},
|
||||||
|
TotalAmount = totalAmount
|
||||||
|
};
|
||||||
|
_logger.LogInfo(
|
||||||
|
"GetPendingExpenseListAsync completed. TenantId={TenantId}",
|
||||||
|
tenantId); // [Completion Log]
|
||||||
|
|
||||||
|
return Ok(ApiResponse<object>.SuccessResponse(response, "Pending Expenses fetched successfully", 200)); // [Success Response]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
_logger.LogWarning("GetPendingExpenseListAsync canceled by client. TenantId={TenantId}", tenantId); // [Cancel Log]
|
||||||
|
return StatusCode(499, ApiResponse<object>.ErrorResponse("Client has canceled the opration", "Client has canceled the opration", 499)); // [Cancel Response]
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "GetPendingExpenseListAsync failed. TenantId={TenantId}", tenantId); // [Error Log]
|
||||||
|
return StatusCode(500,
|
||||||
|
ApiResponse<object>.ErrorResponse("An error occurred while fetching pending expenses.", "An error occurred while fetching pending expenses.", 500)); // [Error Response]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -33,25 +33,25 @@ namespace Marco.Pms.Services.Controllers
|
|||||||
#region =================================================================== Contact Get APIs ===================================================================
|
#region =================================================================== Contact Get APIs ===================================================================
|
||||||
|
|
||||||
[HttpGet("list")]
|
[HttpGet("list")]
|
||||||
public async Task<IActionResult> GetContactList([FromQuery] string? search, [FromQuery] string? filter, [FromQuery] Guid? projectId, [FromQuery] bool active = true,
|
public async Task<IActionResult> GetContactList([FromQuery] string? searchString, [FromQuery] string? filter, [FromQuery] Guid? projectId, [FromQuery] bool active = true,
|
||||||
[FromQuery] int pageNumber = 1, [FromQuery] int pageSize = 20)
|
[FromQuery] int pageNumber = 1, [FromQuery] int pageSize = 20)
|
||||||
{
|
{
|
||||||
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
|
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
|
||||||
var response = await _directoryService.GetListOfContactsAsync(search: search, filter: filter, projectId: projectId, active: active, pageSize: pageSize, pageNumber: pageNumber, tenantId, loggedInEmployee);
|
var response = await _directoryService.GetListOfContactsAsync(search: searchString, filter: filter, projectId: projectId, active: active, pageSize: pageSize, pageNumber: pageNumber, tenantId, loggedInEmployee);
|
||||||
|
|
||||||
return StatusCode(response.StatusCode, response);
|
return StatusCode(response.StatusCode, response);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
public async Task<IActionResult> GetContactList([FromQuery] string? search, [FromQuery] List<Guid>? bucketIds, [FromQuery] List<Guid>? categoryIds, [FromQuery] Guid? projectId, [FromQuery] bool active = true)
|
public async Task<IActionResult> GetContactList([FromQuery] string? searchString, [FromQuery] List<Guid>? bucketIds, [FromQuery] List<Guid>? categoryIds, [FromQuery] Guid? projectId, [FromQuery] bool active = true)
|
||||||
{
|
{
|
||||||
ContactFilterDto filterDto = new ContactFilterDto
|
ContactFilterDto filterDto = new ContactFilterDto
|
||||||
{
|
{
|
||||||
BucketIds = bucketIds,
|
BucketIds = bucketIds,
|
||||||
CategoryIds = categoryIds
|
CategoryIds = categoryIds
|
||||||
};
|
};
|
||||||
var response = await _directoryService.GetListOfContactsOld(search, active, filterDto, projectId);
|
var response = await _directoryService.GetListOfContactsOld(searchString, active, filterDto, projectId);
|
||||||
|
|
||||||
|
|
||||||
return StatusCode(response.StatusCode, response);
|
return StatusCode(response.StatusCode, response);
|
||||||
|
@ -35,6 +35,7 @@ namespace Marco.Pms.Services.Controllers
|
|||||||
private readonly ILoggingService _logger;
|
private readonly ILoggingService _logger;
|
||||||
private readonly IMapper _mapper;
|
private readonly IMapper _mapper;
|
||||||
private readonly Guid tenantId;
|
private readonly Guid tenantId;
|
||||||
|
private readonly Guid organizationId;
|
||||||
|
|
||||||
private static readonly Guid ProjectEntity = Guid.Parse("c8fe7115-aa27-43bc-99f4-7b05fabe436e");
|
private static readonly Guid ProjectEntity = Guid.Parse("c8fe7115-aa27-43bc-99f4-7b05fabe436e");
|
||||||
private static readonly Guid EmployeeEntity = Guid.Parse("dbb9555a-7a0c-40f2-a9ed-f0463f1ceed7");
|
private static readonly Guid EmployeeEntity = Guid.Parse("dbb9555a-7a0c-40f2-a9ed-f0463f1ceed7");
|
||||||
@ -52,6 +53,7 @@ namespace Marco.Pms.Services.Controllers
|
|||||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||||
_mapper = mapper ?? throw new ArgumentNullException(nameof(mapper));
|
_mapper = mapper ?? throw new ArgumentNullException(nameof(mapper));
|
||||||
tenantId = userHelper.GetTenantId();
|
tenantId = userHelper.GetTenantId();
|
||||||
|
organizationId = _userHelper.GetCurrentOrganizationId();
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("list/{entityTypeId}/entity/{entityId}")]
|
[HttpGet("list/{entityTypeId}/entity/{entityId}")]
|
||||||
@ -93,21 +95,21 @@ namespace Marco.Pms.Services.Controllers
|
|||||||
return NotFound(ApiResponse<object>.ErrorResponse("Entity type not found", "Entity Type not found in database", 404));
|
return NotFound(ApiResponse<object>.ErrorResponse("Entity type not found", "Entity Type not found in database", 404));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Project permission check
|
//// Project permission check
|
||||||
if (ProjectEntity == entityTypeId)
|
//if (ProjectEntity == entityTypeId)
|
||||||
{
|
//{
|
||||||
var hasProjectPermission = await _permission.HasProjectPermission(loggedInEmployee, entityId);
|
// var hasProjectPermission = await _permission.HasProjectPermission(loggedInEmployee, entityId);
|
||||||
if (!hasProjectPermission)
|
// if (!hasProjectPermission)
|
||||||
{
|
// {
|
||||||
_logger.LogWarning("Employee {EmployeeId} does not have project access for ProjectId {ProjectId}", loggedInEmployee.Id, entityId);
|
// _logger.LogWarning("Employee {EmployeeId} does not have project access for ProjectId {ProjectId}", loggedInEmployee.Id, entityId);
|
||||||
return StatusCode(403, ApiResponse<object>.ErrorResponse("Access Denied.", "You do not have permission to access project documents", 403));
|
// return StatusCode(403, ApiResponse<object>.ErrorResponse("Access Denied.", "You do not have permission to access project documents", 403));
|
||||||
}
|
// }
|
||||||
}
|
//}
|
||||||
// Employee validation
|
// Employee validation
|
||||||
else if (EmployeeEntity == entityTypeId)
|
else if (EmployeeEntity == entityTypeId)
|
||||||
{
|
{
|
||||||
var isEmployeeExists = await _context.Employees
|
var isEmployeeExists = await _context.Employees
|
||||||
.AnyAsync(e => e.Id == entityId && e.TenantId == tenantId);
|
.AnyAsync(e => e.Id == entityId && e.OrganizationId == organizationId);
|
||||||
|
|
||||||
if (!isEmployeeExists)
|
if (!isEmployeeExists)
|
||||||
{
|
{
|
||||||
@ -691,7 +693,7 @@ namespace Marco.Pms.Services.Controllers
|
|||||||
bool entityExists = false;
|
bool entityExists = false;
|
||||||
if (entityType.Equals(EmployeeEntity))
|
if (entityType.Equals(EmployeeEntity))
|
||||||
{
|
{
|
||||||
entityExists = await _context.Employees.AnyAsync(e => e.Id == model.EntityId && e.TenantId == tenantId);
|
entityExists = await _context.Employees.AnyAsync(e => e.Id == model.EntityId && e.OrganizationId == organizationId);
|
||||||
}
|
}
|
||||||
else if (entityType.Equals(ProjectEntity))
|
else if (entityType.Equals(ProjectEntity))
|
||||||
{
|
{
|
||||||
@ -1078,15 +1080,15 @@ namespace Marco.Pms.Services.Controllers
|
|||||||
bool entityExists;
|
bool entityExists;
|
||||||
if (entityType.Equals(EmployeeEntity))
|
if (entityType.Equals(EmployeeEntity))
|
||||||
{
|
{
|
||||||
entityExists = await _context.Employees.AnyAsync(e => e.Id == oldAttachment.EntityId && e.TenantId == tenantId);
|
entityExists = await _context.Employees.AnyAsync(e => e.Id == oldAttachment.EntityId && e.OrganizationId == organizationId);
|
||||||
}
|
}
|
||||||
else if (entityType.Equals(ProjectEntity))
|
else if (entityType.Equals(ProjectEntity))
|
||||||
{
|
{
|
||||||
entityExists = await _context.Projects.AnyAsync(p => p.Id == oldAttachment.EntityId && p.TenantId == tenantId);
|
entityExists = await _context.Projects.AnyAsync(p => p.Id == oldAttachment.EntityId && p.TenantId == tenantId);
|
||||||
if (entityExists)
|
//if (entityExists)
|
||||||
{
|
//{
|
||||||
entityExists = await _permission.HasProjectPermission(loggedInEmployee, oldAttachment.EntityId);
|
// entityExists = await _permission.HasProjectPermission(loggedInEmployee, oldAttachment.EntityId);
|
||||||
}
|
//}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
@ -233,84 +233,12 @@ namespace MarcoBMS.Services.Controllers
|
|||||||
_logger.LogInfo("GetEmployeesByProject called. EmployeeId: {EmployeeId}, ProjectId: {ProjectId}, showInactive: {ShowInactive}",
|
_logger.LogInfo("GetEmployeesByProject called. EmployeeId: {EmployeeId}, ProjectId: {ProjectId}, showInactive: {ShowInactive}",
|
||||||
loggedInEmployee.Id, projectId ?? Guid.Empty, showInactive);
|
loggedInEmployee.Id, projectId ?? Guid.Empty, showInactive);
|
||||||
|
|
||||||
// Step 3: Fetch permissions concurrently
|
var employees = await _context.Employees
|
||||||
var viewAllTask = Task.Run(async () =>
|
|
||||||
{
|
|
||||||
var _permission = scope.ServiceProvider.GetRequiredService<PermissionServices>();
|
|
||||||
return await _permission.HasPermission(PermissionsMaster.ViewAllEmployees, loggedInEmployee.Id);
|
|
||||||
});
|
|
||||||
var viewTeamTask = Task.Run(async () =>
|
|
||||||
{
|
|
||||||
var _permission = scope.ServiceProvider.GetRequiredService<PermissionServices>();
|
|
||||||
return await _permission.HasPermission(PermissionsMaster.ViewTeamMembers, loggedInEmployee.Id);
|
|
||||||
});
|
|
||||||
|
|
||||||
await Task.WhenAll(viewAllTask, viewTeamTask);
|
|
||||||
|
|
||||||
var hasViewAllEmployeesPermission = viewAllTask.Result;
|
|
||||||
var hasViewTeamMembersPermission = viewTeamTask.Result;
|
|
||||||
|
|
||||||
List<Employee> employees = new List<Employee>();
|
|
||||||
|
|
||||||
// Step 4: Query based on permission
|
|
||||||
if (hasViewAllEmployeesPermission && !projectId.HasValue)
|
|
||||||
{
|
|
||||||
// OrganizationId needs to be retrieved from loggedInEmployee or context based on your app's structure
|
|
||||||
var employeeQuery = _context.Employees
|
|
||||||
.AsNoTracking() // Optimize EF query for read-only operation[web:1][web:13][web:18]
|
|
||||||
.Include(e => e.JobRole)
|
.Include(e => e.JobRole)
|
||||||
.Where(e => e.OrganizationId == organizationId);
|
.Include(e => e.Organization)
|
||||||
|
.Where(e => e.OrganizationId == loggedInEmployee.OrganizationId && e.IsActive != showInactive)
|
||||||
employeeQuery = showInactive
|
|
||||||
? employeeQuery.Where(e => !e.IsActive)
|
|
||||||
: employeeQuery.Where(e => e.IsActive);
|
|
||||||
|
|
||||||
employees = await employeeQuery.ToListAsync();
|
|
||||||
_logger.LogInfo("Employee list fetched with full access. Count: {Count}", employees.Count);
|
|
||||||
}
|
|
||||||
else if (hasViewTeamMembersPermission && !showInactive && !projectId.HasValue)
|
|
||||||
{
|
|
||||||
// Only active team members with limited permission
|
|
||||||
var projectIds = await _projectServices.GetMyProjectIdsAsync(tenantId, loggedInEmployee);
|
|
||||||
|
|
||||||
employees = await _context.ProjectAllocations
|
|
||||||
.AsNoTracking()
|
|
||||||
.Include(pa => pa.Employee)
|
|
||||||
.ThenInclude(e => e!.JobRole)
|
|
||||||
.Where(pa =>
|
|
||||||
projectIds.Contains(pa.ProjectId)
|
|
||||||
&& pa.IsActive
|
|
||||||
&& pa.Employee != null
|
|
||||||
&& pa.Employee.IsActive
|
|
||||||
&& pa.TenantId == tenantId)
|
|
||||||
.Select(pa => pa.Employee!)
|
|
||||||
.Distinct()
|
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
|
|
||||||
_logger.LogInfo("Employee list fetched with limited access (active only). Count: {Count}", employees.Count);
|
|
||||||
}
|
|
||||||
|
|
||||||
// If a specific projectId is provided, override employee fetching to ensure strict project context
|
|
||||||
if (projectId.HasValue)
|
|
||||||
{
|
|
||||||
employees = await _context.ProjectAllocations
|
|
||||||
.AsNoTracking()
|
|
||||||
.Include(pa => pa.Employee)
|
|
||||||
.ThenInclude(e => e!.JobRole)
|
|
||||||
.Where(pa =>
|
|
||||||
pa.ProjectId == projectId
|
|
||||||
&& pa.IsActive
|
|
||||||
&& pa.Employee != null
|
|
||||||
&& pa.Employee.IsActive
|
|
||||||
&& pa.TenantId == tenantId)
|
|
||||||
.Select(pa => pa.Employee!)
|
|
||||||
.Distinct()
|
|
||||||
.ToListAsync();
|
|
||||||
|
|
||||||
_logger.LogInfo("Employee list fetched for specific project. ProjectId: {ProjectId}. Count: {Count}",
|
|
||||||
projectId, employees.Count);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Step 5: Map to view model
|
// Step 5: Map to view model
|
||||||
result = employees.Select(e => _mapper.Map<EmployeeVM>(e)).Distinct().ToList();
|
result = employees.Select(e => _mapper.Map<EmployeeVM>(e)).Distinct().ToList();
|
||||||
|
|
||||||
@ -329,7 +257,7 @@ namespace MarcoBMS.Services.Controllers
|
|||||||
|
|
||||||
|
|
||||||
[HttpGet("basic")]
|
[HttpGet("basic")]
|
||||||
public async Task<IActionResult> GetEmployeesByProjectBasic(Guid? projectId, [FromQuery] string? searchString)
|
public async Task<IActionResult> GetEmployeesByProjectBasic(Guid? projectId, [FromQuery] string? searchString, [FromQuery] bool sendAll = false)
|
||||||
{
|
{
|
||||||
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
|
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
|
||||||
var employeeQuery = _context.Employees.Where(e => e.IsActive);
|
var employeeQuery = _context.Employees.Where(e => e.IsActive);
|
||||||
@ -353,8 +281,11 @@ namespace MarcoBMS.Services.Controllers
|
|||||||
var searchStringLower = searchString.ToLower();
|
var searchStringLower = searchString.ToLower();
|
||||||
employeeQuery = employeeQuery.Where(e => (e.FirstName + " " + e.LastName).ToLower().Contains(searchStringLower));
|
employeeQuery = employeeQuery.Where(e => (e.FirstName + " " + e.LastName).ToLower().Contains(searchStringLower));
|
||||||
}
|
}
|
||||||
|
if (!sendAll)
|
||||||
var response = await employeeQuery.Take(10).Select(e => _mapper.Map<BasicEmployeeVM>(e)).ToListAsync();
|
{
|
||||||
|
employeeQuery = employeeQuery.Take(10);
|
||||||
|
}
|
||||||
|
var response = await employeeQuery.Select(e => _mapper.Map<BasicEmployeeVM>(e)).ToListAsync();
|
||||||
return Ok(ApiResponse<object>.SuccessResponse(response, $"{response.Count} records of employees fetched successfully", 200));
|
return Ok(ApiResponse<object>.SuccessResponse(response, $"{response.Count} records of employees fetched successfully", 200));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -470,6 +401,9 @@ namespace MarcoBMS.Services.Controllers
|
|||||||
Guid tenantId = _userHelper.GetTenantId();
|
Guid tenantId = _userHelper.GetTenantId();
|
||||||
var LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
|
var LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
|
||||||
Guid employeeId = Guid.Empty;
|
Guid employeeId = Guid.Empty;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
if (model == null)
|
if (model == null)
|
||||||
return BadRequest(ApiResponse<object>.ErrorResponse("Invalid data", "Invaild Data", 400));
|
return BadRequest(ApiResponse<object>.ErrorResponse("Invalid data", "Invaild Data", 400));
|
||||||
|
|
||||||
@ -602,6 +536,7 @@ namespace MarcoBMS.Services.Controllers
|
|||||||
{
|
{
|
||||||
// Correlation and context capture for logs
|
// Correlation and context capture for logs
|
||||||
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
|
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
|
||||||
|
Guid organizationId = model.OrganizationId ?? loggedInEmployee.OrganizationId;
|
||||||
|
|
||||||
{
|
{
|
||||||
if (model == null)
|
if (model == null)
|
||||||
@ -625,10 +560,10 @@ namespace MarcoBMS.Services.Controllers
|
|||||||
if (model.Id.HasValue && model.Id.Value != Guid.Empty)
|
if (model.Id.HasValue && model.Id.Value != Guid.Empty)
|
||||||
{
|
{
|
||||||
existingEmployee = await _context.Employees
|
existingEmployee = await _context.Employees
|
||||||
.FirstOrDefaultAsync(e => e.Id == model.Id && e.OrganizationId == model.OrganizationId);
|
.FirstOrDefaultAsync(e => e.Id == model.Id && e.OrganizationId == organizationId);
|
||||||
if (existingEmployee == null)
|
if (existingEmployee == null)
|
||||||
{
|
{
|
||||||
_logger.LogInfo("Employee not found for update. Id={EmployeeId}, Org={OrgId}", model.Id, model.OrganizationId);
|
_logger.LogInfo("Employee not found for update. Id={EmployeeId}, Org={OrgId}", model.Id, organizationId);
|
||||||
return NotFound(ApiResponse<object>.ErrorResponse("Employee not found", "Employee not found in database", 404));
|
return NotFound(ApiResponse<object>.ErrorResponse("Employee not found", "Employee not found in database", 404));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -680,10 +615,10 @@ namespace MarcoBMS.Services.Controllers
|
|||||||
if (!string.IsNullOrWhiteSpace(model.Email))
|
if (!string.IsNullOrWhiteSpace(model.Email))
|
||||||
{
|
{
|
||||||
var emailExists = await _context.Employees
|
var emailExists = await _context.Employees
|
||||||
.AnyAsync(e => e.Email == model.Email && e.OrganizationId == model.OrganizationId);
|
.AnyAsync(e => e.Email == model.Email);
|
||||||
if (emailExists)
|
if (emailExists)
|
||||||
{
|
{
|
||||||
_logger.LogInfo("Employee email already exists in org. Email={Email}, Org={OrgId}", model.Email, model.OrganizationId);
|
_logger.LogInfo("Employee email already exists. Email={Email}", model.Email);
|
||||||
return StatusCode(403, ApiResponse<object>.ErrorResponse(
|
return StatusCode(403, ApiResponse<object>.ErrorResponse(
|
||||||
"Employee with email already exists",
|
"Employee with email already exists",
|
||||||
"Employee with this email already exists", 403));
|
"Employee with this email already exists", 403));
|
||||||
@ -723,7 +658,7 @@ namespace MarcoBMS.Services.Controllers
|
|||||||
existingEmployee.ApplicationUserId = createdIdentityUser.Id;
|
existingEmployee.ApplicationUserId = createdIdentityUser.Id;
|
||||||
await SendResetIfApplicableAsync(createdIdentityUser, existingEmployee.FirstName ?? "User");
|
await SendResetIfApplicableAsync(createdIdentityUser, existingEmployee.FirstName ?? "User");
|
||||||
}
|
}
|
||||||
|
existingEmployee.OrganizationId = organizationId;
|
||||||
await _context.SaveChangesAsync();
|
await _context.SaveChangesAsync();
|
||||||
|
|
||||||
employeeId = existingEmployee.Id;
|
employeeId = existingEmployee.Id;
|
||||||
@ -745,7 +680,7 @@ namespace MarcoBMS.Services.Controllers
|
|||||||
newEmployee.ApplicationUserId = createdIdentityUser.Id;
|
newEmployee.ApplicationUserId = createdIdentityUser.Id;
|
||||||
await SendResetIfApplicableAsync(createdIdentityUser, newEmployee.FirstName ?? "User");
|
await SendResetIfApplicableAsync(createdIdentityUser, newEmployee.FirstName ?? "User");
|
||||||
}
|
}
|
||||||
|
newEmployee.OrganizationId = organizationId;
|
||||||
await _context.Employees.AddAsync(newEmployee);
|
await _context.Employees.AddAsync(newEmployee);
|
||||||
await _context.SaveChangesAsync();
|
await _context.SaveChangesAsync();
|
||||||
|
|
||||||
@ -877,6 +812,7 @@ namespace MarcoBMS.Services.Controllers
|
|||||||
public async Task<IActionResult> CreateUserMobileAsync([FromBody] MobileUserManageDto model)
|
public async Task<IActionResult> CreateUserMobileAsync([FromBody] MobileUserManageDto model)
|
||||||
{
|
{
|
||||||
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
|
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
|
||||||
|
Guid organizationId = model.OrganizationId ?? loggedInEmployee.OrganizationId;
|
||||||
if (tenantId == Guid.Empty)
|
if (tenantId == Guid.Empty)
|
||||||
{
|
{
|
||||||
_logger.LogWarning("Tenant resolution failed in CreateUserMobile"); // structured warning
|
_logger.LogWarning("Tenant resolution failed in CreateUserMobile"); // structured warning
|
||||||
@ -912,11 +848,11 @@ namespace MarcoBMS.Services.Controllers
|
|||||||
if (model.Id == null || model.Id == Guid.Empty)
|
if (model.Id == null || model.Id == Guid.Empty)
|
||||||
{
|
{
|
||||||
var emailExists = await _context.Employees
|
var emailExists = await _context.Employees
|
||||||
.AnyAsync(e => e.Email == model.Email && e.OrganizationId == model.OrganizationId);
|
.AnyAsync(e => e.Email == model.Email);
|
||||||
|
|
||||||
if (emailExists)
|
if (emailExists)
|
||||||
{
|
{
|
||||||
_logger.LogInfo("Employee email already exists in org. Email={Email}, Org={OrgId}", model.Email ?? string.Empty, model.OrganizationId);
|
_logger.LogInfo("Employee email already exists in org. Email={Email}, Org={OrgId}", model.Email ?? string.Empty, organizationId);
|
||||||
return StatusCode(409, ApiResponse<object>.ErrorResponse("Employee with email already exists", "Employee with this email already exists", 409));
|
return StatusCode(409, ApiResponse<object>.ErrorResponse("Employee with email already exists", "Employee with this email already exists", 409));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -933,7 +869,7 @@ namespace MarcoBMS.Services.Controllers
|
|||||||
JoiningDate = model.JoiningDate,
|
JoiningDate = model.JoiningDate,
|
||||||
JobRoleId = model.JobRoleId,
|
JobRoleId = model.JobRoleId,
|
||||||
Photo = imageBytes,
|
Photo = imageBytes,
|
||||||
OrganizationId = model.OrganizationId,
|
OrganizationId = organizationId,
|
||||||
HasApplicationAccess = model.HasApplicationAccess,
|
HasApplicationAccess = model.HasApplicationAccess,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -1001,7 +937,7 @@ namespace MarcoBMS.Services.Controllers
|
|||||||
existingEmployee.PhoneNumber = model.PhoneNumber;
|
existingEmployee.PhoneNumber = model.PhoneNumber;
|
||||||
existingEmployee.JoiningDate = model.JoiningDate;
|
existingEmployee.JoiningDate = model.JoiningDate;
|
||||||
existingEmployee.JobRoleId = model.JobRoleId;
|
existingEmployee.JobRoleId = model.JobRoleId;
|
||||||
existingEmployee.OrganizationId = model.OrganizationId;
|
existingEmployee.OrganizationId = organizationId;
|
||||||
existingEmployee.HasApplicationAccess = model.HasApplicationAccess;
|
existingEmployee.HasApplicationAccess = model.HasApplicationAccess;
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(existingEmployee.Email) && !string.IsNullOrWhiteSpace(model.Email))
|
if (string.IsNullOrWhiteSpace(existingEmployee.Email) && !string.IsNullOrWhiteSpace(model.Email))
|
||||||
@ -1011,7 +947,7 @@ namespace MarcoBMS.Services.Controllers
|
|||||||
|
|
||||||
if (emailExists)
|
if (emailExists)
|
||||||
{
|
{
|
||||||
_logger.LogInfo("Employee email already exists in org. Email={Email}, Org={OrgId}", model.Email, model.OrganizationId);
|
_logger.LogInfo("Employee email already exists in org. Email={Email}, Org={OrgId}", model.Email, organizationId);
|
||||||
return StatusCode(409, ApiResponse<object>.ErrorResponse("Employee with email already exists", "Employee with this email already exists", 409));
|
return StatusCode(409, ApiResponse<object>.ErrorResponse("Employee with email already exists", "Employee with this email already exists", 409));
|
||||||
}
|
}
|
||||||
existingEmployee.Email = model.Email;
|
existingEmployee.Email = model.Email;
|
||||||
@ -1075,7 +1011,7 @@ namespace MarcoBMS.Services.Controllers
|
|||||||
|
|
||||||
Guid tenantId = _userHelper.GetTenantId();
|
Guid tenantId = _userHelper.GetTenantId();
|
||||||
var LoggedEmployee = await _userHelper.GetCurrentEmployeeAsync();
|
var LoggedEmployee = await _userHelper.GetCurrentEmployeeAsync();
|
||||||
Employee? employee = await _context.Employees.FirstOrDefaultAsync(e => e.Id == id && e.TenantId == tenantId);
|
Employee? employee = await _context.Employees.FirstOrDefaultAsync(e => e.Id == id && e.OrganizationId == organizationId);
|
||||||
if (employee == null)
|
if (employee == null)
|
||||||
{
|
{
|
||||||
_logger.LogWarning("Employee with ID {EmploueeId} not found in database", id);
|
_logger.LogWarning("Employee with ID {EmploueeId} not found in database", id);
|
||||||
@ -1248,11 +1184,14 @@ namespace MarcoBMS.Services.Controllers
|
|||||||
|
|
||||||
// Prepare reset link sender helper
|
// Prepare reset link sender helper
|
||||||
private async Task SendResetIfApplicableAsync(ApplicationUser u, string firstName)
|
private async Task SendResetIfApplicableAsync(ApplicationUser u, string firstName)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrWhiteSpace(u.Email))
|
||||||
{
|
{
|
||||||
var token = await _userManager.GeneratePasswordResetTokenAsync(u);
|
var token = await _userManager.GeneratePasswordResetTokenAsync(u);
|
||||||
var resetLink = $"{_configuration["AppSettings:WebFrontendUrl"]}/reset-password?token={WebUtility.UrlEncode(token)}";
|
var resetLink = $"{_configuration["AppSettings:WebFrontendUrl"]}/reset-password?token={WebUtility.UrlEncode(token)}";
|
||||||
await _emailSender.SendResetPasswordEmailOnRegister(u.Email ?? "", firstName, resetLink);
|
await _emailSender.SendResetPasswordEmailOnRegister(u.Email, firstName, resetLink);
|
||||||
_logger.LogInfo("Reset password email queued. Email={Email}", u.Email ?? "");
|
_logger.LogInfo("Reset password email queued. Email={Email}", u.Email);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -482,6 +482,13 @@ namespace MarcoBMS.Services.Controllers
|
|||||||
taskAllocationQuery = taskAllocationQuery.Where(t => t.WorkItem != null &&
|
taskAllocationQuery = taskAllocationQuery.Where(t => t.WorkItem != null &&
|
||||||
taskFilter.ActivityIds.Contains(t.WorkItem.ActivityId));
|
taskFilter.ActivityIds.Contains(t.WorkItem.ActivityId));
|
||||||
}
|
}
|
||||||
|
if (taskFilter.ServiceIds?.Any() ?? false)
|
||||||
|
{
|
||||||
|
taskAllocationQuery = taskAllocationQuery.Where(t => t.WorkItem != null &&
|
||||||
|
t.WorkItem.ActivityMaster != null &&
|
||||||
|
t.WorkItem.ActivityMaster.ActivityGroup != null &&
|
||||||
|
taskFilter.ServiceIds.Contains(t.WorkItem.ActivityMaster.ActivityGroup.ServiceId));
|
||||||
|
}
|
||||||
if (taskFilter.dateFrom.HasValue && taskFilter.dateTo.HasValue)
|
if (taskFilter.dateFrom.HasValue && taskFilter.dateTo.HasValue)
|
||||||
{
|
{
|
||||||
taskAllocationQuery = taskAllocationQuery.Where(t => t.AssignmentDate.Date >= taskFilter.dateFrom.Value.Date &&
|
taskAllocationQuery = taskAllocationQuery.Where(t => t.AssignmentDate.Date >= taskFilter.dateFrom.Value.Date &&
|
||||||
@ -745,6 +752,97 @@ namespace MarcoBMS.Services.Controllers
|
|||||||
return Ok(ApiResponse<object>.SuccessResponse(taskVM, "Success", 200));
|
return Ok(ApiResponse<object>.SuccessResponse(taskVM, "Success", 200));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpGet("filter/{projectId}")]
|
||||||
|
public async Task<IActionResult> GetTaskFilterObject(Guid projectId)
|
||||||
|
{
|
||||||
|
// Get the current tenant from claims/context
|
||||||
|
Guid tenantId = GetTenantId();
|
||||||
|
|
||||||
|
// Log API invocation with the project and tenant for traceability
|
||||||
|
_logger.LogInfo("Fetching filter objects for ProjectId={ProjectId}, TenantId={TenantId}", projectId, tenantId);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// AsNoTracking for improved performance—no intention to update these records
|
||||||
|
// Only fetch & project properties actually required (DTO projection)
|
||||||
|
var tasks = await _context.TaskAllocations
|
||||||
|
.Include(t => t.WorkItem)
|
||||||
|
.ThenInclude(wi => wi!.WorkArea)
|
||||||
|
.ThenInclude(wa => wa!.Floor)
|
||||||
|
.ThenInclude(f => f!.Building)
|
||||||
|
.Include(t => t.WorkItem)
|
||||||
|
.ThenInclude(wi => wi!.ActivityMaster)
|
||||||
|
.ThenInclude(a => a!.ActivityGroup)
|
||||||
|
.ThenInclude(ag => ag!.Service)
|
||||||
|
.Where(t => t.WorkItem != null &&
|
||||||
|
t.WorkItem.WorkArea != null &&
|
||||||
|
t.WorkItem.WorkArea.Floor != null &&
|
||||||
|
t.WorkItem.WorkArea.Floor.Building != null &&
|
||||||
|
t.WorkItem.WorkArea.Floor.Building.ProjectId == projectId &&
|
||||||
|
t.TenantId == tenantId).ToListAsync();
|
||||||
|
|
||||||
|
// Distinct by Id (since projection doesn't guarantee uniqueness across different allocations)
|
||||||
|
var buildings = tasks.Where(t => t.WorkItem != null && t.WorkItem.WorkArea != null && t.WorkItem.WorkArea.Floor != null && t.WorkItem.WorkArea.Floor.Building != null)
|
||||||
|
.Select(t => t.WorkItem!.WorkArea!.Floor!.Building!)
|
||||||
|
.Select(b => new
|
||||||
|
{
|
||||||
|
Id = b.Id,
|
||||||
|
Name = b.Name
|
||||||
|
}).Distinct().ToList();
|
||||||
|
|
||||||
|
var floors = tasks.Where(t => t.WorkItem != null && t.WorkItem.WorkArea != null && t.WorkItem.WorkArea.Floor != null)
|
||||||
|
.Select(t => t.WorkItem!.WorkArea!.Floor!)
|
||||||
|
.Select(f => new
|
||||||
|
{
|
||||||
|
Id = f.Id,
|
||||||
|
Name = f.FloorName,
|
||||||
|
BuildingId = f.BuildingId
|
||||||
|
}).Distinct().ToList();
|
||||||
|
|
||||||
|
var activities = tasks.Where(t => t.WorkItem != null &&
|
||||||
|
t.WorkItem.ActivityMaster != null &&
|
||||||
|
t.WorkItem.ActivityMaster.ActivityGroup != null &&
|
||||||
|
t.WorkItem.ActivityMaster.ActivityGroup.Service != null)
|
||||||
|
.Select(t => t.WorkItem!.ActivityMaster!)
|
||||||
|
.Select(a => new
|
||||||
|
{
|
||||||
|
Id = a.Id,
|
||||||
|
Name = a.ActivityName
|
||||||
|
}).Distinct().ToList();
|
||||||
|
|
||||||
|
var services = tasks.Where(t => t.WorkItem != null &&
|
||||||
|
t.WorkItem.ActivityMaster != null &&
|
||||||
|
t.WorkItem.ActivityMaster.ActivityGroup != null &&
|
||||||
|
t.WorkItem.ActivityMaster.ActivityGroup.Service != null)
|
||||||
|
.Select(t => t.WorkItem!.ActivityMaster!.ActivityGroup!.Service!)
|
||||||
|
.Select(s => new
|
||||||
|
{
|
||||||
|
Id = s.Id,
|
||||||
|
Name = s.Name
|
||||||
|
}).Distinct().ToList();
|
||||||
|
|
||||||
|
var response = new
|
||||||
|
{
|
||||||
|
Buildings = buildings,
|
||||||
|
Floors = floors,
|
||||||
|
Activities = activities,
|
||||||
|
Services = services
|
||||||
|
};
|
||||||
|
|
||||||
|
_logger.LogInfo("Successfully fetched filter objects for ProjectId={ProjectId}, TenantId={TenantId}", projectId, tenantId);
|
||||||
|
|
||||||
|
// Use DTO in API response for clarity and easier frontend usage
|
||||||
|
return Ok(ApiResponse<object>.SuccessResponse(response, "Filter object for task fetched successfully", 200));
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Failed to fetch filter objects for ProjectId={ProjectId}, TenantId={TenantId}", projectId, tenantId);
|
||||||
|
// Return a standard error result
|
||||||
|
return StatusCode(500, ApiResponse<object>.ErrorResponse("Failed to fetch filter object.", 500));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Approves a reported task after validation, updates status, and stores attachments/comments.
|
/// Approves a reported task after validation, updates status, and stores attachments/comments.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -273,12 +273,7 @@ namespace Marco.Pms.Services.Controllers
|
|||||||
return StatusCode(403,
|
return StatusCode(403,
|
||||||
ApiResponse<object>.ErrorResponse("Access denied", "User does not have the required permissions for this action.", 403));
|
ApiResponse<object>.ErrorResponse("Access denied", "User does not have the required permissions for this action.", 403));
|
||||||
}
|
}
|
||||||
if (!hasManagePermission && (hasModifyPermission || hasViewPermission) && id != loggedInEmployee.TenantId)
|
|
||||||
{
|
|
||||||
_logger.LogWarning("Permission denied: User {EmployeeId} attempted to access tenant details of other tenant.", loggedInEmployee.Id);
|
|
||||||
return StatusCode(403,
|
|
||||||
ApiResponse<object>.ErrorResponse("Access denied", "User does not have the required permissions for this action.", 403));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a single DbContext for main tenant fetch and related data requests
|
// Create a single DbContext for main tenant fetch and related data requests
|
||||||
await using var _context = await _dbContextFactory.CreateDbContextAsync();
|
await using var _context = await _dbContextFactory.CreateDbContextAsync();
|
||||||
@ -297,6 +292,13 @@ namespace Marco.Pms.Services.Controllers
|
|||||||
}
|
}
|
||||||
_logger.LogInfo("Tenant {TenantId} found.", tenant.Id);
|
_logger.LogInfo("Tenant {TenantId} found.", tenant.Id);
|
||||||
|
|
||||||
|
if (!hasManagePermission && (hasModifyPermission || hasViewPermission) && tenant.OrganizationId != loggedInEmployee.OrganizationId)
|
||||||
|
{
|
||||||
|
_logger.LogWarning("Permission denied: User {EmployeeId} attempted to access tenant details of other tenant.", loggedInEmployee.Id);
|
||||||
|
return StatusCode(403,
|
||||||
|
ApiResponse<object>.ErrorResponse("Access denied", "User does not have the required permissions for this action.", 403));
|
||||||
|
}
|
||||||
|
|
||||||
// Fetch dependent data in parallel to improve performance
|
// Fetch dependent data in parallel to improve performance
|
||||||
var employeesTask = Task.Run(async () =>
|
var employeesTask = Task.Run(async () =>
|
||||||
{
|
{
|
||||||
@ -550,7 +552,7 @@ namespace Marco.Pms.Services.Controllers
|
|||||||
JobRole = adminJobRole, // Link to the newly created role
|
JobRole = adminJobRole, // Link to the newly created role
|
||||||
CurrentAddress = model.BillingAddress,
|
CurrentAddress = model.BillingAddress,
|
||||||
IsActive = true,
|
IsActive = true,
|
||||||
IsSystem = false,
|
IsSystem = true,
|
||||||
IsPrimary = true,
|
IsPrimary = true,
|
||||||
OrganizationId = organization.Id,
|
OrganizationId = organization.Id,
|
||||||
HasApplicationAccess = true
|
HasApplicationAccess = true
|
||||||
@ -566,43 +568,36 @@ namespace Marco.Pms.Services.Controllers
|
|||||||
};
|
};
|
||||||
_context.ApplicationRoles.Add(applicationRole);
|
_context.ApplicationRoles.Add(applicationRole);
|
||||||
|
|
||||||
var rolePermissionMappigs = new List<RolePermissionMappings> {
|
var featureIds = new List<Guid>
|
||||||
new RolePermissionMappings
|
{
|
||||||
|
new Guid("a4e25142-449b-4334-a6e5-22f70e4732d7"), // Expense Management feature
|
||||||
|
new Guid("81ab8a87-8ccd-4015-a917-0627cee6a100"), // Employee Management feature
|
||||||
|
new Guid("52c9cf54-1eb2-44d2-81bb-524cf29c0a94"), // Attendance Management feature
|
||||||
|
new Guid("a8cf4331-8f04-4961-8360-a3f7c3cc7462"), // Document Management feature
|
||||||
|
new Guid("be3b3afc-6ccf-4566-b9b6-aafcb65546be"), // Masters Management feature
|
||||||
|
new Guid("39e66f81-efc6-446c-95bd-46bff6cfb606"), // Directory Management feature
|
||||||
|
new Guid("6d4c82d6-dbce-48ab-b8b8-f785f4d8c914") // Organization Management feature
|
||||||
|
};
|
||||||
|
|
||||||
|
var permissionIds = await _context.FeaturePermissions.Where(fp => featureIds.Contains(fp.FeatureId)).Select(fp => fp.Id).ToListAsync();
|
||||||
|
|
||||||
|
var rolePermissionMappigs = permissionIds.Select(p => new RolePermissionMappings
|
||||||
|
{
|
||||||
|
ApplicationRoleId = applicationRole.Id,
|
||||||
|
FeaturePermissionId = p
|
||||||
|
}).ToList();
|
||||||
|
|
||||||
|
rolePermissionMappigs.Add(new RolePermissionMappings
|
||||||
{
|
{
|
||||||
ApplicationRoleId = applicationRole.Id,
|
ApplicationRoleId = applicationRole.Id,
|
||||||
FeaturePermissionId = PermissionsMaster.ModifyTenant
|
FeaturePermissionId = PermissionsMaster.ModifyTenant
|
||||||
},
|
});
|
||||||
new RolePermissionMappings
|
rolePermissionMappigs.Add(new RolePermissionMappings
|
||||||
{
|
{
|
||||||
ApplicationRoleId = applicationRole.Id,
|
ApplicationRoleId = applicationRole.Id,
|
||||||
FeaturePermissionId = PermissionsMaster.ViewTenant
|
FeaturePermissionId = PermissionsMaster.ViewTenant
|
||||||
},
|
});
|
||||||
new RolePermissionMappings
|
|
||||||
{
|
|
||||||
ApplicationRoleId = applicationRole.Id,
|
|
||||||
FeaturePermissionId = PermissionsMaster.ManageMasters
|
|
||||||
},
|
|
||||||
new RolePermissionMappings
|
|
||||||
{
|
|
||||||
ApplicationRoleId = applicationRole.Id,
|
|
||||||
FeaturePermissionId = PermissionsMaster.ViewMasters
|
|
||||||
},
|
|
||||||
new RolePermissionMappings
|
|
||||||
{
|
|
||||||
ApplicationRoleId = applicationRole.Id,
|
|
||||||
FeaturePermissionId = PermissionsMaster.ViewOrganization
|
|
||||||
},
|
|
||||||
new RolePermissionMappings
|
|
||||||
{
|
|
||||||
ApplicationRoleId = applicationRole.Id,
|
|
||||||
FeaturePermissionId = PermissionsMaster.AddOrganization
|
|
||||||
},
|
|
||||||
new RolePermissionMappings
|
|
||||||
{
|
|
||||||
ApplicationRoleId = applicationRole.Id,
|
|
||||||
FeaturePermissionId = PermissionsMaster.EditOrganization
|
|
||||||
}
|
|
||||||
};
|
|
||||||
_context.RolePermissionMappings.AddRange(rolePermissionMappigs);
|
_context.RolePermissionMappings.AddRange(rolePermissionMappigs);
|
||||||
|
|
||||||
_context.EmployeeRoleMappings.Add(new EmployeeRoleMapping
|
_context.EmployeeRoleMappings.Add(new EmployeeRoleMapping
|
||||||
@ -651,6 +646,22 @@ namespace Marco.Pms.Services.Controllers
|
|||||||
_context.OrgServiceMappings.AddRange(serviceOrgMappings);
|
_context.OrgServiceMappings.AddRange(serviceOrgMappings);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var _masteData = scope.ServiceProvider.GetRequiredService<MasterDataService>();
|
||||||
|
|
||||||
|
var expensesTypeMaster = _masteData.GetExpensesTypeesData(tenant.Id);
|
||||||
|
var paymentModeMatser = _masteData.GetPaymentModesData(tenant.Id);
|
||||||
|
var documentCategoryMaster = _masteData.GetDocumentCategoryData(tenant.Id);
|
||||||
|
|
||||||
|
var employeeDocumentId = documentCategoryMaster.Where(dc => dc.Name == "Employee Documents").Select(dc => dc.Id).FirstOrDefault();
|
||||||
|
var projectDocumentId = documentCategoryMaster.Where(dc => dc.Name == "Project Documents").Select(dc => dc.Id).FirstOrDefault();
|
||||||
|
|
||||||
|
var documentTypeMaster = _masteData.GetDocumentTypeData(tenant.Id, employeeDocumentId, projectDocumentId);
|
||||||
|
|
||||||
|
_context.ExpensesTypeMaster.AddRange(expensesTypeMaster);
|
||||||
|
_context.PaymentModeMatser.AddRange(paymentModeMatser);
|
||||||
|
_context.DocumentCategoryMasters.AddRange(documentCategoryMaster);
|
||||||
|
_context.DocumentTypeMasters.AddRange(documentTypeMaster);
|
||||||
|
|
||||||
// All entities are now added to the context. Save them all in a single database operation.
|
// All entities are now added to the context. Save them all in a single database operation.
|
||||||
await _context.SaveChangesAsync();
|
await _context.SaveChangesAsync();
|
||||||
|
|
||||||
|
@ -253,16 +253,29 @@ namespace Marco.Pms.Services.Helpers
|
|||||||
!ts.IsCancelled &&
|
!ts.IsCancelled &&
|
||||||
ts.EndDate.Date >= DateTime.UtcNow.Date); // FIX: Subscription should not be expired
|
ts.EndDate.Date >= DateTime.UtcNow.Date); // FIX: Subscription should not be expired
|
||||||
|
|
||||||
|
var featureIds = new List<Guid>
|
||||||
|
{
|
||||||
|
new Guid("a4e25142-449b-4334-a6e5-22f70e4732d7"), // Expense Management feature
|
||||||
|
new Guid("81ab8a87-8ccd-4015-a917-0627cee6a100"), // Employee Management feature
|
||||||
|
new Guid("52c9cf54-1eb2-44d2-81bb-524cf29c0a94"), // Attendance Management feature
|
||||||
|
new Guid("a8cf4331-8f04-4961-8360-a3f7c3cc7462"), // Document Management feature
|
||||||
|
new Guid("be3b3afc-6ccf-4566-b9b6-aafcb65546be"), // Masters Management feature
|
||||||
|
new Guid("39e66f81-efc6-446c-95bd-46bff6cfb606"), // Directory Management feature
|
||||||
|
new Guid("6d4c82d6-dbce-48ab-b8b8-f785f4d8c914") // Organization Management feature
|
||||||
|
//new Guid("2f3509b7-160d-410a-b9b6-daadd96c986d") // Tenant Management feature
|
||||||
|
};
|
||||||
|
|
||||||
if (tenantSubscription == null)
|
if (tenantSubscription == null)
|
||||||
{
|
{
|
||||||
_logger.LogWarning("No active subscription found for tenant: {TenantId}", tenantId);
|
_logger.LogWarning("No active subscription found for tenant: {TenantId}", tenantId);
|
||||||
return new List<Guid>();
|
return featureIds;
|
||||||
}
|
}
|
||||||
|
|
||||||
_logger.LogDebug("Active subscription found for tenant: {TenantId}, PlanId: {PlanId}",
|
_logger.LogDebug("Active subscription found for tenant: {TenantId}, PlanId: {PlanId}",
|
||||||
tenantId, tenantSubscription.Plan!.Id);
|
tenantId, tenantSubscription.Plan!.Id);
|
||||||
|
|
||||||
var featureIds = new List<Guid> { new Guid("2f3509b7-160d-410a-b9b6-daadd96c986d"), new Guid("be3b3afc-6ccf-4566-b9b6-aafcb65546be") };
|
//var featureIds = new List<Guid> { new Guid("be3b3afc-6ccf-4566-b9b6-aafcb65546be") };
|
||||||
|
|
||||||
|
|
||||||
// Step 2: Get feature details from Plan
|
// Step 2: Get feature details from Plan
|
||||||
var featureDetails = await _featureDetailsHelper.GetFeatureDetails(tenantSubscription.Plan!.FeaturesId);
|
var featureDetails = await _featureDetailsHelper.GetFeatureDetails(tenantSubscription.Plan!.FeaturesId);
|
||||||
|
@ -1,10 +1,9 @@
|
|||||||
using MarcoBMS.Services.Service;
|
using MarcoBMS.Services.Service;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
|
||||||
using Microsoft.AspNetCore.SignalR;
|
using Microsoft.AspNetCore.SignalR;
|
||||||
|
|
||||||
namespace Marco.Pms.Services.Hubs
|
namespace Marco.Pms.Services.Hubs
|
||||||
{
|
{
|
||||||
[Authorize]
|
//[Authorize]
|
||||||
public class MarcoHub : Hub
|
public class MarcoHub : Hub
|
||||||
{
|
{
|
||||||
private readonly ILoggingService _logger;
|
private readonly ILoggingService _logger;
|
||||||
|
@ -248,6 +248,7 @@ namespace Marco.Pms.Services.MappingProfiles
|
|||||||
dest => dest.Id,
|
dest => dest.Id,
|
||||||
opt => opt.MapFrom(src => Guid.Parse(src.Id)));
|
opt => opt.MapFrom(src => Guid.Parse(src.Id)));
|
||||||
|
|
||||||
|
CreateMap<Expenses, ExpenseDetailsVM>();
|
||||||
CreateMap<ExpenseDetailsMongoDB, ExpenseDetailsVM>()
|
CreateMap<ExpenseDetailsMongoDB, ExpenseDetailsVM>()
|
||||||
.ForMember(
|
.ForMember(
|
||||||
dest => dest.Id,
|
dest => dest.Id,
|
||||||
|
@ -1533,9 +1533,11 @@ namespace Marco.Pms.Services.Service
|
|||||||
return ApiResponse<object>.ErrorResponse("Contact ID is empty", "Contact ID is empty", 400);
|
return ApiResponse<object>.ErrorResponse("Contact ID is empty", "Contact ID is empty", 400);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var (hasAdminPermission, hasManagerPermission, hasUserPermission) = await CheckPermissionsAsync(loggedInEmployee.Id);
|
||||||
|
|
||||||
var bucketIds = await _context.ContactBucketMappings.Where(cb => cb.ContactId == id).Select(cb => cb.BucketId).ToListAsync();
|
var bucketIds = await _context.ContactBucketMappings.Where(cb => cb.ContactId == id).Select(cb => cb.BucketId).ToListAsync();
|
||||||
var hasContactAccess = await _context.EmployeeBucketMappings.AnyAsync(eb => bucketIds.Contains(eb.BucketId) && eb.EmployeeId == loggedInEmployee.Id);
|
var hasContactAccess = await _context.EmployeeBucketMappings.AnyAsync(eb => bucketIds.Contains(eb.BucketId) && eb.EmployeeId == loggedInEmployee.Id);
|
||||||
if (hasContactAccess)
|
if (!hasAdminPermission && !hasContactAccess)
|
||||||
{
|
{
|
||||||
_logger.LogWarning("Employee {EmployeeId} does not have permission to delete contact {ContactId}",
|
_logger.LogWarning("Employee {EmployeeId} does not have permission to delete contact {ContactId}",
|
||||||
loggedInEmployee.Id, id);
|
loggedInEmployee.Id, id);
|
||||||
@ -2131,7 +2133,7 @@ namespace Marco.Pms.Services.Service
|
|||||||
|
|
||||||
var bucketIds = await _context.ContactBucketMappings.Where(cb => cb.ContactId == noteDto.ContactId).Select(cb => cb.BucketId).ToListAsync();
|
var bucketIds = await _context.ContactBucketMappings.Where(cb => cb.ContactId == noteDto.ContactId).Select(cb => cb.BucketId).ToListAsync();
|
||||||
var hasContactAccess = await _context.EmployeeBucketMappings.AnyAsync(eb => bucketIds.Contains(eb.BucketId) && eb.EmployeeId == loggedInEmployee.Id);
|
var hasContactAccess = await _context.EmployeeBucketMappings.AnyAsync(eb => bucketIds.Contains(eb.BucketId) && eb.EmployeeId == loggedInEmployee.Id);
|
||||||
if (!hasAdminPermission && hasContactAccess)
|
if (!hasAdminPermission && !hasContactAccess)
|
||||||
{
|
{
|
||||||
_logger.LogWarning("Employee {EmployeeId} does not have permission to delete contact {ContactId}",
|
_logger.LogWarning("Employee {EmployeeId} does not have permission to delete contact {ContactId}",
|
||||||
loggedInEmployee.Id, noteDto.ContactId);
|
loggedInEmployee.Id, noteDto.ContactId);
|
||||||
@ -2270,10 +2272,11 @@ namespace Marco.Pms.Services.Service
|
|||||||
|
|
||||||
return ApiResponse<object>.ErrorResponse("Note not found", "Note not found", 404);
|
return ApiResponse<object>.ErrorResponse("Note not found", "Note not found", 404);
|
||||||
}
|
}
|
||||||
|
var (hasAdminPermission, hasManagerPermission, hasUserPermission) = await CheckPermissionsAsync(loggedInEmployee.Id);
|
||||||
|
|
||||||
var bucketIds = await _context.ContactBucketMappings.Where(cb => cb.ContactId == note.ContactId).Select(cb => cb.BucketId).ToListAsync();
|
var bucketIds = await _context.ContactBucketMappings.Where(cb => cb.ContactId == note.ContactId).Select(cb => cb.BucketId).ToListAsync();
|
||||||
var hasContactAccess = await _context.EmployeeBucketMappings.AnyAsync(eb => bucketIds.Contains(eb.BucketId) && eb.EmployeeId == loggedInEmployee.Id);
|
var hasContactAccess = await _context.EmployeeBucketMappings.AnyAsync(eb => bucketIds.Contains(eb.BucketId) && eb.EmployeeId == loggedInEmployee.Id);
|
||||||
if (hasContactAccess)
|
if (!hasAdminPermission && !hasContactAccess)
|
||||||
{
|
{
|
||||||
_logger.LogWarning("Employee {EmployeeId} does not have permission to delete contact {ContactId}",
|
_logger.LogWarning("Employee {EmployeeId} does not have permission to delete contact {ContactId}",
|
||||||
loggedInEmployee.Id, note.ContactId);
|
loggedInEmployee.Id, note.ContactId);
|
||||||
|
@ -23,6 +23,7 @@ using MarcoBMS.Services.Service;
|
|||||||
using Microsoft.CodeAnalysis;
|
using Microsoft.CodeAnalysis;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
using Document = Marco.Pms.Model.DocumentManager.Document;
|
using Document = Marco.Pms.Model.DocumentManager.Document;
|
||||||
|
|
||||||
namespace Marco.Pms.Services.Service
|
namespace Marco.Pms.Services.Service
|
||||||
@ -129,6 +130,16 @@ namespace Marco.Pms.Services.Service
|
|||||||
// 3. --- Build Base Query and Apply Permissions ---
|
// 3. --- Build Base Query and Apply Permissions ---
|
||||||
// Start with a base IQueryable. Filters will be chained onto this.
|
// Start with a base IQueryable. Filters will be chained onto this.
|
||||||
var expensesQuery = _context.Expenses
|
var expensesQuery = _context.Expenses
|
||||||
|
.Include(e => e.PaidBy)
|
||||||
|
.Include(e => e.CreatedBy)
|
||||||
|
.Include(e => e.ProcessedBy)
|
||||||
|
.Include(e => e.ApprovedBy)
|
||||||
|
.Include(e => e.ReviewedBy)
|
||||||
|
.Include(e => e.PaymentMode)
|
||||||
|
.Include(e => e.Project)
|
||||||
|
.Include(e => e.PaymentMode)
|
||||||
|
.Include(e => e.ExpensesType)
|
||||||
|
.Include(e => e.Status)
|
||||||
.Where(e => e.TenantId == tenantId); // Always filter by TenantId first.
|
.Where(e => e.TenantId == tenantId); // Always filter by TenantId first.
|
||||||
|
|
||||||
if (cacheList == null)
|
if (cacheList == null)
|
||||||
@ -176,6 +187,10 @@ namespace Marco.Pms.Services.Service
|
|||||||
{
|
{
|
||||||
expensesQuery = expensesQuery.Where(e => expenseFilter.PaidById.Contains(e.PaidById));
|
expensesQuery = expensesQuery.Where(e => expenseFilter.PaidById.Contains(e.PaidById));
|
||||||
}
|
}
|
||||||
|
if (expenseFilter.ExpenseTypeIds?.Any() == true)
|
||||||
|
{
|
||||||
|
expensesQuery = expensesQuery.Where(e => expenseFilter.ExpenseTypeIds.Contains(e.ExpensesTypeId));
|
||||||
|
}
|
||||||
|
|
||||||
// Only allow filtering by 'CreatedBy' if the user has 'View All' permission.
|
// Only allow filtering by 'CreatedBy' if the user has 'View All' permission.
|
||||||
if (expenseFilter.CreatedByIds?.Any() == true && hasViewAllPermissionTask.Result)
|
if (expenseFilter.CreatedByIds?.Any() == true && hasViewAllPermissionTask.Result)
|
||||||
@ -213,7 +228,8 @@ namespace Marco.Pms.Services.Service
|
|||||||
return ApiResponse<object>.SuccessResponse(new List<ExpenseList>(), "No expenses found for the given criteria.", 200);
|
return ApiResponse<object>.SuccessResponse(new List<ExpenseList>(), "No expenses found for the given criteria.", 200);
|
||||||
}
|
}
|
||||||
|
|
||||||
expenseVM = await GetAllExpnesRelatedTables(expensesList, tenantId);
|
//expenseVM = await GetAllExpnesRelatedTables(expensesList, tenantId);
|
||||||
|
expenseVM = _mapper.Map<List<ExpenseList>>(expensesList);
|
||||||
totalPages = (int)Math.Ceiling((double)totalEntites / pageSize);
|
totalPages = (int)Math.Ceiling((double)totalEntites / pageSize);
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -276,7 +292,18 @@ namespace Marco.Pms.Services.Service
|
|||||||
ExpenseDetailsMongoDB? expenseDetails = null;
|
ExpenseDetailsMongoDB? expenseDetails = null;
|
||||||
if (expenseDetails == null)
|
if (expenseDetails == null)
|
||||||
{
|
{
|
||||||
var expense = await _context.Expenses.AsNoTracking().FirstOrDefaultAsync(e => e.Id == id && e.TenantId == tenantId);
|
var expense = await _context.Expenses
|
||||||
|
.Include(e => e.PaidBy)
|
||||||
|
.Include(e => e.CreatedBy)
|
||||||
|
.Include(e => e.ProcessedBy)
|
||||||
|
.Include(e => e.ApprovedBy)
|
||||||
|
.Include(e => e.ReviewedBy)
|
||||||
|
.Include(e => e.PaymentMode)
|
||||||
|
.Include(e => e.Project)
|
||||||
|
.Include(e => e.PaymentMode)
|
||||||
|
.Include(e => e.ExpensesType)
|
||||||
|
.Include(e => e.Status)
|
||||||
|
.AsNoTracking().FirstOrDefaultAsync(e => e.Id == id && e.TenantId == tenantId);
|
||||||
|
|
||||||
if (expense == null)
|
if (expense == null)
|
||||||
{
|
{
|
||||||
@ -318,6 +345,13 @@ namespace Marco.Pms.Services.Service
|
|||||||
{
|
{
|
||||||
status.PermissionIds = permissionStatusMappings.Where(ps => ps.StatusId == status.Id).Select(ps => ps.PermissionIds).FirstOrDefault();
|
status.PermissionIds = permissionStatusMappings.Where(ps => ps.StatusId == status.Id).Select(ps => ps.PermissionIds).FirstOrDefault();
|
||||||
}
|
}
|
||||||
|
int index = vm.NextStatus.FindIndex(ns => ns.DisplayName == "Reject");
|
||||||
|
if (index > -1)
|
||||||
|
{
|
||||||
|
var item = vm.NextStatus[index];
|
||||||
|
vm.NextStatus.RemoveAt(index);
|
||||||
|
vm.NextStatus.Insert(0, item);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
vm.ExpensesReimburse = _mapper.Map<ExpensesReimburseVM>(expensesReimburse);
|
vm.ExpensesReimburse = _mapper.Map<ExpensesReimburseVM>(expensesReimburse);
|
||||||
|
|
||||||
@ -361,66 +395,25 @@ namespace Marco.Pms.Services.Service
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// Task 1: Get all distinct projects associated with the tenant's expenses.
|
var expenses = await _context.Expenses
|
||||||
var projectsTask = Task.Run(async () =>
|
.Include(e => e.PaidBy)
|
||||||
{
|
.Include(e => e.Project)
|
||||||
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
|
.Include(e => e.CreatedBy)
|
||||||
return await dbContext.Expenses
|
.Include(e => e.Status)
|
||||||
.Where(e => e.TenantId == tenantId && e.Project != null)
|
.Include(e => e.ExpensesType)
|
||||||
.Select(e => e.Project!)
|
.Where(e => e.TenantId == tenantId)
|
||||||
.Distinct()
|
|
||||||
.Select(p => new { p.Id, Name = $"{p.Name}" })
|
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
});
|
|
||||||
|
|
||||||
// Task 2: Get all distinct users who paid for the tenant's expenses.
|
|
||||||
var paidByTask = Task.Run(async () =>
|
|
||||||
{
|
|
||||||
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
|
|
||||||
return await dbContext.Expenses
|
|
||||||
.Where(e => e.TenantId == tenantId && e.PaidBy != null)
|
|
||||||
.Select(e => e.PaidBy!)
|
|
||||||
.Distinct()
|
|
||||||
.Select(u => new { u.Id, Name = $"{u.FirstName} {u.LastName}" })
|
|
||||||
.ToListAsync();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Task 3: Get all distinct users who created the tenant's expenses.
|
|
||||||
var createdByTask = Task.Run(async () =>
|
|
||||||
{
|
|
||||||
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
|
|
||||||
return await dbContext.Expenses
|
|
||||||
.Where(e => e.TenantId == tenantId && e.CreatedBy != null)
|
|
||||||
.Select(e => e.CreatedBy!)
|
|
||||||
.Distinct()
|
|
||||||
.Select(u => new { u.Id, Name = $"{u.FirstName} {u.LastName}" })
|
|
||||||
.ToListAsync();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Task 4: Get all distinct statuses associated with the tenant's expenses.
|
|
||||||
var statusTask = Task.Run(async () =>
|
|
||||||
{
|
|
||||||
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
|
|
||||||
return await dbContext.Expenses
|
|
||||||
.Where(e => e.TenantId == tenantId && e.Status != null)
|
|
||||||
.Select(e => e.Status!)
|
|
||||||
.Distinct()
|
|
||||||
.Select(s => new { s.Id, s.Name })
|
|
||||||
.ToListAsync();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Execute all four queries concurrently. The total wait time will be determined
|
|
||||||
// by the longest-running query, not the sum of all four.
|
|
||||||
await Task.WhenAll(projectsTask, paidByTask, createdByTask, statusTask);
|
|
||||||
|
|
||||||
// Construct the final object from the results of the completed tasks.
|
// Construct the final object from the results of the completed tasks.
|
||||||
return ApiResponse<object>.SuccessResponse(new
|
var response = new
|
||||||
{
|
{
|
||||||
Projects = await projectsTask,
|
Projects = expenses.Where(e => e.Project != null).Select(e => new { Id = e.Project!.Id, Name = e.Project.Name }).Distinct().ToList(),
|
||||||
PaidBy = await paidByTask,
|
PaidBy = expenses.Where(e => e.PaidBy != null).Select(e => new { Id = e.PaidBy!.Id, Name = $"{e.PaidBy.FirstName} {e.PaidBy.LastName}" }).Distinct().ToList(),
|
||||||
CreatedBy = await createdByTask,
|
CreatedBy = expenses.Where(e => e.CreatedBy != null).Select(e => new { Id = e.CreatedBy!.Id, Name = $"{e.CreatedBy.FirstName} {e.CreatedBy.LastName}" }).Distinct().ToList(),
|
||||||
Status = await statusTask
|
Status = expenses.Where(e => e.Status != null).Select(e => new { Id = e.Status!.Id, Name = e.Status.Name }).Distinct().ToList(),
|
||||||
}, "Successfully fetched the filter list", 200);
|
ExpensesType = expenses.Where(e => e.ExpensesType != null).Select(e => new { Id = e.ExpensesType!.Id, Name = e.ExpensesType.Name }).Distinct().ToList()
|
||||||
|
};
|
||||||
|
return ApiResponse<object>.SuccessResponse(response, "Successfully fetched the filter list", 200);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@ -458,13 +451,6 @@ namespace Marco.Pms.Services.Service
|
|||||||
return await permissionService.HasPermission(PermissionsMaster.ExpenseUpload, loggedInEmployee.Id);
|
return await permissionService.HasPermission(PermissionsMaster.ExpenseUpload, loggedInEmployee.Id);
|
||||||
});
|
});
|
||||||
|
|
||||||
var hasProjectPermissionTask = Task.Run(async () =>
|
|
||||||
{
|
|
||||||
using var scope = _serviceScopeFactory.CreateScope();
|
|
||||||
var permissionService = scope.ServiceProvider.GetRequiredService<PermissionServices>();
|
|
||||||
return await permissionService.HasProjectPermission(loggedInEmployee, dto.ProjectId);
|
|
||||||
});
|
|
||||||
|
|
||||||
// VALIDATION CHECKS: Use IDbContextFactory for thread-safe, parallel database queries.
|
// VALIDATION CHECKS: Use IDbContextFactory for thread-safe, parallel database queries.
|
||||||
// Each task gets its own DbContext instance.
|
// Each task gets its own DbContext instance.
|
||||||
var projectTask = Task.Run(async () =>
|
var projectTask = Task.Run(async () =>
|
||||||
@ -487,6 +473,15 @@ namespace Marco.Pms.Services.Service
|
|||||||
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
|
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
|
||||||
return await dbContext.PaymentModeMatser.AsNoTracking().FirstOrDefaultAsync(pm => pm.Id == dto.PaymentModeId);
|
return await dbContext.PaymentModeMatser.AsNoTracking().FirstOrDefaultAsync(pm => pm.Id == dto.PaymentModeId);
|
||||||
});
|
});
|
||||||
|
var expenseUIdTask = Task.Run(async () =>
|
||||||
|
{
|
||||||
|
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
|
||||||
|
var result = await dbContext.Expenses
|
||||||
|
.Where(e => !string.IsNullOrWhiteSpace(e.ExpenseUId)).ToListAsync();
|
||||||
|
return result
|
||||||
|
.Select(e => ExtractNumber(e.ExpenseUId))
|
||||||
|
.OrderByDescending(id => id).FirstOrDefault();
|
||||||
|
});
|
||||||
var statusMappingTask = Task.Run(async () =>
|
var statusMappingTask = Task.Run(async () =>
|
||||||
{
|
{
|
||||||
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
|
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
|
||||||
@ -506,13 +501,10 @@ namespace Marco.Pms.Services.Service
|
|||||||
|
|
||||||
|
|
||||||
// Await all prerequisite checks at once.
|
// Await all prerequisite checks at once.
|
||||||
await Task.WhenAll(
|
await Task.WhenAll(hasUploadPermissionTask, projectTask, expenseTypeTask, paymentModeTask, statusMappingTask, paidByTask, expenseUIdTask);
|
||||||
hasUploadPermissionTask, hasProjectPermissionTask,
|
|
||||||
projectTask, expenseTypeTask, paymentModeTask, statusMappingTask, paidByTask
|
|
||||||
);
|
|
||||||
|
|
||||||
// 2. Aggregate and Check Results
|
// 2. Aggregate and Check Results
|
||||||
if (!await hasUploadPermissionTask || !await hasProjectPermissionTask)
|
if (!await hasUploadPermissionTask)
|
||||||
{
|
{
|
||||||
_logger.LogWarning("Access DENIED for employee {EmployeeId} on project {ProjectId}.", loggedInEmployee.Id, dto.ProjectId);
|
_logger.LogWarning("Access DENIED for employee {EmployeeId} on project {ProjectId}.", loggedInEmployee.Id, dto.ProjectId);
|
||||||
return ApiResponse<object>.ErrorResponse("Access Denied.", "You do not have permission to upload expenses for this project.", 403);
|
return ApiResponse<object>.ErrorResponse("Access Denied.", "You do not have permission to upload expenses for this project.", 403);
|
||||||
@ -524,6 +516,7 @@ namespace Marco.Pms.Services.Service
|
|||||||
var paymentMode = await paymentModeTask;
|
var paymentMode = await paymentModeTask;
|
||||||
var statusMapping = await statusMappingTask;
|
var statusMapping = await statusMappingTask;
|
||||||
var paidBy = await paidByTask;
|
var paidBy = await paidByTask;
|
||||||
|
var lastExpenseUId = expenseUIdTask.Result;
|
||||||
|
|
||||||
if (project == null) validationErrors.Add("Project not found.");
|
if (project == null) validationErrors.Add("Project not found.");
|
||||||
if (paidBy == null) validationErrors.Add("Paid by employee not found");
|
if (paidBy == null) validationErrors.Add("Paid by employee not found");
|
||||||
@ -540,8 +533,11 @@ namespace Marco.Pms.Services.Service
|
|||||||
return ApiResponse<object>.ErrorResponse("Invalid input data.", errorMessage, 400);
|
return ApiResponse<object>.ErrorResponse("Invalid input data.", errorMessage, 400);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var currentexpenseUId = (lastExpenseUId + 1).ToString("D5");
|
||||||
|
|
||||||
// 3. Entity Creation
|
// 3. Entity Creation
|
||||||
var expense = _mapper.Map<Expenses>(dto);
|
var expense = _mapper.Map<Expenses>(dto);
|
||||||
|
expense.ExpenseUId = $"EX-{currentexpenseUId}";
|
||||||
expense.CreatedById = loggedInEmployee.Id;
|
expense.CreatedById = loggedInEmployee.Id;
|
||||||
expense.CreatedAt = DateTime.UtcNow;
|
expense.CreatedAt = DateTime.UtcNow;
|
||||||
expense.TenantId = tenantId;
|
expense.TenantId = tenantId;
|
||||||
@ -1084,6 +1080,14 @@ namespace Marco.Pms.Services.Service
|
|||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region =================================================================== Helper Functions ===================================================================
|
#region =================================================================== Helper Functions ===================================================================
|
||||||
|
|
||||||
|
private int ExtractNumber(string id)
|
||||||
|
{
|
||||||
|
// Extract trailing number; handles EX_0001, EX-0001, EX0001
|
||||||
|
var m = Regex.Match(id ?? string.Empty, @"(\d+)$");
|
||||||
|
return m.Success ? int.Parse(m.Value) : int.MinValue; // put invalid IDs at the bottom
|
||||||
|
}
|
||||||
|
|
||||||
private static object ExceptionMapper(Exception ex)
|
private static object ExceptionMapper(Exception ex)
|
||||||
{
|
{
|
||||||
return new
|
return new
|
||||||
@ -1217,46 +1221,6 @@ namespace Marco.Pms.Services.Service
|
|||||||
|
|
||||||
private async Task<ExpenseDetailsMongoDB> GetAllExpnesRelatedTablesForSingle(Expenses model, Guid tenantId)
|
private async Task<ExpenseDetailsMongoDB> GetAllExpnesRelatedTablesForSingle(Expenses model, Guid tenantId)
|
||||||
{
|
{
|
||||||
var projectTask = Task.Run(async () =>
|
|
||||||
{
|
|
||||||
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
|
|
||||||
return await dbContext.Projects.AsNoTracking().FirstOrDefaultAsync(p => p.Id == model.ProjectId && p.TenantId == tenantId);
|
|
||||||
});
|
|
||||||
var paidByTask = Task.Run(async () =>
|
|
||||||
{
|
|
||||||
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
|
|
||||||
return await dbContext.Employees.Include(e => e.JobRole).AsNoTracking().FirstOrDefaultAsync(e => e.Id == model.PaidById && e.TenantId == tenantId);
|
|
||||||
});
|
|
||||||
var createdByTask = Task.Run(async () =>
|
|
||||||
{
|
|
||||||
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
|
|
||||||
return await dbContext.Employees.Include(e => e.JobRole).AsNoTracking().FirstOrDefaultAsync(e => e.Id == model.CreatedById && e.TenantId == tenantId);
|
|
||||||
});
|
|
||||||
var reviewedByTask = Task.Run(async () =>
|
|
||||||
{
|
|
||||||
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
|
|
||||||
return await dbContext.Employees.Include(e => e.JobRole).AsNoTracking().FirstOrDefaultAsync(e => e.Id == model.ReviewedById && e.TenantId == tenantId);
|
|
||||||
});
|
|
||||||
var approvedByTask = Task.Run(async () =>
|
|
||||||
{
|
|
||||||
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
|
|
||||||
return await dbContext.Employees.Include(e => e.JobRole).AsNoTracking().FirstOrDefaultAsync(e => e.Id == model.ApprovedById && e.TenantId == tenantId);
|
|
||||||
});
|
|
||||||
var processedByTask = Task.Run(async () =>
|
|
||||||
{
|
|
||||||
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
|
|
||||||
return await dbContext.Employees.Include(e => e.JobRole).AsNoTracking().FirstOrDefaultAsync(e => e.Id == model.ProcessedById && e.TenantId == tenantId);
|
|
||||||
});
|
|
||||||
var expenseTypeTask = Task.Run(async () =>
|
|
||||||
{
|
|
||||||
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
|
|
||||||
return await dbContext.ExpensesTypeMaster.AsNoTracking().FirstOrDefaultAsync(et => et.Id == model.ExpensesTypeId && et.TenantId == tenantId);
|
|
||||||
});
|
|
||||||
var paymentModeTask = Task.Run(async () =>
|
|
||||||
{
|
|
||||||
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
|
|
||||||
return await dbContext.PaymentModeMatser.AsNoTracking().FirstOrDefaultAsync(pm => pm.Id == model.PaymentModeId && pm.TenantId == tenantId);
|
|
||||||
});
|
|
||||||
var statusMappingTask = Task.Run(async () =>
|
var statusMappingTask = Task.Run(async () =>
|
||||||
{
|
{
|
||||||
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
|
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
|
||||||
@ -1304,29 +1268,20 @@ namespace Marco.Pms.Services.Service
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Await all prerequisite checks at once.
|
// Await all prerequisite checks at once.
|
||||||
await Task.WhenAll(projectTask, expenseTypeTask, paymentModeTask, statusMappingTask, paidByTask, createdByTask, reviewedByTask, approvedByTask,
|
await Task.WhenAll(statusTask, billAttachmentsTask);
|
||||||
processedByTask, statusTask, billAttachmentsTask);
|
|
||||||
|
|
||||||
var project = projectTask.Result;
|
|
||||||
var expenseType = expenseTypeTask.Result;
|
|
||||||
var paymentMode = paymentModeTask.Result;
|
|
||||||
var statusMapping = statusMappingTask.Result;
|
var statusMapping = statusMappingTask.Result;
|
||||||
var paidBy = paidByTask.Result;
|
|
||||||
var createdBy = createdByTask.Result;
|
|
||||||
var reviewedBy = reviewedByTask.Result;
|
|
||||||
var approvedBy = approvedByTask.Result;
|
|
||||||
var processedBy = processedByTask.Result;
|
|
||||||
var billAttachment = billAttachmentsTask.Result;
|
var billAttachment = billAttachmentsTask.Result;
|
||||||
|
|
||||||
|
|
||||||
var response = _mapper.Map<ExpenseDetailsMongoDB>(model);
|
var response = _mapper.Map<ExpenseDetailsMongoDB>(model);
|
||||||
|
|
||||||
response.Project = _mapper.Map<ProjectBasicMongoDB>(project);
|
response.Project = _mapper.Map<ProjectBasicMongoDB>(model.Project);
|
||||||
response.PaidBy = _mapper.Map<BasicEmployeeMongoDB>(paidBy);
|
response.PaidBy = _mapper.Map<BasicEmployeeMongoDB>(model.PaidBy);
|
||||||
response.CreatedBy = _mapper.Map<BasicEmployeeMongoDB>(createdBy);
|
response.CreatedBy = _mapper.Map<BasicEmployeeMongoDB>(model.CreatedBy);
|
||||||
response.ReviewedBy = _mapper.Map<BasicEmployeeMongoDB>(reviewedBy);
|
response.ReviewedBy = _mapper.Map<BasicEmployeeMongoDB>(model.ReviewedBy);
|
||||||
response.ApprovedBy = _mapper.Map<BasicEmployeeMongoDB>(approvedBy);
|
response.ApprovedBy = _mapper.Map<BasicEmployeeMongoDB>(model.ApprovedBy);
|
||||||
response.ProcessedBy = _mapper.Map<BasicEmployeeMongoDB>(processedBy);
|
response.ProcessedBy = _mapper.Map<BasicEmployeeMongoDB>(model.ProcessedBy);
|
||||||
if (statusMapping != null)
|
if (statusMapping != null)
|
||||||
{
|
{
|
||||||
response.Status = _mapper.Map<ExpensesStatusMasterMongoDB>(statusMapping.Status);
|
response.Status = _mapper.Map<ExpensesStatusMasterMongoDB>(statusMapping.Status);
|
||||||
@ -1337,8 +1292,8 @@ namespace Marco.Pms.Services.Service
|
|||||||
var status = statusTask.Result;
|
var status = statusTask.Result;
|
||||||
response.Status = _mapper.Map<ExpensesStatusMasterMongoDB>(status);
|
response.Status = _mapper.Map<ExpensesStatusMasterMongoDB>(status);
|
||||||
}
|
}
|
||||||
response.PaymentMode = _mapper.Map<PaymentModeMatserMongoDB>(paymentMode);
|
response.PaymentMode = _mapper.Map<PaymentModeMatserMongoDB>(model.PaymentMode);
|
||||||
response.ExpensesType = _mapper.Map<ExpensesTypeMasterMongoDB>(expenseType);
|
response.ExpensesType = _mapper.Map<ExpensesTypeMasterMongoDB>(model.ExpensesType);
|
||||||
if (billAttachment != null) response.Documents = billAttachment.Documents;
|
if (billAttachment != null) response.Documents = billAttachment.Documents;
|
||||||
|
|
||||||
return response;
|
return response;
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
using Marco.Pms.Model.Forum;
|
using Marco.Pms.Model.DocumentManager;
|
||||||
|
using Marco.Pms.Model.Forum;
|
||||||
using Marco.Pms.Model.Master;
|
using Marco.Pms.Model.Master;
|
||||||
|
|
||||||
namespace Marco.Pms.Services.Service
|
namespace Marco.Pms.Services.Service
|
||||||
@ -335,6 +336,194 @@ namespace Marco.Pms.Services.Service
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
public List<DocumentCategoryMaster> GetDocumentCategoryData(Guid tenantId)
|
||||||
|
{
|
||||||
|
return new List<DocumentCategoryMaster> {
|
||||||
|
new DocumentCategoryMaster
|
||||||
|
{
|
||||||
|
Id = Guid.NewGuid(),
|
||||||
|
Name = "Project Documents",
|
||||||
|
Description = "Project documents are formal records that outline the plans, progress, and details necessary to execute and manage a project effectively.",
|
||||||
|
EntityTypeId = Guid.Parse("c8fe7115-aa27-43bc-99f4-7b05fabe436e"),
|
||||||
|
CreatedAt = new DateTime(2025, 9, 15, 12, 42, 3, 202, DateTimeKind.Utc),
|
||||||
|
TenantId = tenantId
|
||||||
|
},
|
||||||
|
new DocumentCategoryMaster
|
||||||
|
{
|
||||||
|
Id = Guid.NewGuid(),
|
||||||
|
Name = "Employee Documents",
|
||||||
|
Description = "Employment details along with legal IDs like passports or driver’s licenses to verify identity and work authorization.",
|
||||||
|
EntityTypeId = Guid.Parse("dbb9555a-7a0c-40f2-a9ed-f0463f1ceed7"),
|
||||||
|
CreatedAt = new DateTime(2025, 9, 15, 12, 42, 3, 202, DateTimeKind.Utc),
|
||||||
|
TenantId = tenantId
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
public List<DocumentTypeMaster> GetDocumentTypeData(Guid tenantId, Guid employeeDocumentId, Guid projectDocumentId)
|
||||||
|
{
|
||||||
|
return new List<DocumentTypeMaster> {
|
||||||
|
new DocumentTypeMaster
|
||||||
|
{
|
||||||
|
Id = Guid.NewGuid(),
|
||||||
|
Name = "Aadhaar card",
|
||||||
|
RegexExpression = "^[2-9][0-9]{11}$",
|
||||||
|
AllowedContentType = "application/pdf,image/jpeg",
|
||||||
|
MaxSizeAllowedInMB = 2,
|
||||||
|
IsValidationRequired = true,
|
||||||
|
IsMandatory = true,
|
||||||
|
CreatedAt = new DateTime(2025, 9, 3, 10, 46, 49, 955, DateTimeKind.Utc),
|
||||||
|
IsSystem = true,
|
||||||
|
IsActive = true,
|
||||||
|
DocumentCategoryId = employeeDocumentId,
|
||||||
|
TenantId = tenantId
|
||||||
|
},
|
||||||
|
new DocumentTypeMaster
|
||||||
|
{
|
||||||
|
Id = Guid.NewGuid(),
|
||||||
|
Name = "Pan Card",
|
||||||
|
RegexExpression = "^[A-Z]{5}[0-9]{4}[A-Z]{1}$",
|
||||||
|
AllowedContentType = "application/pdf,image/jpeg",
|
||||||
|
MaxSizeAllowedInMB = 2,
|
||||||
|
IsValidationRequired = true,
|
||||||
|
IsMandatory = true,
|
||||||
|
CreatedAt = new DateTime(2025, 9, 3, 10, 46, 49, 955, DateTimeKind.Utc),
|
||||||
|
IsSystem = true,
|
||||||
|
IsActive = true,
|
||||||
|
DocumentCategoryId = employeeDocumentId,
|
||||||
|
TenantId = tenantId
|
||||||
|
},
|
||||||
|
new DocumentTypeMaster
|
||||||
|
{
|
||||||
|
Id = Guid.NewGuid(),
|
||||||
|
Name = "Voter Card",
|
||||||
|
RegexExpression = "^[A-Z]{3}[0-9]{7}$",
|
||||||
|
AllowedContentType = "application/pdf,image/jpeg",
|
||||||
|
MaxSizeAllowedInMB = 2,
|
||||||
|
IsValidationRequired = true,
|
||||||
|
IsMandatory = true,
|
||||||
|
CreatedAt = new DateTime(2025, 9, 3, 10, 46, 49, 955, DateTimeKind.Utc),
|
||||||
|
IsSystem = true,
|
||||||
|
IsActive = true,
|
||||||
|
DocumentCategoryId = employeeDocumentId,
|
||||||
|
TenantId = tenantId
|
||||||
|
},
|
||||||
|
new DocumentTypeMaster
|
||||||
|
{
|
||||||
|
Id = Guid.NewGuid(),
|
||||||
|
Name = "Passport",
|
||||||
|
RegexExpression = "^[A-PR-WY][1-9]\\d\\s?\\d{4}[1-9]$",
|
||||||
|
AllowedContentType = "application/pdf,image/jpeg",
|
||||||
|
MaxSizeAllowedInMB = 2,
|
||||||
|
IsValidationRequired = true,
|
||||||
|
IsMandatory = true,
|
||||||
|
CreatedAt = new DateTime(2025, 9, 3, 10, 46, 49, 955, DateTimeKind.Utc),
|
||||||
|
IsSystem = true,
|
||||||
|
IsActive = true,
|
||||||
|
DocumentCategoryId = employeeDocumentId,
|
||||||
|
TenantId = tenantId
|
||||||
|
},
|
||||||
|
new DocumentTypeMaster
|
||||||
|
{
|
||||||
|
Id = Guid.NewGuid(),
|
||||||
|
Name = "Bank Passbook",
|
||||||
|
RegexExpression = "^\\d{9,18}$",
|
||||||
|
AllowedContentType = "application/pdf,image/jpeg",
|
||||||
|
MaxSizeAllowedInMB = 2,
|
||||||
|
IsValidationRequired = true,
|
||||||
|
IsMandatory = true,
|
||||||
|
CreatedAt = new DateTime(2025, 9, 3, 10, 46, 49, 955, DateTimeKind.Utc),
|
||||||
|
IsSystem = true,
|
||||||
|
IsActive = true,
|
||||||
|
DocumentCategoryId = employeeDocumentId,
|
||||||
|
TenantId = tenantId
|
||||||
|
},
|
||||||
|
|
||||||
|
new DocumentTypeMaster
|
||||||
|
{
|
||||||
|
Id = Guid.NewGuid(),
|
||||||
|
Name = "Bill of Quantities (BOQ)",
|
||||||
|
AllowedContentType = "application/pdf,application/msword,application/vnd.ms-excel,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet,application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
||||||
|
MaxSizeAllowedInMB = 1,
|
||||||
|
IsValidationRequired = false,
|
||||||
|
IsMandatory = false,
|
||||||
|
CreatedAt = new DateTime(2025, 9, 3, 10, 46, 49, 955, DateTimeKind.Utc),
|
||||||
|
IsSystem = true,
|
||||||
|
IsActive = true,
|
||||||
|
DocumentCategoryId = projectDocumentId,
|
||||||
|
TenantId = tenantId
|
||||||
|
},
|
||||||
|
new DocumentTypeMaster
|
||||||
|
{
|
||||||
|
Id = Guid.NewGuid(),
|
||||||
|
Name = "Work Order",
|
||||||
|
AllowedContentType = "application/pdf,application/msword,application/vnd.ms-excel,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet,application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
||||||
|
MaxSizeAllowedInMB = 1,
|
||||||
|
IsValidationRequired = false,
|
||||||
|
IsMandatory = false,
|
||||||
|
CreatedAt = new DateTime(2025, 9, 3, 10, 46, 49, 955, DateTimeKind.Utc),
|
||||||
|
IsSystem = true,
|
||||||
|
IsActive = true,
|
||||||
|
DocumentCategoryId = projectDocumentId,
|
||||||
|
TenantId = tenantId
|
||||||
|
},
|
||||||
|
new DocumentTypeMaster
|
||||||
|
{
|
||||||
|
Id = Guid.NewGuid(),
|
||||||
|
Name = "Letter of Agreement",
|
||||||
|
AllowedContentType = "application/pdf,application/msword,application/vnd.ms-excel,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet,application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
||||||
|
MaxSizeAllowedInMB = 1,
|
||||||
|
IsValidationRequired = false,
|
||||||
|
IsMandatory = false,
|
||||||
|
CreatedAt = new DateTime(2025, 9, 3, 10, 46, 49, 955, DateTimeKind.Utc),
|
||||||
|
IsSystem = true,
|
||||||
|
IsActive = true,
|
||||||
|
DocumentCategoryId = projectDocumentId,
|
||||||
|
TenantId = tenantId
|
||||||
|
},
|
||||||
|
new DocumentTypeMaster
|
||||||
|
{
|
||||||
|
Id = Guid.NewGuid(),
|
||||||
|
Name = "Health and Safety Document",
|
||||||
|
AllowedContentType = "application/pdf,application/msword,application/vnd.ms-excel,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet,application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
||||||
|
MaxSizeAllowedInMB = 1,
|
||||||
|
IsValidationRequired = false,
|
||||||
|
IsMandatory = false,
|
||||||
|
CreatedAt = new DateTime(2025, 9, 3, 10, 46, 49, 955, DateTimeKind.Utc),
|
||||||
|
IsSystem = true,
|
||||||
|
IsActive = true,
|
||||||
|
DocumentCategoryId = projectDocumentId,
|
||||||
|
TenantId = tenantId
|
||||||
|
},
|
||||||
|
new DocumentTypeMaster
|
||||||
|
{
|
||||||
|
Id = Guid.NewGuid(),
|
||||||
|
Name = "Standard Operating Procedure (SOP)",
|
||||||
|
AllowedContentType = "application/pdf,application/msword,application/vnd.ms-excel,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet,application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
||||||
|
MaxSizeAllowedInMB = 1,
|
||||||
|
IsValidationRequired = false,
|
||||||
|
IsMandatory = false,
|
||||||
|
CreatedAt = new DateTime(2025, 9, 3, 10, 46, 49, 955, DateTimeKind.Utc),
|
||||||
|
IsSystem = true,
|
||||||
|
IsActive = true,
|
||||||
|
DocumentCategoryId = projectDocumentId,
|
||||||
|
TenantId = tenantId
|
||||||
|
},
|
||||||
|
new DocumentTypeMaster
|
||||||
|
{
|
||||||
|
Id = Guid.NewGuid(),
|
||||||
|
Name = "Drawings",
|
||||||
|
AllowedContentType = "application/pdf,image/vnd.dwg,application/acad",
|
||||||
|
MaxSizeAllowedInMB = 20,
|
||||||
|
IsValidationRequired = false,
|
||||||
|
IsMandatory = false,
|
||||||
|
CreatedAt = new DateTime(2025, 9, 3, 10, 46, 49, 955, DateTimeKind.Utc),
|
||||||
|
IsSystem = true,
|
||||||
|
IsActive = true,
|
||||||
|
DocumentCategoryId = projectDocumentId,
|
||||||
|
TenantId = tenantId
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
public List<object> GetData(Guid tenantId)
|
public List<object> GetData(Guid tenantId)
|
||||||
{
|
{
|
||||||
return new List<object>
|
return new List<object>
|
||||||
|
@ -67,7 +67,7 @@ namespace Marco.Pms.Services.Service
|
|||||||
_logger.LogInfo("Basic project list requested by EmployeeId {EmployeeId}", loggedInEmployee.Id);
|
_logger.LogInfo("Basic project list requested by EmployeeId {EmployeeId}", loggedInEmployee.Id);
|
||||||
|
|
||||||
// Step 2: Get the list of project IDs the user has access to
|
// Step 2: Get the list of project IDs the user has access to
|
||||||
List<Guid> accessibleProjectIds = await GetMyProjects(tenantId, loggedInEmployee);
|
List<Guid> accessibleProjectIds = await _context.Projects.Where(p => p.TenantId == tenantId).Select(p => p.Id).ToListAsync();
|
||||||
|
|
||||||
if (accessibleProjectIds == null || !accessibleProjectIds.Any())
|
if (accessibleProjectIds == null || !accessibleProjectIds.Any())
|
||||||
{
|
{
|
||||||
@ -96,7 +96,7 @@ namespace Marco.Pms.Services.Service
|
|||||||
_logger.LogInfo("Starting GetAllProjects for TenantId: {TenantId}, User: {UserId}", tenantId, loggedInEmployee.Id);
|
_logger.LogInfo("Starting GetAllProjects for TenantId: {TenantId}, User: {UserId}", tenantId, loggedInEmployee.Id);
|
||||||
|
|
||||||
// --- Step 1: Get a list of project IDs the user can access ---
|
// --- Step 1: Get a list of project IDs the user can access ---
|
||||||
List<Guid> projectIds = await GetMyProjects(tenantId, loggedInEmployee);
|
List<Guid> projectIds = await _context.Projects.Where(p => p.TenantId == tenantId).Select(p => p.Id).ToListAsync();
|
||||||
if (!projectIds.Any())
|
if (!projectIds.Any())
|
||||||
{
|
{
|
||||||
_logger.LogInfo("User has no assigned projects. Returning empty list.");
|
_logger.LogInfo("User has no assigned projects. Returning empty list.");
|
||||||
@ -201,21 +201,21 @@ namespace Marco.Pms.Services.Service
|
|||||||
using var scope = _serviceScopeFactory.CreateScope();
|
using var scope = _serviceScopeFactory.CreateScope();
|
||||||
var _permission = scope.ServiceProvider.GetRequiredService<PermissionServices>();
|
var _permission = scope.ServiceProvider.GetRequiredService<PermissionServices>();
|
||||||
|
|
||||||
// Step 1: Check global view project permission
|
//// Step 1: Check global view project permission
|
||||||
var hasViewProjectPermission = await _permission.HasPermission(PermissionsMaster.ViewProject, loggedInEmployee.Id, id);
|
//var hasViewProjectPermission = await _permission.HasPermission(PermissionsMaster.ViewProject, loggedInEmployee.Id, id);
|
||||||
if (!hasViewProjectPermission)
|
//if (!hasViewProjectPermission)
|
||||||
{
|
//{
|
||||||
_logger.LogWarning("ViewProjects permission denied for EmployeeId: {EmployeeId}", loggedInEmployee.Id);
|
// _logger.LogWarning("ViewProjects permission denied for EmployeeId: {EmployeeId}", loggedInEmployee.Id);
|
||||||
return ApiResponse<object>.ErrorResponse("Access denied", "You don't have permission to view projects", 403);
|
// return ApiResponse<object>.ErrorResponse("Access denied", "You don't have permission to view projects", 403);
|
||||||
}
|
//}
|
||||||
|
|
||||||
// Step 2: Check permission for this specific project
|
//// Step 2: Check permission for this specific project
|
||||||
var hasProjectPermission = await _permission.HasProjectPermission(loggedInEmployee, id);
|
//var hasProjectPermission = await _permission.HasProjectPermission(loggedInEmployee, id);
|
||||||
if (!hasProjectPermission)
|
//if (!hasProjectPermission)
|
||||||
{
|
//{
|
||||||
_logger.LogWarning("Project-specific access denied. EmployeeId: {EmployeeId}, ProjectId: {ProjectId}", loggedInEmployee.Id, id);
|
// _logger.LogWarning("Project-specific access denied. EmployeeId: {EmployeeId}, ProjectId: {ProjectId}", loggedInEmployee.Id, id);
|
||||||
return ApiResponse<object>.ErrorResponse("Access denied", "You don't have access to this project", 403);
|
// return ApiResponse<object>.ErrorResponse("Access denied", "You don't have access to this project", 403);
|
||||||
}
|
//}
|
||||||
|
|
||||||
// Step 3: Fetch project with status
|
// Step 3: Fetch project with status
|
||||||
var projectDetails = await _cache.GetProjectDetails(id);
|
var projectDetails = await _cache.GetProjectDetails(id);
|
||||||
@ -368,17 +368,20 @@ namespace Marco.Pms.Services.Service
|
|||||||
return ApiResponse<object>.ErrorResponse("Access Denied", "You do not have permission to create a project for this tenant.", 403);
|
return ApiResponse<object>.ErrorResponse("Access Denied", "You do not have permission to create a project for this tenant.", 403);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var promoterId = model.PromoterId ?? loggedInEmployee.OrganizationId;
|
||||||
|
var pmcId = model.PMCId ?? loggedInEmployee.OrganizationId;
|
||||||
|
|
||||||
// Step 2: Concurrent validation for Promoter and PMC organization existence.
|
// Step 2: Concurrent validation for Promoter and PMC organization existence.
|
||||||
// Run database queries in parallel for better performance.
|
// Run database queries in parallel for better performance.
|
||||||
var promoterTask = Task.Run(async () =>
|
var promoterTask = Task.Run(async () =>
|
||||||
{
|
{
|
||||||
await using var context = await _dbContextFactory.CreateDbContextAsync();
|
await using var context = await _dbContextFactory.CreateDbContextAsync();
|
||||||
return await context.Organizations.FirstOrDefaultAsync(o => o.Id == model.PromoterId);
|
return await context.Organizations.FirstOrDefaultAsync(o => o.Id == promoterId);
|
||||||
});
|
});
|
||||||
var pmcTask = Task.Run(async () =>
|
var pmcTask = Task.Run(async () =>
|
||||||
{
|
{
|
||||||
await using var context = await _dbContextFactory.CreateDbContextAsync();
|
await using var context = await _dbContextFactory.CreateDbContextAsync();
|
||||||
return await context.Organizations.FirstOrDefaultAsync(o => o.Id == model.PMCId);
|
return await context.Organizations.FirstOrDefaultAsync(o => o.Id == pmcId);
|
||||||
});
|
});
|
||||||
|
|
||||||
await Task.WhenAll(promoterTask, pmcTask);
|
await Task.WhenAll(promoterTask, pmcTask);
|
||||||
@ -388,18 +391,20 @@ namespace Marco.Pms.Services.Service
|
|||||||
|
|
||||||
if (promoter == null)
|
if (promoter == null)
|
||||||
{
|
{
|
||||||
_logger.LogWarning("Promoter check failed. PromoterId={PromoterId} not found.", model.PromoterId);
|
_logger.LogWarning("Promoter check failed. PromoterId={PromoterId} not found.", promoterId);
|
||||||
return ApiResponse<object>.ErrorResponse("Promoter not found", "Promoter not found in database.", 404);
|
return ApiResponse<object>.ErrorResponse("Promoter not found", "Promoter not found in database.", 404);
|
||||||
}
|
}
|
||||||
if (pmc == null)
|
if (pmc == null)
|
||||||
{
|
{
|
||||||
_logger.LogWarning("PMC check failed. PMCId={PMCId} not found.", model.PMCId);
|
_logger.LogWarning("PMC check failed. PMCId={PMCId} not found.", pmcId);
|
||||||
return ApiResponse<object>.ErrorResponse("PMC not found", "PMC not found in database.", 404);
|
return ApiResponse<object>.ErrorResponse("PMC not found", "PMC not found in database.", 404);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Step 3: Prepare the project entity.
|
// Step 3: Prepare the project entity.
|
||||||
var loggedInUserId = loggedInEmployee.Id;
|
var loggedInUserId = loggedInEmployee.Id;
|
||||||
var project = _mapper.Map<Project>(model);
|
var project = _mapper.Map<Project>(model);
|
||||||
|
project.PromoterId = promoterId;
|
||||||
|
project.PMCId = pmcId;
|
||||||
project.TenantId = tenantId;
|
project.TenantId = tenantId;
|
||||||
|
|
||||||
// Step 4: Save the new project to the database.
|
// Step 4: Save the new project to the database.
|
||||||
@ -476,6 +481,7 @@ namespace Marco.Pms.Services.Service
|
|||||||
// --- Step 1: Fetch the Existing Entity from the Database ---
|
// --- Step 1: Fetch the Existing Entity from the Database ---
|
||||||
// This is crucial to avoid the data loss bug. We only want to modify an existing record.
|
// This is crucial to avoid the data loss bug. We only want to modify an existing record.
|
||||||
var existingProject = await _context.Projects
|
var existingProject = await _context.Projects
|
||||||
|
.AsNoTracking()
|
||||||
.Where(p => p.Id == id && p.TenantId == tenantId)
|
.Where(p => p.Id == id && p.TenantId == tenantId)
|
||||||
.SingleOrDefaultAsync();
|
.SingleOrDefaultAsync();
|
||||||
|
|
||||||
@ -496,17 +502,20 @@ namespace Marco.Pms.Services.Service
|
|||||||
return ApiResponse<object>.ErrorResponse("Access Denied", "You do not have permission to update a project for this tenant.", 403);
|
return ApiResponse<object>.ErrorResponse("Access Denied", "You do not have permission to update a project for this tenant.", 403);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var promoterId = model.PromoterId ?? loggedInEmployee.OrganizationId;
|
||||||
|
var pmcId = model.PMCId ?? loggedInEmployee.OrganizationId;
|
||||||
|
|
||||||
// 1bb. Concurrent validation for Promoter and PMC organization existence.
|
// 1bb. Concurrent validation for Promoter and PMC organization existence.
|
||||||
// Run database queries in parallel for better performance.
|
// Run database queries in parallel for better performance.
|
||||||
var promoterTask = Task.Run(async () =>
|
var promoterTask = Task.Run(async () =>
|
||||||
{
|
{
|
||||||
await using var context = await _dbContextFactory.CreateDbContextAsync();
|
await using var context = await _dbContextFactory.CreateDbContextAsync();
|
||||||
return await context.Organizations.FirstOrDefaultAsync(o => o.Id == model.PromoterId);
|
return await context.Organizations.FirstOrDefaultAsync(o => o.Id == promoterId);
|
||||||
});
|
});
|
||||||
var pmcTask = Task.Run(async () =>
|
var pmcTask = Task.Run(async () =>
|
||||||
{
|
{
|
||||||
await using var context = await _dbContextFactory.CreateDbContextAsync();
|
await using var context = await _dbContextFactory.CreateDbContextAsync();
|
||||||
return await context.Organizations.FirstOrDefaultAsync(o => o.Id == model.PMCId);
|
return await context.Organizations.FirstOrDefaultAsync(o => o.Id == pmcId);
|
||||||
});
|
});
|
||||||
|
|
||||||
await Task.WhenAll(promoterTask, pmcTask);
|
await Task.WhenAll(promoterTask, pmcTask);
|
||||||
@ -516,12 +525,12 @@ namespace Marco.Pms.Services.Service
|
|||||||
|
|
||||||
if (promoter == null)
|
if (promoter == null)
|
||||||
{
|
{
|
||||||
_logger.LogWarning("Promoter check failed. PromoterId={PromoterId} not found.", model.PromoterId);
|
_logger.LogWarning("Promoter check failed. PromoterId={PromoterId} not found.", promoterId);
|
||||||
return ApiResponse<object>.ErrorResponse("Promoter not found", "Promoter not found in database.", 404);
|
return ApiResponse<object>.ErrorResponse("Promoter not found", "Promoter not found in database.", 404);
|
||||||
}
|
}
|
||||||
if (pmc == null)
|
if (pmc == null)
|
||||||
{
|
{
|
||||||
_logger.LogWarning("PMC check failed. PMCId={PMCId} not found.", model.PMCId);
|
_logger.LogWarning("PMC check failed. PMCId={PMCId} not found.", pmcId);
|
||||||
return ApiResponse<object>.ErrorResponse("PMC not found", "PMC not found in database.", 404);
|
return ApiResponse<object>.ErrorResponse("PMC not found", "PMC not found in database.", 404);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -538,8 +547,11 @@ namespace Marco.Pms.Services.Service
|
|||||||
// This only modifies the properties defined in the mapping, preventing data loss.
|
// This only modifies the properties defined in the mapping, preventing data loss.
|
||||||
_mapper.Map(model, existingProject);
|
_mapper.Map(model, existingProject);
|
||||||
|
|
||||||
|
existingProject.PromoterId = promoterId;
|
||||||
|
existingProject.PMCId = pmcId;
|
||||||
|
|
||||||
// Mark the entity as modified (if your mapping doesn't do it automatically).
|
// Mark the entity as modified (if your mapping doesn't do it automatically).
|
||||||
_context.Entry(existingProject).State = EntityState.Modified;
|
_context.Projects.Update(existingProject);
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
@ -48,7 +48,7 @@
|
|||||||
},
|
},
|
||||||
"MongoDB": {
|
"MongoDB": {
|
||||||
"SerilogDatabaseUrl": "mongodb://localhost:27017/DotNetLogs",
|
"SerilogDatabaseUrl": "mongodb://localhost:27017/DotNetLogs",
|
||||||
"ConnectionString": "mongodb://localhost:27017/MarcoBMS_Caches?socketTimeoutMS=500&serverSelectionTimeoutMS=500&connectTimeoutMS=500",
|
"ConnectionString": "mongodb://devuser:DevPass123@147.93.98.152:27017/MarcoBMSCacheLocalDev?authSource=admin&replicaSet=rs01",
|
||||||
"ModificationConnectionString": "mongodb://devuser:DevPass123@147.93.98.152:27017/MarcoBMSLocalDev?authSource=admin&eplicaSet=rs01&directConnection=true"
|
"ModificationConnectionString": "mongodb://devuser:DevPass123@147.93.98.152:27017/MarcoBMSLocalDev?authSource=admin&replicaSet=rs01&directConnection=true"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user