Compare commits

..

36 Commits

Author SHA1 Message Date
7e20807325 corrected the distinct by error 2025-10-11 15:57:53 +05:30
186486d934 Added the new API to get organizations for dropdown 2025-10-11 15:36:52 +05:30
d07f0311ae Optmized the work status master APIs 2025-10-11 13:52:20 +05:30
2f6031e62c revert e02636b6b66287a22fcfb34c150aece1b136cc68
revert Optmized the work status master APIs
2025-10-11 08:19:59 +00:00
e02636b6b6 Optmized the work status master APIs 2025-10-11 13:48:36 +05:30
bad784e147 Optimized the contact related msater APIs 2025-10-11 12:59:31 +05:30
886d0bb3b1 Clearing the employee profile cache when selecting the tenant 2025-10-10 18:32:01 +05:30
74dd9eeb8d organization Tenant Mapping is already existed then return error message 2025-10-10 17:53:23 +05:30
1939a63d9a Assigning the appilcation role to root employee of the any organization 2025-10-10 17:05:43 +05:30
c07db9f94d removed employee project cache when adding the subscription 2025-10-10 15:59:42 +05:30
53da15416a Removing all employees profile from cache for certain tenant 2025-10-10 15:46:56 +05:30
a809bdd469 Merge pull request 'Sloved issues of permissions not been assigned when updating the subscription' (#144) from Ashutosh_Bug#1461 into main
Reviewed-on: #144
2025-10-10 08:49:19 +00:00
bb2c098cb9 Sloved issues of permissions not been assigned when updating the subscription 2025-10-10 14:17:32 +05:30
bd3b70f4ea Merge pull request 'Added the RequestedAt and RequestedBy in attendance' (#143) from Ashutosh_Enhancement#1452 into main
Reviewed-on: #143
2025-10-10 07:06:25 +00:00
000cef3bb7 enhanced the get team based on project 2025-10-10 12:36:11 +05:30
522deae8f7 Added the RequestedAt and RequestedBy in attendance 2025-10-10 12:08:53 +05:30
9a8aa4f5ce Changed the index of reject button in expense controller 2025-10-09 16:52:34 +05:30
af92ab977b Added the get filter API 2025-10-09 16:47:06 +05:30
c06dc8ebe7 Selecting the default services in project allocation 2025-10-09 16:00:26 +05:30
8609db64d2 Made the serviceId is nullable in project allocation 2025-10-09 15:23:37 +05:30
e831f50505 Removed the logic to save the FCM from verify FCM API 2025-10-08 11:33:34 +05:30
ba88fbced6 Removed the FCM token from MPIN verify API 2025-10-08 11:27:30 +05:30
13d2e1cd7d Corrected the mistake of showing all images except selected project 2025-10-07 12:34:48 +05:30
26acfec408 Added the todays completed tasks 2025-10-06 18:33:37 +05:30
3bd38f3c68 Changed the logic in API to get Attendance logs by employee 2025-10-06 16:10:58 +05:30
2e29dc9946 Chnaged the check for persentages 2025-10-06 14:42:29 +05:30
db752a4678 Merge branch 'main' of https://git.marcoaiot.com/admin/marco.pms.api 2025-10-06 13:16:31 +05:30
245182eb07 Added the attendance persentage and task persentage 2025-10-06 13:15:48 +05:30
9daf76c6eb return all employees when allEmployee=true 2025-10-05 00:24:26 +05:30
c33afa58c3 Solved spelling mistake 2025-10-04 17:51:58 +05:30
4ceb5c3cb2 Changed the authontication logic for delete contact notes 2025-10-04 17:50:18 +05:30
d0dabf776b Added the condition to not get services object in project allocation 2025-10-04 17:26:08 +05:30
04223578ad Added the ExpenseUId in expenses tables 2025-10-04 16:56:04 +05:30
f94a7de4ab change email in complete system 2025-10-01 17:46:11 +05:30
eb3a65428e Modyfied the attendance report API 2025-10-01 15:09:51 +05:30
8ddb414e91 Added the organization type in assignd organization list 2025-09-30 18:08:07 +05:30
63 changed files with 1062 additions and 3121 deletions

View File

@ -1,7 +1,6 @@
using Marco.Pms.Model.Activities;
using Marco.Pms.Model.AttendanceModule;
using Marco.Pms.Model.Authentication;
using Marco.Pms.Model.Collection;
using Marco.Pms.Model.Directory;
using Marco.Pms.Model.DocumentManager;
using Marco.Pms.Model.Employees;
@ -134,13 +133,6 @@ namespace Marco.Pms.DataAccess.Data
public DbSet<ProjectServiceMapping> ProjectServiceMappings { get; set; }
public DbSet<ProjectOrgMapping> ProjectOrgMappings { get; set; }
// Collection
public DbSet<Invoice> Invoices { get; set; }
public DbSet<InvoiceComment> InvoiceComments { get; set; }
public DbSet<InvoiceAttachment> InvoiceAttachments { get; set; }
public DbSet<ReceivedInvoicePayment> ReceivedInvoicePayments { get; set; }
public DbSet<PaymentAdjustmentHead> PaymentAdjustmentHeads { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
@ -783,65 +775,6 @@ namespace Marco.Pms.DataAccess.Data
}
);
modelBuilder.Entity<PaymentAdjustmentHead>().HasData(
new PaymentAdjustmentHead
{
Id = Guid.Parse("dbdc047f-a2d2-4db0-b0e6-b9d9f923a0f1"),
Name = "Advance payment",
Description = "An advance payment is a sum paid before receiving goods or services, often to secure a transaction or cover initial costs.",
IsActive = true,
TenantId = Guid.Parse("b3466e83-7e11-464c-b93a-daf047838b26")
},
new PaymentAdjustmentHead
{
Id = Guid.Parse("66c3c241-8b52-4327-a5ad-c1faf102583e"),
Name = "Base Amount",
Description = "The base amount refers to the principal sum or original value used as a reference in financial calculations, excluding taxes, fees, or additional charges.",
IsActive = true,
TenantId = Guid.Parse("b3466e83-7e11-464c-b93a-daf047838b26")
},
new PaymentAdjustmentHead
{
Id = Guid.Parse("0d70cb2e-827e-44fc-90a5-c2c55ba51ba9"),
Name = "Tax Deducted at Source (TDS)",
Description = "TDS, or Tax Deducted at Source, is a system under the Indian Income Tax Act where tax is deducted at the point of income generation—such as salary, interest, or rent—and remitted to the government to prevent tax evasion and ensure timely collection.",
IsActive = true,
TenantId = Guid.Parse("b3466e83-7e11-464c-b93a-daf047838b26")
},
new PaymentAdjustmentHead
{
Id = Guid.Parse("95f35acd-d979-4177-91ea-fd03a00e49ff"),
Name = "Retention",
Description = "Retention refers to a company's ability to keep customers, employees, or profits over time, commonly measured as a percentage and critical for long-term business sustainability and growth.",
IsActive = true,
TenantId = Guid.Parse("b3466e83-7e11-464c-b93a-daf047838b26")
},
new PaymentAdjustmentHead
{
Id = Guid.Parse("3f09b19a-8d45-4cf2-be27-f4f09b38b9f7"),
Name = "Tax",
Description = "Tax is a mandatory financial charge imposed by a government on individuals or entities to fund public services and government operations, without direct benefit to the taxpayer.",
IsActive = true,
TenantId = Guid.Parse("b3466e83-7e11-464c-b93a-daf047838b26")
},
new PaymentAdjustmentHead
{
Id = Guid.Parse("ec5e6a5f-ce62-44e5-8911-8426bbb4dde8"),
Name = "Penalty",
Description = "A penalty in the context of taxation is a financial sanction imposed by the government on individuals or entities for non-compliance with tax laws, such as late filing, underreporting income, or failure to pay taxes, and is typically calculated as a percentage of the tax due or a fixed amount.",
IsActive = true,
TenantId = Guid.Parse("b3466e83-7e11-464c-b93a-daf047838b26")
},
new PaymentAdjustmentHead
{
Id = Guid.Parse("50584332-1cb7-4359-9721-c8ea35040881"),
Name = "Utility fees",
Description = "Utility fees are recurring charges for essential services such as electricity, water, gas, sewage, waste disposal, internet, and telecommunications, typically based on usage and necessary for operating a home or business.",
IsActive = true,
TenantId = Guid.Parse("b3466e83-7e11-464c-b93a-daf047838b26")
}
);
modelBuilder.Entity<EntityTypeMaster>().HasData(
new EntityTypeMaster
{
@ -1106,7 +1039,6 @@ namespace Marco.Pms.DataAccess.Data
// Project Module
new Feature { Id = new Guid("53176ebf-c75d-42e5-839f-4508ffac3def"), Description = "Manage Project", Name = "Project Management", ModuleId = new Guid("bf59fd88-b57a-4d67-bf01-3780f385896b"), IsActive = true },
new Feature { Id = new Guid("a4e25142-449b-4334-a6e5-22f70e4732d7"), Description = "Expense Management is the systematic process of tracking, controlling, and reporting business-related expenditures.", Name = "Expense Management", ModuleId = new Guid("bf59fd88-b57a-4d67-bf01-3780f385896b"), IsActive = true },
new Feature { Id = new Guid("fc586e7d-ed1a-45e5-bb51-9f34af98ec13"), Description = "Collection Management is a feature that enables organizations to track, organize, and manage the status and recovery of receivables or assets efficiently throughout their lifecycle, supporting systematic follow-up and resolution of outstanding accounts.", Name = "Collection Management", ModuleId = new Guid("bf59fd88-b57a-4d67-bf01-3780f385896b"), IsActive = true },
new Feature { Id = new Guid("9d4b5489-2079-40b9-bd77-6e1bf90bc19f"), Description = "Manage Tasks", Name = "Task Management", ModuleId = new Guid("bf59fd88-b57a-4d67-bf01-3780f385896b"), IsActive = true },
// Employee Module
@ -1178,13 +1110,6 @@ namespace Marco.Pms.DataAccess.Data
new FeaturePermission { Id = new Guid("ea5a1529-4ee8-4828-80ea-0e23c9d4dd11"), FeatureId = new Guid("a4e25142-449b-4334-a6e5-22f70e4732d7"), IsEnabled = true, Name = "Process", Description = "Allows a user to handle post-approval actions such as recording payments, updating financial records, or marking expenses as reimbursed or settled." },
new FeaturePermission { Id = new Guid("bdee29a2-b73b-402d-8dd1-c4b1f81ccbc3"), FeatureId = new Guid("a4e25142-449b-4334-a6e5-22f70e4732d7"), IsEnabled = true, Name = "Manage", Description = "Allows a user to configure and control system settings, such as managing expense types, payment modes, permissions, and overall workflow rules." },
// Collection Management Feature
new FeaturePermission { Id = new Guid("dbf17591-09fe-4c93-9e1a-12db8f5cc5de"), FeatureId = new Guid("fc586e7d-ed1a-45e5-bb51-9f34af98ec13"), IsEnabled = true, Name = "Collection Admin", Description = "Collection Admin is a permission that grants a user full administrative control over collections, including creating, editing, managing access, and deleting collections within a system." },
new FeaturePermission { Id = new Guid("c8d7eea5-4033-4aad-9ebe-76de49896830"), FeatureId = new Guid("fc586e7d-ed1a-45e5-bb51-9f34af98ec13"), IsEnabled = true, Name = "View Collection", Description = "View Collection is a permission that allows users to see and browse assets or items within a collection without making any modifications or edits to its contents." },
new FeaturePermission { Id = new Guid("b93141fd-dbd3-4051-8f57-bf25d18e3555"), FeatureId = new Guid("fc586e7d-ed1a-45e5-bb51-9f34af98ec13"), IsEnabled = true, Name = "Create Collection", Description = "Authorizes users to create new collections for organizing related resources and managing access" },
new FeaturePermission { Id = new Guid("455187b4-fef1-41f9-b3d0-025d0b6302c3"), FeatureId = new Guid("fc586e7d-ed1a-45e5-bb51-9f34af98ec13"), IsEnabled = true, Name = "Edit Collection", Description = "Ability to modify collection properties, content, and access rights." },
new FeaturePermission { Id = new Guid("061d9ccd-85b4-4cb0-be06-2f9f32cebb72"), FeatureId = new Guid("fc586e7d-ed1a-45e5-bb51-9f34af98ec13"), IsEnabled = true, Name = "Add Payment", Description = " Enables entry and processing of payment transactions." },
// Organization Management Feature
new FeaturePermission { Id = new Guid("068cb3c1-49c5-4746-9f29-1fce16e820ac"), FeatureId = new Guid("6d4c82d6-dbce-48ab-b8b8-f785f4d8c914"), IsEnabled = true, Name = "Add Organization", Description = "Allow user to create new organization" },
new FeaturePermission { Id = new Guid("c1ae1363-ab8a-4bd9-a9d1-8c2c6083873a"), FeatureId = new Guid("6d4c82d6-dbce-48ab-b8b8-f785f4d8c914"), IsEnabled = true, Name = "Edit Organization", Description = "Allow the user to update the basic information of the organization" },

View File

@ -12,7 +12,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
namespace Marco.Pms.DataAccess.Migrations
{
[DbContext(typeof(ApplicationDbContext))]
[Migration("20251003093145_Added_ExpenceUID_In_Expense_Table")]
[Migration("20251004112239_Added_ExpenceUID_In_Expense_Table")]
partial class Added_ExpenceUID_In_Expense_Table
{
/// <inheritdoc />

View File

@ -12,7 +12,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
namespace Marco.Pms.DataAccess.Migrations
{
[DbContext(typeof(ApplicationDbContext))]
[Migration("20251008121556_Added_Requested_In_Attendance_Table")]
[Migration("20251010062100_Added_Requested_In_Attendance_Table")]
partial class Added_Requested_In_Attendance_Table
{
/// <inheritdoc />

View File

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

View File

@ -19,8 +19,6 @@ namespace Marco.Pms.Model.AttendanceModule
public Guid ProjectID { get; set; }
public DateTime AttendanceDate { get; set; }
public DateTime? RequestedAt { get; set; }
public DateTime? ApprovedAt { get; set; }
public DateTime? InTime { get; set; }
public DateTime? OutTime { get; set; }
public bool IsApproved { get; set; }
@ -31,6 +29,8 @@ namespace Marco.Pms.Model.AttendanceModule
[ForeignKey("ApprovedById")]
[ValidateNever]
public Employee? Approver { get; set; }
public DateTime? RequestedAt { get; set; }
public DateTime? ApprovedAt { get; set; }
public Guid? RequestedById { get; set; }
[ForeignKey("RequestedById")]

View File

@ -1,41 +0,0 @@
using Marco.Pms.Model.Employees;
using Marco.Pms.Model.Projects;
using Marco.Pms.Model.Utilities;
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
using System.ComponentModel.DataAnnotations.Schema;
namespace Marco.Pms.Model.Collection
{
public class Invoice : TenantRelation
{
public Guid Id { get; set; }
public string Title { get; set; } = default!;
public string Description { get; set; } = default!;
public string InvoiceNumber { get; set; } = default!;
public string? EInvoiceNumber { get; set; }
public Guid ProjectId { get; set; }
[ValidateNever]
[ForeignKey("ProjectId")]
public Project? Project { get; set; }
public DateTime InvoiceDate { get; set; }
public DateTime ClientSubmitedDate { get; set; }
public DateTime ExceptedPaymentDate { get; set; }
public double BasicAmount { get; set; }
public double TaxAmount { get; set; }
public bool IsActive { get; set; } = true;
public bool MarkAsCompleted { get; set; } = true;
public DateTime CreatedAt { get; set; }
public Guid CreatedById { get; set; }
[ValidateNever]
[ForeignKey("CreatedById")]
public Employee? CreatedBy { get; set; }
public DateTime? UpdatedAt { get; set; }
public Guid? UpdatedById { get; set; }
[ValidateNever]
[ForeignKey("UpdatedById")]
public Employee? UpdatedBy { get; set; }
}
}

View File

@ -1,22 +0,0 @@
using Marco.Pms.Model.DocumentManager;
using Marco.Pms.Model.Utilities;
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
using System.ComponentModel.DataAnnotations.Schema;
namespace Marco.Pms.Model.Collection
{
public class InvoiceAttachment : TenantRelation
{
public Guid Id { get; set; }
public Guid InvoiceId { get; set; }
[ValidateNever]
[ForeignKey("InvoiceId")]
public Invoice? Invoice { get; set; }
public Guid DocumentId { get; set; }
[ValidateNever]
[ForeignKey("DocumentId")]
public Document? Document { get; set; }
}
}

View File

@ -1,24 +0,0 @@
using Marco.Pms.Model.Employees;
using Marco.Pms.Model.Utilities;
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
using System.ComponentModel.DataAnnotations.Schema;
namespace Marco.Pms.Model.Collection
{
public class InvoiceComment : TenantRelation
{
public Guid Id { get; set; }
public string Comment { get; set; } = default!;
public DateTime CreatedAt { get; set; }
public Guid CreatedById { get; set; }
[ValidateNever]
[ForeignKey("CreatedById")]
public Employee? CreatedBy { get; set; }
public Guid InvoiceId { get; set; }
[ValidateNever]
[ForeignKey("InvoiceId")]
public Invoice? Invoice { get; set; }
}
}

View File

@ -1,12 +0,0 @@
using Marco.Pms.Model.Utilities;
namespace Marco.Pms.Model.Collection
{
public class PaymentAdjustmentHead : TenantRelation
{
public Guid Id { get; set; }
public string Name { get; set; } = default!;
public string? Description { get; set; }
public bool IsActive { get; set; } = true;
}
}

View File

@ -1,33 +0,0 @@
using Marco.Pms.Model.Employees;
using Marco.Pms.Model.Utilities;
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
using System.ComponentModel.DataAnnotations.Schema;
namespace Marco.Pms.Model.Collection
{
public class ReceivedInvoicePayment : TenantRelation
{
public Guid Id { get; set; }
public Guid InvoiceId { get; set; }
[ValidateNever]
[ForeignKey("InvoiceId")]
public Invoice? Invoice { get; set; }
public DateTime PaymentReceivedDate { get; set; }
public string TransactionId { get; set; } = default!;
public double Amount { get; set; }
public string Comment { get; set; } = default!;
public bool IsActive { get; set; } = true;
public Guid PaymentAdjustmentHeadId { get; set; }
[ValidateNever]
[ForeignKey("PaymentAdjustmentHeadId")]
public PaymentAdjustmentHead? PaymentAdjustmentHead { get; set; }
public DateTime CreatedAt { get; set; }
public Guid CreatedById { get; set; }
[ValidateNever]
[ForeignKey("CreatedById")]
public Employee? CreatedBy { get; set; }
}
}

View File

@ -1,8 +0,0 @@
namespace Marco.Pms.Model.Dtos.Collection
{
public class InvoiceCommentDto
{
public required string Comment { get; set; }
public required Guid InvoiceId { get; set; }
}
}

View File

@ -1,20 +0,0 @@
using Marco.Pms.Model.Utilities;
namespace Marco.Pms.Model.Dtos.Collection
{
public class InvoiceDto
{
public Guid? Id { get; set; }
public required string Title { get; set; }
public string? Description { get; set; }
public required string InvoiceNumber { get; set; }
public string? EInvoiceNumber { get; set; }
public required Guid ProjectId { get; set; }
public required DateTime InvoiceDate { get; set; }
public required DateTime ClientSubmitedDate { get; set; }
public required DateTime ExceptedPaymentDate { get; set; }
public double BasicAmount { get; set; }
public double TaxAmount { get; set; }
public List<FileUploadModel>? Attachments { get; set; }
}
}

View File

@ -1,9 +0,0 @@
namespace Marco.Pms.Model.Dtos.Collection
{
public class PaymentAdjustmentHeadDto
{
public Guid? Id { get; set; }
public required string Name { get; set; }
public string? Description { get; set; }
}
}

View File

@ -1,13 +0,0 @@
namespace Marco.Pms.Model.Dtos.Collection
{
public class ReceivedInvoicePaymentDto
{
public Guid? Id { get; set; }
public required Guid InvoiceId { get; set; }
public required DateTime PaymentReceivedDate { get; set; }
public required string TransactionId { get; set; }
public required Guid PaymentAdjustmentHeadId { get; set; }
public required double Amount { get; set; }
public required string Comment { get; set; }
}
}

View File

@ -19,7 +19,7 @@
public string? EmergencyPhoneNumber { get; set; }
public string? EmergencyContactPerson { get; set; }
public Guid JobRoleId { get; set; }
public Guid? OrganizationId { get; set; }
public required Guid OrganizationId { get; set; }
public required bool HasApplicationAccess { get; set; }
}
public class MobileUserManageDto
@ -33,7 +33,7 @@
public required string Gender { get; set; }
public Guid JobRoleId { get; set; }
public string? ProfileImage { get; set; }
public Guid? OrganizationId { get; set; }
public required Guid OrganizationId { get; set; }
public required bool HasApplicationAccess { get; set; }
}

View File

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

View File

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

View File

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

View File

@ -27,7 +27,7 @@ namespace Marco.Pms.Model.Dtos.Project
[DisplayName("Project Status")]
[Required(ErrorMessage = "Project Status is required!")]
public required Guid ProjectStatusId { get; set; }
public Guid? PromoterId { get; set; }
public Guid? PMCId { get; set; }
public required Guid PromoterId { get; set; }
public required Guid PMCId { get; set; }
}
}

View File

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

View File

@ -27,7 +27,7 @@ namespace Marco.Pms.Model.Dtos.Project
[DisplayName("Project Status")]
[Required(ErrorMessage = "Project Status is required!")]
public required Guid ProjectStatusId { get; set; }
public Guid? PromoterId { get; set; }
public Guid? PMCId { get; set; }
public required Guid PromoterId { get; set; }
public required Guid PMCId { get; set; }
}
}

View File

@ -47,12 +47,6 @@
public static readonly Guid DownloadDocument = Guid.Parse("404373d0-860f-490e-a575-1c086ffbce1d");
public static readonly Guid VerifyDocument = Guid.Parse("13a1f30f-38d1-41bf-8e7a-b75189aab8e0");
public static readonly Guid CollectionAdmin = Guid.Parse("dbf17591-09fe-4c93-9e1a-12db8f5cc5de");
public static readonly Guid ViewCollection = Guid.Parse("c8d7eea5-4033-4aad-9ebe-76de49896830");
public static readonly Guid CreateCollection = Guid.Parse("b93141fd-dbd3-4051-8f57-bf25d18e3555");
public static readonly Guid EditCollection = Guid.Parse("455187b4-fef1-41f9-b3d0-025d0b6302c3");
public static readonly Guid AddPayment = Guid.Parse("061d9ccd-85b4-4cb0-be06-2f9f32cebb72");
public static readonly Guid AddOrganization = Guid.Parse("068cb3c1-49c5-4746-9f29-1fce16e820ac");
public static readonly Guid EditOrganization = Guid.Parse("c1ae1363-ab8a-4bd9-a9d1-8c2c6083873a");
public static readonly Guid ViewOrganization = Guid.Parse("7a6cf830-0008-4e03-b31d-0d050cb634f4");

View File

@ -6,7 +6,6 @@
public List<Guid>? StatusIds { get; set; }
public List<Guid>? CreatedByIds { get; set; }
public List<Guid>? PaidById { get; set; }
public List<Guid>? ExpenseTypeIds { get; set; }
public bool IsTransactionDate { get; set; } = false;
public DateTime? StartDate { get; set; }
public DateTime? EndDate { get; set; }

View File

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

View File

@ -1,16 +0,0 @@
using Marco.Pms.Model.ViewModels.Activities;
namespace Marco.Pms.Model.ViewModels.Collection
{
public class InvoiceAttachmentVM
{
public Guid Id { get; set; }
public Guid InvoiceId { get; set; }
public Guid DocumentId { get; set; }
public string? FileName { get; set; }
public string? ContentType { get; set; }
public string? PreSignedUrl { get; set; }
public BasicEmployeeVM? UploadedBy { get; set; }
}
}

View File

@ -1,13 +0,0 @@
using Marco.Pms.Model.ViewModels.Activities;
namespace Marco.Pms.Model.ViewModels.Collection
{
public class InvoiceCommentVM
{
public Guid Id { get; set; }
public string Comment { get; set; } = default!;
public DateTime CreatedAt { get; set; }
public BasicEmployeeVM? CreatedBy { get; set; }
public Guid InvoiceId { get; set; }
}
}

View File

@ -1,31 +0,0 @@
using Marco.Pms.Model.ViewModels.Activities;
using Marco.Pms.Model.ViewModels.Projects;
namespace Marco.Pms.Model.ViewModels.Collection
{
public class InvoiceDetailsVM
{
public Guid Id { get; set; }
public string Title { get; set; } = default!;
public string Description { get; set; } = default!;
public string InvoiceNumber { get; set; } = default!;
public string? EInvoiceNumber { get; set; }
public BasicProjectVM? Project { get; set; }
public DateTime InvoiceDate { get; set; }
public DateTime ClientSubmitedDate { get; set; }
public DateTime ExceptedPaymentDate { get; set; }
public double BasicAmount { get; set; }
public double TaxAmount { get; set; }
public double BalanceAmount { get; set; }
public bool IsActive { get; set; } = true;
public bool MarkAsCompleted { get; set; } = true;
public DateTime CreatedAt { get; set; }
public BasicEmployeeVM? CreatedBy { get; set; }
public DateTime? UpdatedAt { get; set; }
public BasicEmployeeVM? UpdatedBy { get; set; }
public List<InvoiceAttachmentVM>? Attachments { get; set; }
public List<ReceivedInvoicePaymentVM>? ReceivedInvoicePayments { get; set; }
public List<InvoiceCommentVM>? Comments { get; set; }
}
}

View File

@ -1,27 +0,0 @@
using Marco.Pms.Model.ViewModels.Activities;
using Marco.Pms.Model.ViewModels.Projects;
namespace Marco.Pms.Model.ViewModels.Collection
{
public class InvoiceListVM
{
public Guid Id { get; set; }
public string Title { get; set; } = default!;
public string Description { get; set; } = default!;
public string InvoiceNumber { get; set; } = default!;
public string? EInvoiceNumber { get; set; }
public BasicProjectVM? Project { get; set; }
public DateTime InvoiceDate { get; set; }
public DateTime ClientSubmitedDate { get; set; }
public DateTime ExceptedPaymentDate { get; set; }
public double BasicAmount { get; set; }
public double TaxAmount { get; set; }
public double BalanceAmount { get; set; }
public bool IsActive { get; set; }
public bool MarkAsCompleted { get; set; }
public DateTime CreatedAt { get; set; }
public BasicEmployeeVM? CreatedBy { get; set; }
public DateTime? UpdatedAt { get; set; }
public BasicEmployeeVM? UpdatedBy { get; set; }
}
}

View File

@ -1,10 +0,0 @@
namespace Marco.Pms.Model.ViewModels.Collection
{
public class PaymentAdjustmentHeadVM
{
public Guid Id { get; set; }
public string? Name { get; set; }
public string? Description { get; set; }
public bool IsActive { get; set; } = true;
}
}

View File

@ -1,18 +0,0 @@
using Marco.Pms.Model.ViewModels.Activities;
namespace Marco.Pms.Model.ViewModels.Collection
{
public class ReceivedInvoicePaymentVM
{
public Guid Id { get; set; }
public Guid InvoiceId { get; set; }
public DateTime PaymentReceivedDate { get; set; }
public PaymentAdjustmentHeadVM? PaymentAdjustmentHead { get; set; }
public string TransactionId { get; set; } = default!;
public double Amount { get; set; }
public string? Comment { get; set; }
public bool IsActive { get; set; } = true;
public DateTime CreatedAt { get; set; }
public BasicEmployeeVM? CreatedBy { get; set; }
}
}

View File

@ -19,6 +19,7 @@ namespace Marco.Pms.Model.ViewModels.Expenses
public DateTime TransactionDate { get; set; }
public DateTime CreatedAt { get; set; }
public string SupplerName { get; set; } = string.Empty;
public string? ExpenseUId { get; set; }
public double Amount { get; set; }
public ExpensesStatusMasterVM? Status { get; set; }
public List<ExpensesStatusMasterVM>? NextStatus { get; set; }
@ -26,7 +27,6 @@ namespace Marco.Pms.Model.ViewModels.Expenses
public string? TransactionId { get; set; }
public string Description { get; set; } = string.Empty;
public string? Location { get; set; }
public string? ExpenseUId { get; set; }
public List<BasicDocumentVM> Documents { get; set; } = new List<BasicDocumentVM>();
public List<ExpenseLogVM> ExpenseLogs { get; set; } = new List<ExpenseLogVM>();
public string? GSTNumber { get; set; }

View File

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

View File

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

View File

@ -470,19 +470,6 @@ namespace Marco.Pms.Services.Controllers
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 ---
if (!item.PermissionIds.Any())
{
@ -590,6 +577,7 @@ namespace Marco.Pms.Services.Controllers
{
new MasterMenuVM { Id = 3, Name = "Work Category" },
new MasterMenuVM { Id = 8, Name = "Services" }
//new MasterMenuVM { Id = 10, Name = "Payment Mode" }
}
},
{
@ -603,8 +591,7 @@ namespace Marco.Pms.Services.Controllers
ExpenseManagement, new List<MasterMenuVM>
{
new MasterMenuVM { Id = 6, Name = "Expense Type" },
new MasterMenuVM { Id = 7, Name = "Payment Mode" },
new MasterMenuVM { Id = 10, Name = "Payment Adjustment Head" }
new MasterMenuVM { Id = 7, Name = "Payment Mode" }
}
}
};
@ -621,20 +608,6 @@ namespace Marco.Pms.Services.Controllers
var featureIds = await generalHelper.GetFeatureIdsByTenentIdAsync(tenantId);
_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
var response = featureIds
.Where(id => featureMenus.ContainsKey(id))

View File

@ -5,6 +5,7 @@ using Marco.Pms.Model.Dtos.Attendance;
using Marco.Pms.Model.Employees;
using Marco.Pms.Model.Entitlements;
using Marco.Pms.Model.Mapper;
using Marco.Pms.Model.Projects;
using Marco.Pms.Model.Utilities;
using Marco.Pms.Model.ViewModels.Activities;
using Marco.Pms.Model.ViewModels.AttendanceVM;
@ -157,12 +158,29 @@ namespace MarcoBMS.Services.Controllers
/// <returns></returns>
[HttpGet("project/log")]
public async Task<IActionResult> EmployeeAttendanceByDateRange([FromQuery] Guid? projectId, [FromQuery] Guid? organizationId, [FromQuery] string? dateFrom = null, [FromQuery] string? dateTo = null)
public async Task<IActionResult> EmployeeAttendanceByDateRange([FromQuery] Guid projectId, [FromQuery] Guid? organizationId, [FromQuery] string? dateFrom = null, [FromQuery] string? dateTo = null)
{
var LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
using var scope = _serviceScopeFactory.CreateScope();
var _projectServices = scope.ServiceProvider.GetRequiredService<IProjectServices>();
var hasTeamAttendancePermission = await _permission.HasPermission(PermissionsMaster.TeamAttendance, LoggedInEmployee.Id);
var hasSelfAttendancePermission = await _permission.HasPermission(PermissionsMaster.SelfAttendance, LoggedInEmployee.Id);
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 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 toDate = new DateTime();
@ -178,46 +196,32 @@ namespace MarcoBMS.Services.Controllers
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>();
//Attendance? attendance = null;
ProjectAllocation? teamMember = null;
if (dateFrom == null) fromDate = DateTime.UtcNow.Date;
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)
{
List<Attendance> lstAttendance = await lstAttendanceQuery.ToListAsync();
List<Attendance> lstAttendance = await _context.Attendes
.Include(a => a.RequestedBy)
.ThenInclude(e => e!.JobRole)
.Include(a => a.Approver)
.ThenInclude(e => e!.JobRole)
.Where(c => c.ProjectID == projectId && c.AttendanceDate.Date >= fromDate.Date && c.AttendanceDate.Date <= toDate.Date && c.TenantId == tenantId)
.ToListAsync();
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)
{
var result1 = new EmployeeAttendanceVM()
@ -226,58 +230,98 @@ namespace MarcoBMS.Services.Controllers
CheckInTime = attendance.InTime,
CheckOutTime = attendance.OutTime,
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)
Approver = _mapper.Map<BasicEmployeeVM>(attendance.Approver),
RequestedAt = attendance.RequestedAt,
RequestedBy = _mapper.Map<BasicEmployeeVM>(attendance.RequestedBy)
};
result.Add(result1);
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;
result1.ProjectId = projectId;
result1.ProjectName = teamMember.Project?.Name;
}
else
{
result1.FirstName = null;
result1.LastName = null;
result1.JobRoleName = null;
result1.OrganizationName = null;
}
result.Add(result1);
}
}
}
else if (hasSelfAttendancePermission)
{
List<Attendance> lstAttendances = await _context.Attendes
.Include(a => a.RequestedBy)
.ThenInclude(e => e!.JobRole)
.Include(a => a.Approver)
.ThenInclude(e => e!.JobRole)
.Where(c => c.ProjectID == projectId && c.EmployeeId == loggedInEmployee.Id && c.AttendanceDate.Date >= fromDate.Date &&
c.AttendanceDate.Date <= toDate.Date && c.TenantId == tenantId)
.ToListAsync();
var lstAttendances = await lstAttendanceQuery.Where(a => a.EmployeeId == LoggedInEmployee.Id).ToListAsync();
var projectAllocationQuery = _context.ProjectAllocations
.Include(pa => pa.Project)
.Include(pa => pa.Employee)
.ThenInclude(e => e!.Organization)
.Include(pa => pa.Employee)
.ThenInclude(e => e!.JobRole)
.Where(pa => pa.EmployeeId == loggedInEmployee.Id && pa.TenantId == tenantId && pa.IsActive &&
pa.ProjectId == projectId && pa.Project != null &&
pa.Employee != null && pa.Employee.Organization != null && pa.Employee.JobRole != null);
var projectIds = lstAttendances.Select(a => a.ProjectID).ToList();
var projects = await _context.Projects.Where(p => projectIds.Contains(p.Id) && p.TenantId == tenantId).ToListAsync();
if (organizationId.HasValue)
{
projectAllocationQuery = projectAllocationQuery.Where(pa => pa.Employee != null && pa.Employee.OrganizationId == organizationId);
}
var projectAllocation = await projectAllocationQuery.FirstOrDefaultAsync();
foreach (var attendance in lstAttendances)
{
EmployeeAttendanceVM result1 = new EmployeeAttendanceVM
if (projectAllocation != null)
{
Id = attendance.Id,
EmployeeAvatar = null,
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,
CheckInTime = attendance.InTime,
CheckOutTime = attendance.OutTime,
Activity = attendance.Activity,
RequestedAt = attendance.RequestedAt,
RequestedBy = _mapper.Map<BasicEmployeeVM>(attendance.RequestedBy),
ApprovedAt = attendance.ApprovedAt,
Approver = _mapper.Map<BasicEmployeeVM>(attendance.Approver)
};
result.Add(result1);
EmployeeAttendanceVM result1 = new EmployeeAttendanceVM
{
Id = attendance.Id,
EmployeeAvatar = null,
EmployeeId = projectAllocation.EmployeeId,
FirstName = projectAllocation.Employee?.FirstName,
LastName = projectAllocation.Employee?.LastName,
JobRoleName = projectAllocation.Employee?.JobRole?.Name,
OrganizationName = projectAllocation.Employee?.Organization?.Name,
ProjectId = attendance.ProjectID,
ProjectName = projectAllocation.Project?.Name,
CheckInTime = attendance.InTime,
CheckOutTime = attendance.OutTime,
Activity = attendance.Activity,
ApprovedAt = attendance.ApprovedAt,
Approver = _mapper.Map<BasicEmployeeVM>(attendance.Approver),
RequestedAt = attendance.RequestedAt,
RequestedBy = _mapper.Map<BasicEmployeeVM>(attendance.RequestedBy)
};
result.Add(result1);
}
}
}
_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));
}
[HttpGet("project/team")]
/// <summary>
/// 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).
@ -287,14 +331,12 @@ namespace MarcoBMS.Services.Controllers
/// <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>
/// <returns>An IActionResult containing a list of employee attendance records or an error response.</returns>
[HttpGet("project/team")]
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 loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
// --- 1. Initial Validation and Permission Checks ---
_logger.LogInfo("Fetching attendance for ProjectId: {ProjectId}, TenantId: {TenantId}", projectId ?? Guid.Empty, tenantId);
_logger.LogInfo("Fetching attendance for ProjectId: {ProjectId}, TenantId: {TenantId}", projectId, tenantId);
// Validate date format
if (!DateTime.TryParse(date, out var forDate))
@ -302,6 +344,20 @@ namespace MarcoBMS.Services.Controllers
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 ---
try
{
@ -310,17 +366,13 @@ namespace MarcoBMS.Services.Controllers
if (hasTeamAttendancePermission)
{
if (!organizationId.HasValue)
{
organizationId = loggedInEmployee.OrganizationId;
}
_logger.LogInfo("EmployeeId: {EmployeeId} has Team Attendance permission. Fetching team attendance.", loggedInEmployee.Id);
result = await GetTeamAttendanceAsync(tenantId, projectId, organizationId.Value, forDate, includeInactive);
result = await GetTeamAttendanceAsync(tenantId, projectId, organizationId, forDate, includeInactive);
}
else if (await _permission.HasPermission(PermissionsMaster.SelfAttendance, loggedInEmployee.Id))
{
_logger.LogInfo("EmployeeId: {EmployeeId} has Self Attendance permission. Fetching self attendance.", loggedInEmployee.Id);
result = await GetSelfAttendanceAsync(tenantId, projectId, loggedInEmployee.Id, forDate);
result = await GetSelfAttendanceAsync(tenantId, projectId, loggedInEmployee.Id, organizationId, forDate);
}
else
{
@ -328,67 +380,70 @@ namespace MarcoBMS.Services.Controllers
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 ?? Guid.Empty);
_logger.LogInfo("Successfully fetched {Count} attendance records for ProjectId: {ProjectId}", result.Count, projectId);
return Ok(ApiResponse<object>.SuccessResponse(result, $"{result.Count} attendance records fetched successfully."));
}
catch (Exception ex)
{
_logger.LogError(ex, "An error occurred while fetching attendance for ProjectId: {ProjectId}", projectId ?? Guid.Empty);
_logger.LogError(ex, "An error occurred while fetching attendance for ProjectId: {ProjectId}", projectId);
return StatusCode(500, ApiResponse<object>.ErrorResponse("An internal server error occurred."));
}
}
[HttpGet("regularize")]
public async Task<IActionResult> GetRequestRegularizeAttendance([FromQuery] Guid? projectId, [FromQuery] Guid? organizationId, [FromQuery] bool IncludeInActive)
public async Task<IActionResult> GetRequestRegularizeAttendance([FromQuery] Guid projectId, [FromQuery] Guid? organizationId, [FromQuery] bool IncludeInActive)
{
using var scope = _serviceScopeFactory.CreateScope();
var _projectServices = scope.ServiceProvider.GetRequiredService<IProjectServices>();
Employee LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
var result = new List<EmployeeAttendanceVM>();
var hasProjectPermission = await _permission.HasProjectPermission(LoggedInEmployee, projectId);
var lstAttendanceQuery = _context.Attendes
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));
}
List<Attendance> lstAttendance = await _context.Attendes
.Include(a => a.RequestedBy)
.ThenInclude(e => e!.JobRole)
.Include(a => a.Employee)
.ThenInclude(e => e!.Organization)
.Include(a => a.Employee)
.Include(a => a.Approver)
.ThenInclude(e => e!.JobRole)
.Where(c => c.ProjectID == projectId && c.Activity == ATTENDANCE_MARK_TYPE.REQUEST_REGULARIZE && c.TenantId == tenantId)
.ToListAsync();
.Where(c => c.Activity == ATTENDANCE_MARK_TYPE.REQUEST_REGULARIZE && c.Employee != null && c.Employee.JobRole != null && c.TenantId == tenantId);
if (organizationId.HasValue)
{
lstAttendanceQuery = lstAttendanceQuery.Where(a => a.Employee != null && a.Employee.OrganizationId == organizationId);
}
if (projectId.HasValue)
{
lstAttendanceQuery = lstAttendanceQuery.Where(a => a.ProjectID == projectId);
}
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 idList = projectteam.Select(p => p.EmployeeId).ToList();
foreach (Attendance attende in lstAttendance)
{
var result1 = new EmployeeAttendanceVM()
var teamMember = projectteam.Find(m => m.EmployeeId == attende.EmployeeId);
if (teamMember != null && teamMember.Employee != null && teamMember.Employee.JobRole != null)
{
Id = attende.Id,
CheckInTime = attende.InTime,
CheckOutTime = attende.OutTime,
Activity = attende.Activity,
EmployeeAvatar = null,
EmployeeId = attende.EmployeeId,
ProjectId = attende.ProjectID,
FirstName = attende.Employee?.FirstName,
ProjectName = projects.Where(p => p.Id == attende.ProjectID).Select(p => p.Name).FirstOrDefault(),
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);
var result1 = new EmployeeAttendanceVM()
{
Id = attende.Id,
CheckInTime = attende.InTime,
CheckOutTime = attende.OutTime,
Activity = attende.Activity,
EmployeeAvatar = null,
EmployeeId = attende.EmployeeId,
FirstName = teamMember.Employee.FirstName,
LastName = teamMember.Employee.LastName,
JobRoleName = teamMember.Employee.JobRole.Name,
OrganizationName = teamMember.Employee.Organization?.Name,
ProjectId = projectId,
ProjectName = teamMember.Project?.Name,
ApprovedAt = attende.ApprovedAt,
Approver = _mapper.Map<BasicEmployeeVM>(attende.Approver),
RequestedAt = attende.RequestedAt,
RequestedBy = _mapper.Map<BasicEmployeeVM>(attende.RequestedBy)
};
result.Add(result1);
}
}
@ -401,7 +456,8 @@ namespace MarcoBMS.Services.Controllers
return Ok(ApiResponse<object>.SuccessResponse(result, System.String.Format("{0} Attendance records fetched successfully", result.Count), 200));
}
[HttpPost("record")]
[HttpPost]
[Route("record")]
public async Task<IActionResult> RecordAttendance([FromBody] RecordAttendanceDot recordAttendanceDot)
{
if (!ModelState.IsValid)
@ -417,10 +473,11 @@ namespace MarcoBMS.Services.Controllers
using var scope = _serviceScopeFactory.CreateScope();
var _signalR = scope.ServiceProvider.GetRequiredService<IHubContext<MarcoHub>>();
var _employeeHelper = scope.ServiceProvider.GetRequiredService<EmployeeHelper>();
var _firebase = scope.ServiceProvider.GetRequiredService<IFirebaseService>();
var currentEmployee = await _userHelper.GetCurrentEmployeeAsync();
using var transaction = await _context.Database.BeginTransactionAsync();
try
{
Attendance? attendance = await _context.Attendes.FirstOrDefaultAsync(a => a.Id == recordAttendanceDot.Id && a.TenantId == tenantId); ;
@ -451,22 +508,18 @@ namespace MarcoBMS.Services.Controllers
{
attendance.IsApproved = true;
attendance.Activity = ATTENDANCE_MARK_TYPE.REGULARIZE;
//string timeString = "10:30 PM"; // Format: "hh:mm tt"
attendance.OutTime = finalDateTime;
}
else if (recordAttendanceDot.Action == ATTENDANCE_MARK_TYPE.REQUEST_REGULARIZE)
{
DateTime date = attendance.AttendanceDate;
finalDateTime = GetDateFromTimeStamp(date.Date, recordAttendanceDot.MarkTime);
if (attendance.InTime <= finalDateTime)
if (attendance.InTime < finalDateTime)
{
attendance.OutTime = finalDateTime;
attendance.Activity = ATTENDANCE_MARK_TYPE.REQUEST_REGULARIZE;
attendance.RequestedAt = DateTime.UtcNow;
attendance.RequestedById = currentEmployee.Id;
attendance.RequestedAt = DateTime.UtcNow;
}
else
{
@ -555,13 +608,11 @@ namespace MarcoBMS.Services.Controllers
CheckOutTime = attendance.OutTime,
EmployeeAvatar = null,
EmployeeId = recordAttendanceDot.EmployeeID,
ProjectId = attendance.ProjectID,
FirstName = employee.FirstName,
LastName = employee.LastName,
Id = attendance.Id,
Activity = attendance.Activity,
JobRoleName = employee.JobRole.Name,
OrganizationName = employee.Organization?.Name
JobRoleName = employee.JobRole.Name
};
var sendActivity = 0;
if (recordAttendanceDot.Id == Guid.Empty)
@ -577,7 +628,7 @@ namespace MarcoBMS.Services.Controllers
// --- Push Notification Section ---
// This section attempts to send a test push notification to the user's device.
// It's designed to fail gracefully and handle invalid Firebase Cloud Messaging (FCM) tokens.
var _firebase = scope.ServiceProvider.GetRequiredService<IFirebaseService>();
var name = $"{vm.FirstName} {vm.LastName}";
await _firebase.SendAttendanceMessageAsync(attendance.ProjectID, name, recordAttendanceDot.Action, attendance.EmployeeId, tenantId);
@ -618,9 +669,9 @@ namespace MarcoBMS.Services.Controllers
using var scope = _serviceScopeFactory.CreateScope();
var _s3Service = scope.ServiceProvider.GetRequiredService<S3UploadService>();
var _signalR = scope.ServiceProvider.GetRequiredService<IHubContext<MarcoHub>>();
var _employeeHelper = scope.ServiceProvider.GetRequiredService<EmployeeHelper>();
var _firebase = scope.ServiceProvider.GetRequiredService<IFirebaseService>();
var _signalR = scope.ServiceProvider.GetRequiredService<IHubContext<MarcoHub>>();
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
var batchId = Guid.NewGuid();
@ -685,8 +736,8 @@ namespace MarcoBMS.Services.Controllers
{
attendance.OutTime = finalDateTime;
attendance.Activity = ATTENDANCE_MARK_TYPE.REQUEST_REGULARIZE;
attendance.RequestedAt = DateTime.UtcNow;
attendance.RequestedById = loggedInEmployee.Id;
attendance.RequestedAt = DateTime.UtcNow;
}
else
{
@ -697,14 +748,14 @@ namespace MarcoBMS.Services.Controllers
case ATTENDANCE_MARK_TYPE.REGULARIZE:
attendance.IsApproved = true;
attendance.Activity = ATTENDANCE_MARK_TYPE.REGULARIZE;
attendance.ApprovedAt = DateTime.UtcNow;
attendance.ApprovedById = loggedInEmployee.Id;
attendance.ApprovedAt = DateTime.UtcNow;
break;
case ATTENDANCE_MARK_TYPE.REGULARIZE_REJECT:
attendance.IsApproved = false;
attendance.Activity = ATTENDANCE_MARK_TYPE.REGULARIZE_REJECT;
attendance.ApprovedAt = DateTime.UtcNow;
attendance.ApprovedById = loggedInEmployee.Id;
attendance.ApprovedAt = DateTime.UtcNow;
break;
}
@ -770,7 +821,6 @@ namespace MarcoBMS.Services.Controllers
{
Id = attendance.Id,
EmployeeId = employee.Id,
ProjectId = attendance.ProjectID,
FirstName = employee.FirstName,
LastName = employee.LastName,
CheckInTime = attendance.InTime,
@ -833,54 +883,65 @@ namespace MarcoBMS.Services.Controllers
/// <summary>
/// Fetches attendance for an entire project team using a single, optimized database query.
/// </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 is far more efficient than fetching collections and joining them in memory.
var query = _context.Employees
.Include(e => e!.Organization)
.Include(e => e!.JobRole)
.Where(e => e.OrganizationId == organizationId && e.Organization != null && e.JobRole != null && e.IsActive);
var query = _context.ProjectAllocations
.Include(pa => pa.Project)
.Include(pa => pa.Employee)
.ThenInclude(e => e!.Organization)
.Include(pa => pa.Employee)
.ThenInclude(e => e!.JobRole)
.Where(pa => pa.TenantId == tenantId && pa.ProjectId == projectId);
var lstAttendanceQuery = _context.Attendes.Where(c => c.AttendanceDate.Date == forDate && c.TenantId == tenantId);
if (projectId.HasValue)
// Apply filters based on optional parameters
if (!includeInactive)
{
lstAttendanceQuery = lstAttendanceQuery.Where(a => a.ProjectID == projectId);
query = query.Where(pa => pa.IsActive);
}
if (organizationId.HasValue)
{
query = query.Where(pa => pa.Employee != null && pa.Employee.OrganizationId == organizationId);
}
List<Attendance> lstAttendance = await lstAttendanceQuery.ToListAsync();
List<Attendance> lstAttendance = await _context.Attendes
.Include(a => a.RequestedBy)
.ThenInclude(e => e!.JobRole)
.Include(a => a.Approver)
.ThenInclude(e => e!.JobRole)
.Where(c => c.ProjectID == projectId && c.AttendanceDate.Date == forDate && c.TenantId == tenantId).ToListAsync();
var employees = await query
var teamAttendance = await query
.AsNoTracking()
.ToListAsync();
var projectIds = lstAttendance.Select(a => a.ProjectID).ToList();
var projects = await _context.Projects.Where(p => projectIds.Contains(p.Id) && p.TenantId == tenantId).ToListAsync();
var response = employees
.Select(employee =>
var response = teamAttendance
.Select(teamMember =>
{
var result1 = new EmployeeAttendanceVM()
{
EmployeeAvatar = null,
EmployeeId = employee.Id,
FirstName = employee.FirstName,
LastName = employee.LastName,
OrganizationName = employee.Organization!.Name,
JobRoleName = employee.JobRole!.Name,
EmployeeId = teamMember.EmployeeId,
FirstName = teamMember.Employee?.FirstName,
LastName = teamMember.Employee?.LastName,
OrganizationName = teamMember.Employee?.Organization?.Name,
JobRoleName = teamMember.Employee?.JobRole?.Name,
ProjectId = projectId,
ProjectName = teamMember.Project?.Name
};
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)
{
result1.Id = attendance.Id;
result1.ProjectId = attendance.ProjectID;
result1.CheckInTime = attendance.InTime;
result1.CheckOutTime = attendance.OutTime;
result1.Activity = attendance.Activity;
result1.ProjectName = projects.Where(p => p.Id == attendance.ProjectID).Select(p => p.Name).FirstOrDefault();
result1.ApprovedAt = attendance.ApprovedAt;
result1.Approver = _mapper.Map<BasicEmployeeVM>(attendance.Approver);
result1.RequestedAt = attendance.RequestedAt;
result1.RequestedBy = _mapper.Map<BasicEmployeeVM>(attendance.RequestedBy);
}
return result1;
})
@ -893,50 +954,57 @@ namespace MarcoBMS.Services.Controllers
/// <summary>
/// Fetches a single attendance record for the logged-in employee.
/// </summary>
private async Task<List<EmployeeAttendanceVM>> GetSelfAttendanceAsync(Guid tenantId, Guid? projectId, Guid employeeId, DateTime forDate)
private async Task<List<EmployeeAttendanceVM>> GetSelfAttendanceAsync(Guid tenantId, Guid projectId, Guid employeeId, Guid? organizationId, DateTime forDate)
{
List<EmployeeAttendanceVM> result = new List<EmployeeAttendanceVM>();
// This query fetches the employee's project allocation and their attendance in a single trip.
var lstAttendanceQuery = _context.Attendes
.Where(c => c.EmployeeId == employeeId && c.AttendanceDate.Date == forDate && c.TenantId == tenantId);
Attendance lstAttendance = await _context.Attendes
.Include(a => a.RequestedBy)
.ThenInclude(e => e!.JobRole)
.Include(a => a.Approver)
.ThenInclude(e => e!.JobRole)
.FirstOrDefaultAsync(c => c.ProjectID == projectId && c.EmployeeId == employeeId && c.AttendanceDate.Date == forDate && c.TenantId == tenantId) ?? new Attendance();
if (projectId.HasValue)
var projectAllocationQuery = _context.ProjectAllocations
.Include(pa => pa.Project)
.Include(pa => pa.Employee)
.ThenInclude(e => e!.Organization)
.Include(pa => pa.Employee)
.ThenInclude(e => e!.JobRole)
.Where(pa => pa.EmployeeId == employeeId && pa.TenantId == tenantId && pa.IsActive &&
pa.ProjectId == projectId && pa.Project != null &&
pa.Employee != null && pa.Employee.Organization != null && pa.Employee.JobRole != null);
if (organizationId.HasValue)
{
lstAttendanceQuery = lstAttendanceQuery.Where(a => a.ProjectID == projectId);
projectAllocationQuery = projectAllocationQuery.Where(pa => pa.Employee != null && pa.Employee.OrganizationId == organizationId);
}
List<Attendance> lstAttendances = await lstAttendanceQuery.ToListAsync() ?? new List<Attendance>();
var projectAllocation = await projectAllocationQuery.FirstOrDefaultAsync();
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)
if (projectAllocation != null)
{
foreach (var lstAttendance in lstAttendances)
EmployeeAttendanceVM result1 = new EmployeeAttendanceVM
{
EmployeeAttendanceVM result1 = new EmployeeAttendanceVM
{
Id = lstAttendance.Id,
EmployeeAvatar = null,
ProjectId = lstAttendance.ProjectID,
EmployeeId = employee.Id,
FirstName = employee.FirstName,
OrganizationName = employee.Organization.Name,
ProjectName = projects.Where(p => p.Id == lstAttendance.ProjectID).Select(p => p.Name).FirstOrDefault(),
LastName = employee.LastName,
JobRoleName = employee.JobRole.Name,
CheckInTime = lstAttendance.InTime,
CheckOutTime = lstAttendance.OutTime,
Activity = lstAttendance.Activity
};
result.Add(result1);
}
Id = lstAttendance.Id,
EmployeeAvatar = null,
EmployeeId = projectAllocation.EmployeeId,
FirstName = projectAllocation.Employee?.FirstName,
OrganizationName = projectAllocation.Employee?.Organization?.Name,
LastName = projectAllocation.Employee?.LastName,
JobRoleName = projectAllocation.Employee?.JobRole?.Name,
ProjectId = projectId,
ProjectName = projectAllocation.Project?.Name,
CheckInTime = lstAttendance.InTime,
CheckOutTime = lstAttendance.OutTime,
Activity = lstAttendance.Activity,
ApprovedAt = lstAttendance.ApprovedAt,
Approver = _mapper.Map<BasicEmployeeVM>(lstAttendance.Approver),
RequestedAt = lstAttendance.RequestedAt,
RequestedBy = _mapper.Map<BasicEmployeeVM>(lstAttendance.RequestedBy)
};
result.Add(result1);
}
return result;

View File

@ -7,6 +7,7 @@ using Marco.Pms.Model.Employees;
using Marco.Pms.Model.Entitlements;
using Marco.Pms.Model.Utilities;
using Marco.Pms.Model.ViewModels.Tenant;
using Marco.Pms.Services.Helpers;
using MarcoBMS.Services.Helpers;
using MarcoBMS.Services.Service;
using Microsoft.AspNetCore.Authorization;
@ -59,7 +60,7 @@ namespace MarcoBMS.Services.Controllers
{
var user = await _context.ApplicationUsers
.FirstOrDefaultAsync(u => u.Email == loginDto.Username || u.UserName == loginDto.Username);
.FirstOrDefaultAsync(u => u.Email == loginDto.Username || u.PhoneNumber == loginDto.Username);
if (user == null)
{
@ -103,13 +104,9 @@ namespace MarcoBMS.Services.Controllers
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
var token = _refreshTokenService.GenerateJwtToken(user.UserName, tenant?.Id ?? Guid.Empty, emp.OrganizationId, _jwtSettings);
var refreshToken = await _refreshTokenService.CreateRefreshToken(user.Id, tenant?.Id.ToString(), emp.OrganizationId, _jwtSettings);
var token = _refreshTokenService.GenerateJwtToken(user.UserName, emp.TenantId ?? Guid.Empty, emp.OrganizationId, _jwtSettings);
var refreshToken = await _refreshTokenService.CreateRefreshToken(user.Id, emp.TenantId.ToString(), emp.OrganizationId, _jwtSettings);
_logger.LogInfo("User login successful - UserId: {UserId}", user.Id);
return Ok(ApiResponse<object>.SuccessResponse(new
@ -205,17 +202,12 @@ namespace MarcoBMS.Services.Controllers
}
_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
var token = _refreshTokenService.GenerateJwtToken(user.UserName, tenant?.Id ?? Guid.Empty, emp.OrganizationId, _jwtSettings);
var token = _refreshTokenService.GenerateJwtToken(user.UserName, emp.TenantId ?? Guid.Empty, emp.OrganizationId, _jwtSettings);
// Generate a new refresh token and store it in the database.
_logger.LogInfo("Generating and storing Refresh Token for user: {Username}", user.UserName);
var refreshToken = await _refreshTokenService.CreateRefreshToken(user.Id, tenant?.Id.ToString(), emp.OrganizationId, _jwtSettings);
var refreshToken = await _refreshTokenService.CreateRefreshToken(user.Id, emp.TenantId.ToString(), emp.OrganizationId, _jwtSettings);
// Fetch MPIN Token
var mpinToken = await _context.MPINDetails.FirstOrDefaultAsync(p => p.UserId == Guid.Parse(user.Id));
@ -273,32 +265,31 @@ namespace MarcoBMS.Services.Controllers
}
string? tokenType = claimsPrincipal.FindFirst("token_type")?.Value;
string? tokenTenantId = claimsPrincipal.FindFirst("TenantId")?.Value;
string? tokenUserId = claimsPrincipal.FindFirst(ClaimTypes.NameIdentifier)?.Value;
// Validate essential claims
if (string.IsNullOrWhiteSpace(tokenType) || string.IsNullOrWhiteSpace(tokenUserId))
if (string.IsNullOrWhiteSpace(tokenType) || string.IsNullOrWhiteSpace(tokenTenantId) || string.IsNullOrWhiteSpace(tokenUserId))
{
_logger.LogWarning("MPIN token claims are incomplete");
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
var requestEmployee = await _context.Employees
.Include(e => e.ApplicationUser)
.FirstOrDefaultAsync(e => e.Id == verifyMPIN.EmployeeId && e.HasApplicationAccess && e.ApplicationUserId == tokenUserId && e.IsActive);
.FirstOrDefaultAsync(e => e.Id == verifyMPIN.EmployeeId && e.TenantId == tenantId && e.ApplicationUserId == tokenUserId && e.IsActive);
if (requestEmployee == null || string.IsNullOrWhiteSpace(requestEmployee.ApplicationUserId))
{
_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));
}
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
if (requestEmployee.ApplicationUserId != tokenUserId || tokenType != "mpin" || tenantId == Guid.Empty)
if (requestEmployee.ApplicationUserId != tokenUserId || tokenType != "mpin")
{
_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));
@ -411,9 +402,7 @@ namespace MarcoBMS.Services.Controllers
//var accessToken = _refreshTokenService.GenerateJwtTokenWithOrganization(requestEmployee.ApplicationUser?.UserName, requestEmployee.OrganizationId, _jwtSettings);
//var refreshToken = await _refreshTokenService.CreateRefreshTokenWithOrganization(requestEmployee.ApplicationUserId, requestEmployee.OrganizationId, _jwtSettings);
var tenants = await _context.Tenants.Where(t => t.OrganizationId == requestEmployee.OrganizationId).ToListAsync();
var tenant = tenants.OrderBy(t => t.OnBoardingDate).FirstOrDefault();
var tenant = await _context.Tenants.FirstOrDefaultAsync(t => t.OrganizationId == requestEmployee.OrganizationId);
var accessToken = _refreshTokenService.GenerateJwtToken(requestEmployee.ApplicationUser?.UserName,
tenant?.Id ?? Guid.Empty, requestEmployee.OrganizationId, _jwtSettings);
@ -1481,6 +1470,9 @@ namespace MarcoBMS.Services.Controllers
// Generate and store refresh token
var refreshToken = await _refreshTokenService.CreateRefreshToken(loggedInEmployee.ApplicationUserId, tenantId.ToString(), loggedInEmployee.OrganizationId, _jwtSettings);
var _cache = scope.ServiceProvider.GetRequiredService<CacheUpdateHelper>();
await _cache.ClearAllEmployeesFromCacheByOnlyEmployeeId(loggedInEmployee.Id);
_logger.LogInfo("Tenant selected and tokens generated for TenantId: {TenantId} and Employee: {EmployeeEmail}", tenantId, loggedInEmployee.Email ?? string.Empty);
// Return success response including tokens

File diff suppressed because it is too large Load Diff

View File

@ -2,8 +2,6 @@
using Marco.Pms.Model.Activities;
using Marco.Pms.Model.Dtos.Attendance;
using Marco.Pms.Model.Employees;
using Marco.Pms.Model.Entitlements;
using Marco.Pms.Model.Expenses;
using Marco.Pms.Model.Projects;
using Marco.Pms.Model.Utilities;
using Marco.Pms.Model.ViewModels.DashBoard;
@ -14,7 +12,6 @@ using MarcoBMS.Services.Service;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using System.Globalization;
namespace Marco.Pms.Services.Controllers
{
@ -28,35 +25,19 @@ namespace Marco.Pms.Services.Controllers
private readonly IProjectServices _projectServices;
private readonly ILoggingService _logger;
private readonly PermissionServices _permissionServices;
private readonly IServiceScopeFactory _serviceScopeFactory;
public static readonly Guid ActiveId = Guid.Parse("b74da4c2-d07e-46f2-9919-e75e49b12731");
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)
public DashboardController(ApplicationDbContext context, UserHelper userHelper, IProjectServices projectServices, ILoggingService logger, PermissionServices permissionServices)
{
_context = context;
_userHelper = userHelper;
_projectServices = projectServices;
_logger = logger;
_serviceScopeFactory = serviceScopeFactory;
_permissionServices = permissionServices;
tenantId = userHelper.GetTenantId();
}
[HttpGet("progression")]
public async Task<IActionResult> GetGraph([FromQuery] double days, [FromQuery] string FromDate, [FromQuery] Guid? projectId)
{
var tenantId = _userHelper.GetTenantId();
var LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
DateTime fromDate = new DateTime();
@ -168,6 +149,7 @@ namespace Marco.Pms.Services.Controllers
[HttpGet("projects")]
public async Task<IActionResult> GetProjectCount()
{
var tenantId = _userHelper.GetTenantId();
var LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
var projects = await _context.Projects.Where(p => p.TenantId == tenantId).ToListAsync();
@ -194,6 +176,7 @@ namespace Marco.Pms.Services.Controllers
{
try
{
var tenantId = _userHelper.GetTenantId();
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
_logger.LogInfo("GetTotalEmployees called by user {UserId} for ProjectId: {ProjectId}", loggedInEmployee.Id, projectId ?? Guid.Empty);
@ -286,6 +269,7 @@ namespace Marco.Pms.Services.Controllers
{
try
{
var tenantId = _userHelper.GetTenantId();
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
_logger.LogInfo("GetTotalTasks called by user {UserId} for ProjectId: {ProjectId}", loggedInEmployee.Id, projectId ?? Guid.Empty);
@ -364,10 +348,10 @@ namespace Marco.Pms.Services.Controllers
return StatusCode(500, ApiResponse<object>.ErrorResponse("An internal server error occurred.", null, 500));
}
}
[HttpGet("pending-attendance")]
public async Task<IActionResult> GetPendingAttendance()
{
Guid tenantId = _userHelper.GetTenantId();
var LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
var attendance = await _context.Attendes.Where(a => a.EmployeeId == LoggedInEmployee.Id && a.TenantId == tenantId).ToListAsync();
@ -390,6 +374,7 @@ namespace Marco.Pms.Services.Controllers
[HttpGet("project-attendance/{projectId}")]
public async Task<IActionResult> GetProjectAttendance(Guid projectId, [FromQuery] string? date)
{
Guid tenantId = _userHelper.GetTenantId();
var LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
DateTime currentDate = DateTime.UtcNow;
@ -443,6 +428,7 @@ namespace Marco.Pms.Services.Controllers
[HttpGet("activities/{projectId}")]
public async Task<IActionResult> GetActivities(Guid projectId, [FromQuery] string? date)
{
Guid tenantId = _userHelper.GetTenantId();
var LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
DateTime currentDate = DateTime.UtcNow;
@ -614,317 +600,5 @@ namespace Marco.Pms.Services.Controllers
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]
}
}
}
}

View File

@ -44,14 +44,14 @@ namespace Marco.Pms.Services.Controllers
}
[HttpGet]
public async Task<IActionResult> GetContactList([FromQuery] string? searchString, [FromQuery] List<Guid>? bucketIds, [FromQuery] List<Guid>? categoryIds, [FromQuery] Guid? projectId, [FromQuery] bool active = true)
public async Task<IActionResult> GetContactList([FromQuery] string? search, [FromQuery] List<Guid>? bucketIds, [FromQuery] List<Guid>? categoryIds, [FromQuery] Guid? projectId, [FromQuery] bool active = true)
{
ContactFilterDto filterDto = new ContactFilterDto
{
BucketIds = bucketIds,
CategoryIds = categoryIds
};
var response = await _directoryService.GetListOfContactsOld(searchString, active, filterDto, projectId);
var response = await _directoryService.GetListOfContactsOld(search, active, filterDto, projectId);
return StatusCode(response.StatusCode, response);

View File

@ -35,7 +35,6 @@ namespace Marco.Pms.Services.Controllers
private readonly ILoggingService _logger;
private readonly IMapper _mapper;
private readonly Guid tenantId;
private readonly Guid organizationId;
private static readonly Guid ProjectEntity = Guid.Parse("c8fe7115-aa27-43bc-99f4-7b05fabe436e");
private static readonly Guid EmployeeEntity = Guid.Parse("dbb9555a-7a0c-40f2-a9ed-f0463f1ceed7");
@ -53,7 +52,6 @@ namespace Marco.Pms.Services.Controllers
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_mapper = mapper ?? throw new ArgumentNullException(nameof(mapper));
tenantId = userHelper.GetTenantId();
organizationId = _userHelper.GetCurrentOrganizationId();
}
[HttpGet("list/{entityTypeId}/entity/{entityId}")]
@ -95,21 +93,21 @@ namespace Marco.Pms.Services.Controllers
return NotFound(ApiResponse<object>.ErrorResponse("Entity type not found", "Entity Type not found in database", 404));
}
//// Project permission check
//if (ProjectEntity == entityTypeId)
//{
// var hasProjectPermission = await _permission.HasProjectPermission(loggedInEmployee, entityId);
// if (!hasProjectPermission)
// {
// _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));
// }
//}
// Project permission check
if (ProjectEntity == entityTypeId)
{
var hasProjectPermission = await _permission.HasProjectPermission(loggedInEmployee, entityId);
if (!hasProjectPermission)
{
_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));
}
}
// Employee validation
else if (EmployeeEntity == entityTypeId)
{
var isEmployeeExists = await _context.Employees
.AnyAsync(e => e.Id == entityId && e.OrganizationId == organizationId);
.AnyAsync(e => e.Id == entityId && e.TenantId == tenantId);
if (!isEmployeeExists)
{
@ -693,7 +691,7 @@ namespace Marco.Pms.Services.Controllers
bool entityExists = false;
if (entityType.Equals(EmployeeEntity))
{
entityExists = await _context.Employees.AnyAsync(e => e.Id == model.EntityId && e.OrganizationId == organizationId);
entityExists = await _context.Employees.AnyAsync(e => e.Id == model.EntityId && e.TenantId == tenantId);
}
else if (entityType.Equals(ProjectEntity))
{
@ -1080,15 +1078,15 @@ namespace Marco.Pms.Services.Controllers
bool entityExists;
if (entityType.Equals(EmployeeEntity))
{
entityExists = await _context.Employees.AnyAsync(e => e.Id == oldAttachment.EntityId && e.OrganizationId == organizationId);
entityExists = await _context.Employees.AnyAsync(e => e.Id == oldAttachment.EntityId && e.TenantId == tenantId);
}
else if (entityType.Equals(ProjectEntity))
{
entityExists = await _context.Projects.AnyAsync(p => p.Id == oldAttachment.EntityId && p.TenantId == tenantId);
//if (entityExists)
//{
// entityExists = await _permission.HasProjectPermission(loggedInEmployee, oldAttachment.EntityId);
//}
if (entityExists)
{
entityExists = await _permission.HasProjectPermission(loggedInEmployee, oldAttachment.EntityId);
}
}
else
{

View File

@ -233,11 +233,83 @@ namespace MarcoBMS.Services.Controllers
_logger.LogInfo("GetEmployeesByProject called. EmployeeId: {EmployeeId}, ProjectId: {ProjectId}, showInactive: {ShowInactive}",
loggedInEmployee.Id, projectId ?? Guid.Empty, showInactive);
var employees = await _context.Employees
.Include(e => e.JobRole)
.Include(e => e.Organization)
.Where(e => e.OrganizationId == loggedInEmployee.OrganizationId && e.IsActive != showInactive)
.ToListAsync();
// Step 3: Fetch permissions concurrently
var viewAllTask = Task.Run(async () =>
{
var _permission = scope.ServiceProvider.GetRequiredService<PermissionServices>();
return await _permission.HasPermission(PermissionsMaster.ViewAllEmployees, loggedInEmployee.Id);
});
var viewTeamTask = Task.Run(async () =>
{
var _permission = scope.ServiceProvider.GetRequiredService<PermissionServices>();
return await _permission.HasPermission(PermissionsMaster.ViewTeamMembers, loggedInEmployee.Id);
});
await Task.WhenAll(viewAllTask, viewTeamTask);
var hasViewAllEmployeesPermission = viewAllTask.Result;
var hasViewTeamMembersPermission = viewTeamTask.Result;
List<Employee> employees = new List<Employee>();
// Step 4: Query based on permission
if (hasViewAllEmployeesPermission && !projectId.HasValue)
{
// OrganizationId needs to be retrieved from loggedInEmployee or context based on your app's structure
var employeeQuery = _context.Employees
.AsNoTracking() // Optimize EF query for read-only operation[web:1][web:13][web:18]
.Include(e => e.JobRole)
.Where(e => e.OrganizationId == organizationId);
employeeQuery = showInactive
? employeeQuery.Where(e => !e.IsActive)
: employeeQuery.Where(e => e.IsActive);
employees = await employeeQuery.ToListAsync();
_logger.LogInfo("Employee list fetched with full access. Count: {Count}", employees.Count);
}
else if (hasViewTeamMembersPermission && !showInactive && !projectId.HasValue)
{
// Only active team members with limited permission
var projectIds = await _projectServices.GetMyProjectIdsAsync(tenantId, loggedInEmployee);
employees = await _context.ProjectAllocations
.AsNoTracking()
.Include(pa => pa.Employee)
.ThenInclude(e => e!.JobRole)
.Where(pa =>
projectIds.Contains(pa.ProjectId)
&& pa.IsActive
&& pa.Employee != null
&& pa.Employee.IsActive
&& pa.TenantId == tenantId)
.Select(pa => pa.Employee!)
.Distinct()
.ToListAsync();
_logger.LogInfo("Employee list fetched with limited access (active only). Count: {Count}", employees.Count);
}
// If a specific projectId is provided, override employee fetching to ensure strict project context
if (projectId.HasValue)
{
employees = await _context.ProjectAllocations
.AsNoTracking()
.Include(pa => pa.Employee)
.ThenInclude(e => e!.JobRole)
.Where(pa =>
pa.ProjectId == projectId
&& pa.IsActive
&& pa.Employee != null
&& pa.Employee.IsActive
&& pa.TenantId == tenantId)
.Select(pa => pa.Employee!)
.Distinct()
.ToListAsync();
_logger.LogInfo("Employee list fetched for specific project. ProjectId: {ProjectId}. Count: {Count}",
projectId, employees.Count);
}
// Step 5: Map to view model
result = employees.Select(e => _mapper.Map<EmployeeVM>(e)).Distinct().ToList();
@ -257,7 +329,7 @@ namespace MarcoBMS.Services.Controllers
[HttpGet("basic")]
public async Task<IActionResult> GetEmployeesByProjectBasic(Guid? projectId, [FromQuery] string? searchString, [FromQuery] bool sendAll = false)
public async Task<IActionResult> GetEmployeesByProjectBasic(Guid? projectId, [FromQuery] string? searchString, [FromQuery] bool allEmployee)
{
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
var employeeQuery = _context.Employees.Where(e => e.IsActive);
@ -281,11 +353,17 @@ namespace MarcoBMS.Services.Controllers
var searchStringLower = searchString.ToLower();
employeeQuery = employeeQuery.Where(e => (e.FirstName + " " + e.LastName).ToLower().Contains(searchStringLower));
}
if (!sendAll)
var query = employeeQuery.OrderBy(e => e.FirstName);
if (!allEmployee)
{
employeeQuery = employeeQuery.Take(10);
query = (IOrderedQueryable<Employee>)query.Take(10);
}
var response = await employeeQuery.Select(e => _mapper.Map<BasicEmployeeVM>(e)).ToListAsync();
var response = await query
.Select(e => _mapper.Map<BasicEmployeeVM>(e))
.ToListAsync();
return Ok(ApiResponse<object>.SuccessResponse(response, $"{response.Count} records of employees fetched successfully", 200));
}
@ -401,9 +479,6 @@ namespace MarcoBMS.Services.Controllers
Guid tenantId = _userHelper.GetTenantId();
var LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
Guid employeeId = Guid.Empty;
if (model == null)
return BadRequest(ApiResponse<object>.ErrorResponse("Invalid data", "Invaild Data", 400));
@ -536,7 +611,6 @@ namespace MarcoBMS.Services.Controllers
{
// Correlation and context capture for logs
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
Guid organizationId = model.OrganizationId ?? loggedInEmployee.OrganizationId;
{
if (model == null)
@ -560,10 +634,10 @@ namespace MarcoBMS.Services.Controllers
if (model.Id.HasValue && model.Id.Value != Guid.Empty)
{
existingEmployee = await _context.Employees
.FirstOrDefaultAsync(e => e.Id == model.Id && e.OrganizationId == organizationId);
.FirstOrDefaultAsync(e => e.Id == model.Id);
if (existingEmployee == null)
{
_logger.LogInfo("Employee not found for update. Id={EmployeeId}, Org={OrgId}", model.Id, organizationId);
_logger.LogInfo("Employee not found for update. Id={EmployeeId}", model.Id);
return NotFound(ApiResponse<object>.ErrorResponse("Employee not found", "Employee not found in database", 404));
}
}
@ -658,7 +732,7 @@ namespace MarcoBMS.Services.Controllers
existingEmployee.ApplicationUserId = createdIdentityUser.Id;
await SendResetIfApplicableAsync(createdIdentityUser, existingEmployee.FirstName ?? "User");
}
existingEmployee.OrganizationId = organizationId;
await _context.SaveChangesAsync();
employeeId = existingEmployee.Id;
@ -680,7 +754,7 @@ namespace MarcoBMS.Services.Controllers
newEmployee.ApplicationUserId = createdIdentityUser.Id;
await SendResetIfApplicableAsync(createdIdentityUser, newEmployee.FirstName ?? "User");
}
newEmployee.OrganizationId = organizationId;
await _context.Employees.AddAsync(newEmployee);
await _context.SaveChangesAsync();
@ -812,7 +886,6 @@ namespace MarcoBMS.Services.Controllers
public async Task<IActionResult> CreateUserMobileAsync([FromBody] MobileUserManageDto model)
{
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
Guid organizationId = model.OrganizationId ?? loggedInEmployee.OrganizationId;
if (tenantId == Guid.Empty)
{
_logger.LogWarning("Tenant resolution failed in CreateUserMobile"); // structured warning
@ -848,11 +921,11 @@ namespace MarcoBMS.Services.Controllers
if (model.Id == null || model.Id == Guid.Empty)
{
var emailExists = await _context.Employees
.AnyAsync(e => e.Email == model.Email);
.AnyAsync(e => e.Email == model.Email && e.OrganizationId == model.OrganizationId);
if (emailExists)
if (emailExists && !string.IsNullOrWhiteSpace(model.Email))
{
_logger.LogInfo("Employee email already exists in org. Email={Email}, Org={OrgId}", model.Email ?? string.Empty, organizationId);
_logger.LogInfo("Employee email already exists in org. Email={Email}, Org={OrgId}", model.Email, model.OrganizationId);
return StatusCode(409, ApiResponse<object>.ErrorResponse("Employee with email already exists", "Employee with this email already exists", 409));
}
@ -869,7 +942,7 @@ namespace MarcoBMS.Services.Controllers
JoiningDate = model.JoiningDate,
JobRoleId = model.JobRoleId,
Photo = imageBytes,
OrganizationId = organizationId,
OrganizationId = model.OrganizationId,
HasApplicationAccess = model.HasApplicationAccess,
};
@ -922,7 +995,7 @@ namespace MarcoBMS.Services.Controllers
// Update path: fetch scoped to tenant
var employeeId = model.Id.Value;
var existingEmployee = await _context.Employees
.FirstOrDefaultAsync(e => e.Id == employeeId && e.TenantId == tenantId); // tenant-safe lookup
.FirstOrDefaultAsync(e => e.Id == employeeId); // tenant-safe lookup
if (existingEmployee is null)
{
@ -937,17 +1010,17 @@ namespace MarcoBMS.Services.Controllers
existingEmployee.PhoneNumber = model.PhoneNumber;
existingEmployee.JoiningDate = model.JoiningDate;
existingEmployee.JobRoleId = model.JobRoleId;
existingEmployee.OrganizationId = organizationId;
existingEmployee.OrganizationId = model.OrganizationId;
existingEmployee.HasApplicationAccess = model.HasApplicationAccess;
if (string.IsNullOrWhiteSpace(existingEmployee.Email) && !string.IsNullOrWhiteSpace(model.Email))
{
var emailExists = await _context.Employees
.AnyAsync(e => e.Email == model.Email && e.OrganizationId == model.OrganizationId);
.AnyAsync(e => e.Email == model.Email);
if (emailExists)
{
_logger.LogInfo("Employee email already exists in org. Email={Email}, Org={OrgId}", model.Email, organizationId);
_logger.LogInfo("Employee email already exists in org. Email={Email}, Org={OrgId}", model.Email, model.OrganizationId);
return StatusCode(409, ApiResponse<object>.ErrorResponse("Employee with email already exists", "Employee with this email already exists", 409));
}
existingEmployee.Email = model.Email;
@ -1011,7 +1084,7 @@ namespace MarcoBMS.Services.Controllers
Guid tenantId = _userHelper.GetTenantId();
var LoggedEmployee = await _userHelper.GetCurrentEmployeeAsync();
Employee? employee = await _context.Employees.FirstOrDefaultAsync(e => e.Id == id && e.OrganizationId == organizationId);
Employee? employee = await _context.Employees.FirstOrDefaultAsync(e => e.Id == id && e.TenantId == tenantId);
if (employee == null)
{
_logger.LogWarning("Employee with ID {EmploueeId} not found in database", id);
@ -1181,17 +1254,13 @@ namespace MarcoBMS.Services.Controllers
}
// Prepare reset link sender helper
private async Task SendResetIfApplicableAsync(ApplicationUser u, string firstName)
{
if (!string.IsNullOrWhiteSpace(u.Email))
{
var token = await _userManager.GeneratePasswordResetTokenAsync(u);
var resetLink = $"{_configuration["AppSettings:WebFrontendUrl"]}/reset-password?token={WebUtility.UrlEncode(token)}";
await _emailSender.SendResetPasswordEmailOnRegister(u.Email, firstName, resetLink);
_logger.LogInfo("Reset password email queued. Email={Email}", u.Email);
}
var token = await _userManager.GeneratePasswordResetTokenAsync(u);
var resetLink = $"{_configuration["AppSettings:WebFrontendUrl"]}/reset-password?token={WebUtility.UrlEncode(token)}";
await _emailSender.SendResetPasswordEmailOnRegister(u.Email ?? "", firstName, resetLink);
_logger.LogInfo("Reset password email queued. Email={Email}", u.Email ?? "");
}
}
}

View File

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

View File

@ -1,6 +1,5 @@
using Marco.Pms.DataAccess.Data;
using Marco.Pms.Model.Dtos.Activities;
using Marco.Pms.Model.Dtos.Collection;
using Marco.Pms.Model.Dtos.DocumentManager;
using Marco.Pms.Model.Dtos.Master;
using Marco.Pms.Model.Forum;
@ -976,38 +975,5 @@ namespace Marco.Pms.Services.Controllers
}
#endregion
#region =================================================================== Payment Adjustment Head APIs ===================================================================
[HttpGet("payment-adjustment-head/list")]
public async Task<IActionResult> GetpaymentAdjustmentHeadsList([FromQuery] bool isActive = true)
{
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
var response = await _masterService.GetPaymentAdjustmentHeadListAsync(isActive, loggedInEmployee, tenantId);
return StatusCode(response.StatusCode, response);
}
[HttpPost("payment-adjustment-head")]
public async Task<IActionResult> CreatePaymentAdjustmentHead([FromBody] PaymentAdjustmentHeadDto dto)
{
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
var response = await _masterService.CreatePaymentAdjustmentHeadAsync(dto, loggedInEmployee, tenantId);
return StatusCode(response.StatusCode, response);
}
[HttpPut("payment-adjustment-head/edit/{id}")]
public async Task<IActionResult> UpdatePaymentAdjustmentHead(Guid id, [FromBody] PaymentAdjustmentHeadDto dto)
{
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
var response = await _masterService.UpdatePaymentAdjustmentHeadAsync(id, dto, loggedInEmployee, tenantId);
return StatusCode(response.StatusCode, response);
}
[HttpDelete("payment-adjustment-head/delete/{id}")]
public async Task<IActionResult> DeletePaymentAdjustmentHead(Guid id, [FromQuery] bool isActive = false)
{
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
var response = await _masterService.DeletePaymentAdjustmentHeadAsync(id, isActive, loggedInEmployee, tenantId);
return StatusCode(response.StatusCode, response);
}
#endregion
}
}

View File

@ -9,6 +9,7 @@ using Marco.Pms.Model.ViewModels.Activities;
using Marco.Pms.Model.ViewModels.Master;
using Marco.Pms.Model.ViewModels.Organization;
using Marco.Pms.Model.ViewModels.Projects;
using Marco.Pms.Services.Helpers;
using Marco.Pms.Services.Service;
using MarcoBMS.Services.Helpers;
using MarcoBMS.Services.Service;
@ -30,6 +31,7 @@ namespace Marco.Pms.Services.Controllers
private readonly UserHelper _userHelper;
private readonly Guid tenantId;
private readonly IMapper _mapper;
private readonly Guid loggedOrganizationId;
private readonly ILoggingService _logger;
private static readonly Guid PMCProvider = Guid.Parse("b1877a3b-8832-47b1-bbe3-dc7e98672f49");
@ -47,6 +49,7 @@ namespace Marco.Pms.Services.Controllers
_userHelper = userHelper ?? throw new ArgumentNullException(nameof(userHelper));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_mapper = mapper ?? throw new ArgumentNullException(nameof(mapper));
loggedOrganizationId = _userHelper.GetCurrentOrganizationId();
tenantId = userHelper.GetTenantId();
}
#region =================================================================== Get Functions ===================================================================
@ -669,6 +672,8 @@ namespace Marco.Pms.Services.Controllers
Service = _mapper.Map<ServiceMasterVM>(s)
}).ToList();
await AssignApplicationRoleToOrganization(organization.Id, project.TenantId);
return Ok(ApiResponse<object>.SuccessResponse(response, "Organization successfully assigned to the project", 200));
}
catch (DbUpdateException dbEx)
@ -717,34 +722,36 @@ namespace Marco.Pms.Services.Controllers
return NotFound(ApiResponse<object>.ErrorResponse("Organization not found", "Organization not found in database", 404));
}
if (organizationTenantMapping == null)
{
// Create new tenant-organization mapping if none exists
var newMapping = new TenantOrgMapping
{
OrganizationId = organization.Id,
SPRID = organization.SPRID,
AssignedDate = DateTime.UtcNow,
IsActive = true,
AssignedById = loggedInEmployee.Id,
TenantId = tenantId
};
_context.TenantOrgMappings.Add(newMapping);
await _context.SaveChangesAsync();
await transaction.CommitAsync();
_logger.LogInfo("Assigned organization {OrganizationId} to tenant {TenantId} successfully.", organizationId, tenantId);
}
else
if (organizationTenantMapping != null)
{
_logger.LogInfo("Organization {OrganizationId} is already assigned to tenant {TenantId}. No action taken.", organizationId, tenantId);
// Commit transaction anyway to complete scope cleanly (optional)
await transaction.CommitAsync();
await transaction.RollbackAsync();
return StatusCode(409, ApiResponse<object>.ErrorResponse("Organization is already assigned to tenant", "Organization is already assigned to tenant", 409));
}
// Create new tenant-organization mapping if none exists
var newMapping = new TenantOrgMapping
{
OrganizationId = organization.Id,
SPRID = organization.SPRID,
AssignedDate = DateTime.UtcNow,
IsActive = true,
AssignedById = loggedInEmployee.Id,
TenantId = tenantId
};
_context.TenantOrgMappings.Add(newMapping);
await _context.SaveChangesAsync();
await transaction.CommitAsync();
_logger.LogInfo("Assigned organization {OrganizationId} to tenant {TenantId} successfully.", organizationId, tenantId);
// Prepare response view model
var response = _mapper.Map<BasicOrganizationVm>(organization);
await AssignApplicationRoleToOrganization(organization.Id, tenantId);
return Ok(ApiResponse<object>.SuccessResponse(response, "Organization has been assigned to tenant", 200));
}
catch (DbUpdateException dbEx)
@ -938,45 +945,98 @@ namespace Marco.Pms.Services.Controllers
#endregion
#region =================================================================== Helper Functions ===================================================================
//private ServicesProviderFilter? TryDeserializeServicesProviderFilter(string? filter)
//{
// if (string.IsNullOrWhiteSpace(filter))
// {
// return null;
// }
// var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true };
// ServicesProviderFilter? documentFilter = null;
private async Task AssignApplicationRoleToOrganization(Guid organizationId, Guid tenantId)
{
if (loggedOrganizationId == organizationId)
{
return;
}
await using var _context = await _dbContextFactory.CreateDbContextAsync();
using var scope = _serviceScope.CreateScope();
// try
// {
// // First, try to deserialize directly. This is the expected case (e.g., from a web client).
// documentFilter = JsonSerializer.Deserialize<ServicesProviderFilter>(filter, options);
// }
// catch (JsonException ex)
// {
// _logger.LogError(ex, "[{MethodName}] Failed to directly deserialize filter. Attempting to unescape and re-parse. Filter: {Filter}", nameof(TryDeserializeServicesProviderFilter), filter);
var rootEmployee = await _context.Employees
.Include(e => e.ApplicationUser)
.FirstOrDefaultAsync(e => e.ApplicationUser != null && e.ApplicationUser.IsRootUser.HasValue && e.ApplicationUser.IsRootUser.Value && e.OrganizationId == organizationId && e.IsPrimary);
if (rootEmployee == null)
{
return;
}
string serviceProviderRoleName = "Service Provider Role";
// // If direct deserialization fails, it might be an escaped string (common with tools like Postman or some mobile clients).
// try
// {
// // Unescape the string first, then deserialize the result.
// string unescapedJsonString = JsonSerializer.Deserialize<string>(filter, options) ?? "";
// if (!string.IsNullOrWhiteSpace(unescapedJsonString))
// {
// documentFilter = JsonSerializer.Deserialize<ServicesProviderFilter>(unescapedJsonString, options);
// }
// }
// catch (JsonException ex1)
// {
// // If both attempts fail, log the final error and return null.
// _logger.LogError(ex1, "[{MethodName}] All attempts to deserialize the filter failed. Filter will be ignored. Filter: {Filter}", nameof(TryDeserializeServicesProviderFilter), filter);
// return null;
// }
// }
// return documentFilter;
//}
var serviceProviderRole = await _context.ApplicationRoles.FirstOrDefaultAsync(ar => ar.Role == serviceProviderRoleName && ar.TenantId == tenantId);
if (serviceProviderRole == null)
{
serviceProviderRole = new Model.Roles.ApplicationRole
{
Id = Guid.NewGuid(),
Role = serviceProviderRoleName,
Description = serviceProviderRoleName,
IsSystem = true,
TenantId = tenantId
};
_context.ApplicationRoles.Add(serviceProviderRole);
var rolePermissionMappigs = new List<RolePermissionMappings> {
new RolePermissionMappings
{
ApplicationRoleId = serviceProviderRole.Id,
FeaturePermissionId = PermissionsMaster.ViewProject
},
new RolePermissionMappings
{
ApplicationRoleId = serviceProviderRole.Id,
FeaturePermissionId = PermissionsMaster.ViewProjectInfra
},
new RolePermissionMappings
{
ApplicationRoleId = serviceProviderRole.Id,
FeaturePermissionId = PermissionsMaster.ViewTask
},
new RolePermissionMappings
{
ApplicationRoleId = serviceProviderRole.Id,
FeaturePermissionId = PermissionsMaster.ViewAllEmployees
},
new RolePermissionMappings
{
ApplicationRoleId = serviceProviderRole.Id,
FeaturePermissionId = PermissionsMaster.TeamAttendance
},
new RolePermissionMappings
{
ApplicationRoleId = serviceProviderRole.Id,
FeaturePermissionId = PermissionsMaster.AssignRoles
},
new RolePermissionMappings
{
ApplicationRoleId = serviceProviderRole.Id,
FeaturePermissionId = PermissionsMaster.ManageProjectInfra
},
new RolePermissionMappings
{
ApplicationRoleId = serviceProviderRole.Id,
FeaturePermissionId = PermissionsMaster.AssignAndReportProgress
},
new RolePermissionMappings
{
ApplicationRoleId = serviceProviderRole.Id,
FeaturePermissionId = PermissionsMaster.AddAndEditTask
}
};
_context.RolePermissionMappings.AddRange(rolePermissionMappigs);
}
_context.EmployeeRoleMappings.Add(new EmployeeRoleMapping
{
EmployeeId = rootEmployee.Id,
RoleId = serviceProviderRole.Id,
IsEnabled = true,
TenantId = tenantId
});
var _cache = scope.ServiceProvider.GetRequiredService<CacheUpdateHelper>();
await _cache.ClearAllPermissionIdsByEmployeeID(rootEmployee.Id, tenantId);
}
#endregion
}
}

View File

@ -560,6 +560,13 @@ namespace MarcoBMS.Services.Controllers
var response = await _projectServices.GetAssignedOrganizationsToProjectAsync(projectId, tenantId, loggedInEmployee);
return StatusCode(response.StatusCode, response);
}
[HttpGet("get/assigned/organization/dropdown/{projectId}")]
public async Task<IActionResult> GetAssignedOrganizationsToProjectForDropdownAsync(Guid projectId)
{
Employee loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
var response = await _projectServices.GetAssignedOrganizationsToProjectForDropdownAsync(projectId, tenantId, loggedInEmployee);
return StatusCode(response.StatusCode, response);
}
#endregion
}

View File

@ -449,7 +449,7 @@ namespace Marco.Pms.Services.Controllers
}
[HttpGet("report-attendance")]
public async Task<IActionResult> GetAttendanceReportAsync()
public async Task<IActionResult> GetAttendanceReportAsync([FromQuery] bool isCurrentMonth = false)
{
Guid tenantId = _userHelper.GetTenantId();
using var scope = _serviceScopeFactory.CreateScope();
@ -458,48 +458,75 @@ namespace Marco.Pms.Services.Controllers
DateTime firstDayOfMonth = new DateTime(today.Year, today.Month, 1);
DateTime firstDayOfNextMonth = firstDayOfMonth.AddMonths(1);
if (!isCurrentMonth)
{
firstDayOfNextMonth = firstDayOfMonth;
firstDayOfMonth = firstDayOfMonth.AddMonths(-1);
}
// Generate list of all dates in the month
var allDates = Enumerable.Range(0, (firstDayOfNextMonth - firstDayOfMonth).Days)
.Select(offset => firstDayOfMonth.AddDays(offset))
.ToList();
var attendances = await _context.Attendes
.Include(a => a.Employee)
.Where(a => a.AttendanceDate >= firstDayOfMonth && a.AttendanceDate < firstDayOfNextMonth && a.Employee != null && a.TenantId == tenantId)
.GroupBy(a => a.ProjectID)
.ToListAsync();
var attendancesTask = Task.Run(async () =>
{
await using var context = await _dbContextFactory.CreateDbContextAsync();
return await context.Attendes
.Where(a => a.AttendanceDate >= firstDayOfMonth && a.AttendanceDate < firstDayOfNextMonth && a.Employee != null && a.TenantId == tenantId)
.GroupBy(a => a.ProjectID)
.ToListAsync();
});
var projectAllocationTask = Task.Run(async () =>
{
await using var context = await _dbContextFactory.CreateDbContextAsync();
return await context.ProjectAllocations
.Include(pa => pa.Employee)
.Where(pa => pa.TenantId == tenantId && pa.IsActive)
.ToListAsync();
});
await Task.WhenAll(attendancesTask, projectAllocationTask);
var attendances = attendancesTask.Result;
var projectAllocations = projectAllocationTask.Result;
var result = attendances.Select(g =>
{
var projectAllocation = projectAllocations.Where(pa => pa.ProjectId == g.Key && pa.Employee != null).ToList();
var projectAttendance = projectAllocation.Select(pa =>
{
var attendances = g.Where(a => a.EmployeeId == pa.EmployeeId).ToList();
var attendanceDate = attendances.Select(a => a.AttendanceDate.Date).ToList();
return new
{
FirstName = pa.Employee!.FirstName,
LastName = pa.Employee.LastName,
Attendances = allDates.Select(d =>
{
var attendance = attendances.FirstOrDefault(a => a.AttendanceDate.Date == d);
return new
{
AttendanceDate = d,
CheckIn = attendance?.InTime,
CheckOut = attendance?.OutTime,
Activity = attendance?.Activity,
IsApproved = attendance?.ApprovedById.HasValue,
};
}).ToList(),
CheckInCheckOutDone = attendances.Where(a => a.InTime.HasValue && a.OutTime.HasValue && a.Activity == ATTENDANCE_MARK_TYPE.REGULARIZE).Count(),
CheckInDone = attendances.Where(a => a.InTime.HasValue).Count(),
CheckOutPending = attendances.Where(a => a.InTime.HasValue && !a.OutTime.HasValue).Count(),
RejectedRegularize = attendances.Where(a => a.Activity == ATTENDANCE_MARK_TYPE.REGULARIZE_REJECT).Count(),
AbsentAttendance = allDates.Where(d => !attendanceDate.Contains(d) && d.DayOfWeek != DayOfWeek.Sunday).Count()
};
}).OrderBy(ar => ar.FirstName).ThenBy(ar => ar.LastName).ToList();
return new
{
ProjectName = _context.Projects.Where(p => p.Id == g.Key && p.TenantId == tenantId).Select(p => p.Name).FirstOrDefault(),
ProjectAttendance = g.GroupBy(a => a.Employee).Select(gp =>
{
var attendanceDate = gp.Select(a => a.AttendanceDate.Date).ToList();
return new
{
FirstName = gp.Key!.FirstName,
LastName = gp.Key.LastName,
Attendances = allDates.Select(d =>
{
var attendance = gp.FirstOrDefault(a => a.AttendanceDate.Date == d);
return new
{
AttendanceDate = d,
CheckIn = attendance?.InTime,
CheckOut = attendance?.OutTime,
Activity = attendance?.Activity,
IsApproved = attendance?.ApprovedById.HasValue,
};
}).ToList(),
CheckInCheckOutDone = gp.Where(a => a.InTime.HasValue && a.OutTime.HasValue && a.Activity == ATTENDANCE_MARK_TYPE.REGULARIZE).Count(),
CheckInDone = gp.Where(a => a.InTime.HasValue).Count(),
CheckOutPending = gp.Where(a => a.InTime.HasValue && !a.OutTime.HasValue).Count(),
RejectedRegularize = gp.Where(a => a.Activity == ATTENDANCE_MARK_TYPE.REGULARIZE_REJECT).Count(),
AbsentAttendance = allDates.Where(d => !attendanceDate.Contains(d) && d.DayOfWeek != DayOfWeek.Sunday).Count()
};
}).OrderBy(ar => ar.FirstName).ThenBy(ar => ar.LastName).ToList()
ProjectAttendance = projectAttendance
};
}).ToList();

View File

@ -273,7 +273,12 @@ namespace Marco.Pms.Services.Controllers
return StatusCode(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
await using var _context = await _dbContextFactory.CreateDbContextAsync();
@ -292,13 +297,6 @@ namespace Marco.Pms.Services.Controllers
}
_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
var employeesTask = Task.Run(async () =>
{
@ -552,7 +550,7 @@ namespace Marco.Pms.Services.Controllers
JobRole = adminJobRole, // Link to the newly created role
CurrentAddress = model.BillingAddress,
IsActive = true,
IsSystem = true,
IsSystem = false,
IsPrimary = true,
OrganizationId = organization.Id,
HasApplicationAccess = true
@ -568,36 +566,43 @@ namespace Marco.Pms.Services.Controllers
};
_context.ApplicationRoles.Add(applicationRole);
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
var rolePermissionMappigs = new List<RolePermissionMappings> {
new RolePermissionMappings
{
ApplicationRoleId = applicationRole.Id,
FeaturePermissionId = PermissionsMaster.ModifyTenant
},
new RolePermissionMappings
{
ApplicationRoleId = applicationRole.Id,
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
}
};
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,
FeaturePermissionId = PermissionsMaster.ModifyTenant
});
rolePermissionMappigs.Add(new RolePermissionMappings
{
ApplicationRoleId = applicationRole.Id,
FeaturePermissionId = PermissionsMaster.ViewTenant
});
_context.RolePermissionMappings.AddRange(rolePermissionMappigs);
_context.EmployeeRoleMappings.Add(new EmployeeRoleMapping
@ -646,22 +651,6 @@ namespace Marco.Pms.Services.Controllers
_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.
await _context.SaveChangesAsync();
@ -1024,10 +1013,6 @@ namespace Marco.Pms.Services.Controllers
try
{
_ = Task.Run(async () =>
{
await ClearPermissionForTenant();
});
var features = await _featureDetailsHelper.GetFeatureDetails(subscriptionPlan.FeaturesId);
if (features == null)
{
@ -1080,7 +1065,7 @@ namespace Marco.Pms.Services.Controllers
// Get root employee and role for this tenant
var rootEmployee = await _context.Employees
.Include(e => e.ApplicationUser)
.FirstOrDefaultAsync(e => e.ApplicationUser != null && (e.ApplicationUser.IsRootUser ?? false) && e.TenantId == model.TenantId);
.FirstOrDefaultAsync(e => e.ApplicationUser != null && (e.ApplicationUser.IsRootUser ?? false) && e.OrganizationId == tenant.OrganizationId);
if (rootEmployee == null)
{
@ -1138,6 +1123,9 @@ namespace Marco.Pms.Services.Controllers
_logger.LogInfo("Removed {Count} role permission mappings for role {RoleId}", deleteMappings.Count, roleId);
}
var _cache = scope.ServiceProvider.GetRequiredService<CacheUpdateHelper>();
await _cache.ClearAllEmployeesFromCacheByTenantId(tenant.Id);
var _masteData = scope.ServiceProvider.GetRequiredService<MasterDataService>();
if (features.Modules?.ProjectManagement?.Enabled ?? false)
@ -1336,10 +1324,6 @@ namespace Marco.Pms.Services.Controllers
_logger.LogInfo("Subscription plan changed: Tenant={TenantId}, NewPlan={PlanId}",
model.TenantId, model.PlanId);
_ = Task.Run(async () =>
{
await ClearPermissionForTenant();
});
// 8. Update tenant permissions based on subscription features.
var features = await _featureDetailsHelper.GetFeatureDetails(subscriptionPlan.FeaturesId);
@ -1374,7 +1358,7 @@ namespace Marco.Pms.Services.Controllers
// 8c. Find root employee & role for this tenant.
var rootEmployee = await context.Employees
.Include(e => e.ApplicationUser)
.FirstOrDefaultAsync(e => e.ApplicationUser != null && (e.ApplicationUser.IsRootUser ?? false) && e.TenantId == model.TenantId);
.FirstOrDefaultAsync(e => e.ApplicationUser != null && (e.ApplicationUser.IsRootUser ?? false) && e.OrganizationId == tenant.OrganizationId);
if (rootEmployee == null)
{
@ -1385,7 +1369,8 @@ namespace Marco.Pms.Services.Controllers
var rootRoleId = await context.EmployeeRoleMappings
.AsNoTracking()
.Where(er => er.EmployeeId == rootEmployee.Id && er.TenantId == model.TenantId)
.Include(er => er.Role)
.Where(er => er.EmployeeId == rootEmployee.Id && er.TenantId == model.TenantId && er.Role != null && er.Role.Role == "Super User")
.Select(er => er.RoleId)
.FirstOrDefaultAsync();
@ -1450,6 +1435,9 @@ namespace Marco.Pms.Services.Controllers
_logger.LogInfo("Permissions revoked: {Count} for Role={RoleId}", mappingsToRemove.Count, rootRoleId);
}
var _cache = scope.ServiceProvider.GetRequiredService<CacheUpdateHelper>();
await _cache.ClearAllEmployeesFromCacheByTenantId(tenant.Id);
var _masteData = scope.ServiceProvider.GetRequiredService<MasterDataService>();
if (features.Modules?.ProjectManagement?.Enabled ?? false)
@ -1834,19 +1822,6 @@ namespace Marco.Pms.Services.Controllers
return ApiResponse<SubscriptionPlanVM>.SuccessResponse(VM, "Success", 200);
}
private async Task ClearPermissionForTenant()
{
await using var _context = await _dbContextFactory.CreateDbContextAsync();
using var scope = _serviceScopeFactory.CreateScope();
var _cache = scope.ServiceProvider.GetRequiredService<CacheUpdateHelper>();
var _cacheLogger = scope.ServiceProvider.GetRequiredService<ILoggingService>();
var employeeIds = await _context.Employees.Where(e => e.TenantId == tenantId).Select(e => e.Id).ToListAsync();
await _cache.ClearAllEmployeesFromCacheByEmployeeIds(employeeIds, tenantId);
_cacheLogger.LogInfo("{EmployeeCount} number of employee deleted", employeeIds.Count);
}
#endregion
}
}

View File

@ -956,6 +956,28 @@ namespace Marco.Pms.Services.Helpers
_logger.LogError(ex, "Error occured while deleting all employees from Cache");
}
}
public async Task ClearAllEmployeesFromCacheByOnlyEmployeeId(Guid employeeId)
{
try
{
var response = await _employeeCache.ClearAllEmployeesFromCacheByOnlyEmployeeId(employeeId);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error occured while deleting all employees from Cache");
}
}
public async Task ClearAllEmployeesFromCacheByTenantId(Guid tenantId)
{
try
{
var response = await _employeeCache.ClearAllEmployeesFromCacheByTenantId(tenantId);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error occured while deleting all employees from Cache");
}
}
public async Task ClearAllEmployees()
{
try

View File

@ -24,7 +24,7 @@ namespace MarcoBMS.Services.Helpers
public async Task<Employee> GetEmployeeByID(Guid EmployeeID)
{
return await _context.Employees.Include(e => e.JobRole).Include(e => e.Organization).FirstOrDefaultAsync(e => e.Id == EmployeeID) ?? new Employee { };
return await _context.Employees.Include(e => e.JobRole).FirstOrDefaultAsync(e => e.Id == EmployeeID) ?? new Employee { };
}
public async Task<Employee> GetEmployeeByApplicationUserID(string ApplicationUserID)

View File

@ -253,29 +253,16 @@ namespace Marco.Pms.Services.Helpers
!ts.IsCancelled &&
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)
{
_logger.LogWarning("No active subscription found for tenant: {TenantId}", tenantId);
return featureIds;
return new List<Guid>();
}
_logger.LogDebug("Active subscription found for tenant: {TenantId}, PlanId: {PlanId}",
tenantId, tenantSubscription.Plan!.Id);
//var featureIds = new List<Guid> { new Guid("be3b3afc-6ccf-4566-b9b6-aafcb65546be") };
var featureIds = new List<Guid> { new Guid("2f3509b7-160d-410a-b9b6-daadd96c986d"), new Guid("be3b3afc-6ccf-4566-b9b6-aafcb65546be") };
// Step 2: Get feature details from Plan
var featureDetails = await _featureDetailsHelper.GetFeatureDetails(tenantSubscription.Plan!.FeaturesId);

View File

@ -190,6 +190,7 @@ namespace Marco.Pms.Services.Helpers
double totalCompletedWork = workItems.Sum(w => w.CompletedWork);
var todayAssignedTasks = tasks.Where(t => t.AssignmentDate.Date == reportDate).ToList();
var todaysCompletedTasks = tasks.Where(t => t.AssignmentDate.Date == reportDate && t.ReportedById != null).ToList();
var reportPending = tasks.Where(t => t.ReportedDate == null).ToList();
double totalPlannedTask = todayAssignedTasks.Sum(t => t.PlannedTask);
@ -263,14 +264,17 @@ namespace Marco.Pms.Services.Helpers
// Fill report
statisticReport.TodaysAttendances = checkedInEmployeeIds.Count;
statisticReport.TotalEmployees = assignedEmployeeIds.Count;
statisticReport.AttendancePercentage = assignedEmployeeIds.Count > 0 ? (checkedInEmployeeIds.Count / assignedEmployeeIds.Count) * 100 : 0;
statisticReport.RegularizationPending = regularizationIds.Count;
statisticReport.CheckoutPending = checkoutPendingIds.Count;
statisticReport.TotalPlannedWork = totalPlannedWork;
statisticReport.TotalCompletedWork = totalCompletedWork;
statisticReport.CompletionStatus = totalPlannedWork > 0 ? (totalCompletedWork / totalPlannedWork) * 100 : 0;
statisticReport.TotalPlannedTask = totalPlannedTask;
statisticReport.TotalCompletedTask = totalCompletedTask;
statisticReport.CompletionStatus = totalPlannedWork > 0 ? totalCompletedWork / totalPlannedWork : 0;
statisticReport.AttendancePercentage = totalCompletedTask > 0 ? (totalCompletedTask / totalPlannedTask) * 100 : 0;
statisticReport.TodaysAssignTasks = todayAssignedTasks.Count;
statisticReport.TodaysCompletedTasks = todaysCompletedTasks.Count;
statisticReport.ReportPending = reportPending.Count;
statisticReport.TeamOnSite = teamOnSite;
statisticReport.PerformedTasks = performedTasks;

View File

@ -3,7 +3,6 @@ using Microsoft.AspNetCore.SignalR;
namespace Marco.Pms.Services.Hubs
{
//[Authorize]
public class MarcoHub : Hub
{
private readonly ILoggingService _logger;

View File

@ -1,11 +1,9 @@
using AutoMapper;
using Marco.Pms.Model.AppMenu;
using Marco.Pms.Model.Collection;
using Marco.Pms.Model.Directory;
using Marco.Pms.Model.DocumentManager;
using Marco.Pms.Model.Dtos.Activities;
using Marco.Pms.Model.Dtos.AppMenu;
using Marco.Pms.Model.Dtos.Collection;
using Marco.Pms.Model.Dtos.Directory;
using Marco.Pms.Model.Dtos.DocumentManager;
using Marco.Pms.Model.Dtos.Employees;
@ -28,7 +26,6 @@ using Marco.Pms.Model.Projects;
using Marco.Pms.Model.TenantModels;
using Marco.Pms.Model.TenantModels.MongoDBModel;
using Marco.Pms.Model.ViewModels.Activities;
using Marco.Pms.Model.ViewModels.Collection;
using Marco.Pms.Model.ViewModels.Directory;
using Marco.Pms.Model.ViewModels.DocumentManager;
using Marco.Pms.Model.ViewModels.Employee;
@ -251,7 +248,6 @@ namespace Marco.Pms.Services.MappingProfiles
dest => dest.Id,
opt => opt.MapFrom(src => Guid.Parse(src.Id)));
CreateMap<Expenses, ExpenseDetailsVM>();
CreateMap<ExpenseDetailsMongoDB, ExpenseDetailsVM>()
.ForMember(
dest => dest.Id,
@ -259,19 +255,6 @@ namespace Marco.Pms.Services.MappingProfiles
#endregion
#region ======================================================= Collection =======================================================
CreateMap<InvoiceDto, Invoice>();
CreateMap<Invoice, InvoiceListVM>();
CreateMap<Invoice, InvoiceDetailsVM>();
CreateMap<ReceivedInvoicePaymentDto, ReceivedInvoicePayment>();
CreateMap<ReceivedInvoicePayment, ReceivedInvoicePaymentVM>();
CreateMap<InvoiceComment, InvoiceCommentVM>();
CreateMap<InvoiceAttachment, InvoiceAttachmentVM>();
#endregion
#region ======================================================= Master =======================================================
CreateMap<FeaturePermission, FeaturePermissionVM>();
@ -401,27 +384,19 @@ namespace Marco.Pms.Services.MappingProfiles
CreateMap<UpdateContactCategoryDto, ContactCategoryMaster>();
CreateMap<ContactCategoryMaster, ContactCategoryVM>();
#endregion
#region ======================================================= Contact Tag Master =======================================================
CreateMap<CreateContactTagDto, ContactTagMaster>();
CreateMap<UpdateContactTagDto, ContactTagMaster>();
CreateMap<ContactTagMaster, ContactTagVM>();
#endregion
#region ======================================================= Payment Adjustment Head Master =======================================================
CreateMap<PaymentAdjustmentHeadDto, PaymentAdjustmentHead>();
CreateMap<PaymentAdjustmentHead, PaymentAdjustmentHeadVM>();
#endregion
#region ======================================================= Expenses Status Master =======================================================
#endregion
#region ======================================================= Expenses Status Master =======================================================
#endregion
#region ======================================================= Expenses Status Master =======================================================
#endregion
#region ======================================================= Expenses Status Master =======================================================
#endregion
#region ======================================================= Expenses Status Master =======================================================
#endregion

View File

@ -1533,11 +1533,9 @@ namespace Marco.Pms.Services.Service
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 hasContactAccess = await _context.EmployeeBucketMappings.AnyAsync(eb => bucketIds.Contains(eb.BucketId) && eb.EmployeeId == loggedInEmployee.Id);
if (!hasAdminPermission && !hasContactAccess)
if (hasContactAccess)
{
_logger.LogWarning("Employee {EmployeeId} does not have permission to delete contact {ContactId}",
loggedInEmployee.Id, id);
@ -2133,7 +2131,7 @@ namespace Marco.Pms.Services.Service
var bucketIds = await _context.ContactBucketMappings.Where(cb => cb.ContactId == noteDto.ContactId).Select(cb => cb.BucketId).ToListAsync();
var hasContactAccess = await _context.EmployeeBucketMappings.AnyAsync(eb => bucketIds.Contains(eb.BucketId) && eb.EmployeeId == loggedInEmployee.Id);
if (!hasAdminPermission && !hasContactAccess)
if (!hasAdminPermission && hasContactAccess)
{
_logger.LogWarning("Employee {EmployeeId} does not have permission to delete contact {ContactId}",
loggedInEmployee.Id, noteDto.ContactId);
@ -2272,10 +2270,11 @@ namespace Marco.Pms.Services.Service
return ApiResponse<object>.ErrorResponse("Note not found", "Note not found", 404);
}
var (hasAdminPermission, hasManagerPermission, hasUserPermission) = await CheckPermissionsAsync(loggedInEmployee.Id);
var bucketIds = await _context.ContactBucketMappings.Where(cb => cb.ContactId == note.ContactId).Select(cb => cb.BucketId).ToListAsync();
var hasContactAccess = await _context.EmployeeBucketMappings.AnyAsync(eb => bucketIds.Contains(eb.BucketId) && eb.EmployeeId == loggedInEmployee.Id);
var bucketIds = await _context.ContactBucketMappings.AsNoTracking().Where(cb => cb.ContactId == note.ContactId).Select(cb => cb.BucketId).ToListAsync();
var hasContactAccess = await _context.EmployeeBucketMappings.AsNoTracking().AnyAsync(eb => bucketIds.Contains(eb.BucketId) && eb.EmployeeId == loggedInEmployee.Id);
if (!hasAdminPermission && !hasContactAccess)
{
_logger.LogWarning("Employee {EmployeeId} does not have permission to delete contact {ContactId}",

View File

@ -130,16 +130,6 @@ namespace Marco.Pms.Services.Service
// 3. --- Build Base Query and Apply Permissions ---
// Start with a base IQueryable. Filters will be chained onto this.
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.
if (cacheList == null)
@ -187,10 +177,6 @@ namespace Marco.Pms.Services.Service
{
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.
if (expenseFilter.CreatedByIds?.Any() == true && hasViewAllPermissionTask.Result)
@ -228,8 +214,7 @@ namespace Marco.Pms.Services.Service
return ApiResponse<object>.SuccessResponse(new List<ExpenseList>(), "No expenses found for the given criteria.", 200);
}
//expenseVM = await GetAllExpnesRelatedTables(expensesList, tenantId);
expenseVM = _mapper.Map<List<ExpenseList>>(expensesList);
expenseVM = await GetAllExpnesRelatedTables(expensesList, tenantId);
totalPages = (int)Math.Ceiling((double)totalEntites / pageSize);
}
@ -292,18 +277,7 @@ namespace Marco.Pms.Services.Service
ExpenseDetailsMongoDB? expenseDetails = null;
if (expenseDetails == null)
{
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);
var expense = await _context.Expenses.AsNoTracking().FirstOrDefaultAsync(e => e.Id == id && e.TenantId == tenantId);
if (expense == null)
{
@ -395,25 +369,66 @@ namespace Marco.Pms.Services.Service
{
try
{
var expenses = await _context.Expenses
.Include(e => e.PaidBy)
.Include(e => e.Project)
.Include(e => e.CreatedBy)
.Include(e => e.Status)
.Include(e => e.ExpensesType)
.Where(e => e.TenantId == tenantId)
.ToListAsync();
// Task 1: Get all distinct projects associated with the tenant's expenses.
var projectsTask = Task.Run(async () =>
{
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
return await dbContext.Expenses
.Where(e => e.TenantId == tenantId && e.Project != null)
.Select(e => e.Project!)
.Distinct()
.Select(p => new { p.Id, Name = $"{p.Name}" })
.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.
var response = new
return ApiResponse<object>.SuccessResponse(new
{
Projects = expenses.Where(e => e.Project != null).Select(e => new { Id = e.Project!.Id, Name = e.Project.Name }).Distinct().ToList(),
PaidBy = expenses.Where(e => e.PaidBy != null).Select(e => new { Id = e.PaidBy!.Id, Name = $"{e.PaidBy.FirstName} {e.PaidBy.LastName}" }).Distinct().ToList(),
CreatedBy = expenses.Where(e => e.CreatedBy != null).Select(e => new { Id = e.CreatedBy!.Id, Name = $"{e.CreatedBy.FirstName} {e.CreatedBy.LastName}" }).Distinct().ToList(),
Status = expenses.Where(e => e.Status != null).Select(e => new { Id = e.Status!.Id, Name = e.Status.Name }).Distinct().ToList(),
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);
Projects = await projectsTask,
PaidBy = await paidByTask,
CreatedBy = await createdByTask,
Status = await statusTask
}, "Successfully fetched the filter list", 200);
}
catch (Exception ex)
{
@ -451,6 +466,13 @@ namespace Marco.Pms.Services.Service
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.
// Each task gets its own DbContext instance.
var projectTask = Task.Run(async () =>
@ -501,21 +523,21 @@ namespace Marco.Pms.Services.Service
// Await all prerequisite checks at once.
await Task.WhenAll(hasUploadPermissionTask, projectTask, expenseTypeTask, paymentModeTask, statusMappingTask, paidByTask, expenseUIdTask);
await Task.WhenAll(hasUploadPermissionTask, hasProjectPermissionTask, projectTask, expenseTypeTask, paymentModeTask, statusMappingTask, paidByTask, expenseUIdTask);
// 2. Aggregate and Check Results
if (!await hasUploadPermissionTask)
if (!await hasUploadPermissionTask || !await hasProjectPermissionTask)
{
_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);
}
var validationErrors = new List<string>();
var project = await projectTask;
var expenseType = await expenseTypeTask;
var paymentMode = await paymentModeTask;
var statusMapping = await statusMappingTask;
var paidBy = await paidByTask;
var project = projectTask.Result;
var expenseType = expenseTypeTask.Result;
var paymentMode = paymentModeTask.Result;
var statusMapping = statusMappingTask.Result;
var paidBy = paidByTask.Result;
var lastExpenseUId = expenseUIdTask.Result;
if (project == null) validationErrors.Add("Project not found.");
@ -532,9 +554,7 @@ namespace Marco.Pms.Services.Service
_logger.LogWarning("Expense creation failed due to validation errors: {ValidationErrors}", errorMessage);
return ApiResponse<object>.ErrorResponse("Invalid input data.", errorMessage, 400);
}
var currentexpenseUId = (lastExpenseUId + 1).ToString("D5");
// 3. Entity Creation
var expense = _mapper.Map<Expenses>(dto);
expense.ExpenseUId = $"EX-{currentexpenseUId}";
@ -1087,7 +1107,6 @@ namespace Marco.Pms.Services.Service
var m = Regex.Match(id ?? string.Empty, @"(\d+)$");
return m.Success ? int.Parse(m.Value) : int.MinValue; // put invalid IDs at the bottom
}
private static object ExceptionMapper(Exception ex)
{
return new
@ -1221,6 +1240,46 @@ namespace Marco.Pms.Services.Service
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 () =>
{
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
@ -1268,20 +1327,29 @@ namespace Marco.Pms.Services.Service
});
// Await all prerequisite checks at once.
await Task.WhenAll(statusTask, billAttachmentsTask);
await Task.WhenAll(projectTask, expenseTypeTask, paymentModeTask, statusMappingTask, paidByTask, createdByTask, reviewedByTask, approvedByTask,
processedByTask, statusTask, billAttachmentsTask);
var project = projectTask.Result;
var expenseType = expenseTypeTask.Result;
var paymentMode = paymentModeTask.Result;
var statusMapping = statusMappingTask.Result;
var paidBy = paidByTask.Result;
var createdBy = createdByTask.Result;
var reviewedBy = reviewedByTask.Result;
var approvedBy = approvedByTask.Result;
var processedBy = processedByTask.Result;
var billAttachment = billAttachmentsTask.Result;
var response = _mapper.Map<ExpenseDetailsMongoDB>(model);
response.Project = _mapper.Map<ProjectBasicMongoDB>(model.Project);
response.PaidBy = _mapper.Map<BasicEmployeeMongoDB>(model.PaidBy);
response.CreatedBy = _mapper.Map<BasicEmployeeMongoDB>(model.CreatedBy);
response.ReviewedBy = _mapper.Map<BasicEmployeeMongoDB>(model.ReviewedBy);
response.ApprovedBy = _mapper.Map<BasicEmployeeMongoDB>(model.ApprovedBy);
response.ProcessedBy = _mapper.Map<BasicEmployeeMongoDB>(model.ProcessedBy);
response.Project = _mapper.Map<ProjectBasicMongoDB>(project);
response.PaidBy = _mapper.Map<BasicEmployeeMongoDB>(paidBy);
response.CreatedBy = _mapper.Map<BasicEmployeeMongoDB>(createdBy);
response.ReviewedBy = _mapper.Map<BasicEmployeeMongoDB>(reviewedBy);
response.ApprovedBy = _mapper.Map<BasicEmployeeMongoDB>(approvedBy);
response.ProcessedBy = _mapper.Map<BasicEmployeeMongoDB>(processedBy);
if (statusMapping != null)
{
response.Status = _mapper.Map<ExpensesStatusMasterMongoDB>(statusMapping.Status);
@ -1292,8 +1360,8 @@ namespace Marco.Pms.Services.Service
var status = statusTask.Result;
response.Status = _mapper.Map<ExpensesStatusMasterMongoDB>(status);
}
response.PaymentMode = _mapper.Map<PaymentModeMatserMongoDB>(model.PaymentMode);
response.ExpensesType = _mapper.Map<ExpensesTypeMasterMongoDB>(model.ExpensesType);
response.PaymentMode = _mapper.Map<PaymentModeMatserMongoDB>(paymentMode);
response.ExpensesType = _mapper.Map<ExpensesTypeMasterMongoDB>(expenseType);
if (billAttachment != null) response.Documents = billAttachment.Documents;
return response;

View File

@ -1,5 +1,4 @@
using Marco.Pms.Model.DocumentManager;
using Marco.Pms.Model.Forum;
using Marco.Pms.Model.Forum;
using Marco.Pms.Model.Master;
namespace Marco.Pms.Services.Service
@ -336,194 +335,6 @@ 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 drivers 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)
{
return new List<object>

View File

@ -1,11 +1,9 @@
using AutoMapper;
using Marco.Pms.DataAccess.Data;
using Marco.Pms.Helpers.Utility;
using Marco.Pms.Model.Collection;
using Marco.Pms.Model.Directory;
using Marco.Pms.Model.DocumentManager;
using Marco.Pms.Model.Dtos.Activities;
using Marco.Pms.Model.Dtos.Collection;
using Marco.Pms.Model.Dtos.DocumentManager;
using Marco.Pms.Model.Dtos.Master;
using Marco.Pms.Model.Employees;
@ -14,7 +12,6 @@ using Marco.Pms.Model.Master;
using Marco.Pms.Model.MongoDBModels.Utility;
using Marco.Pms.Model.Utilities;
using Marco.Pms.Model.ViewModels.Activities;
using Marco.Pms.Model.ViewModels.Collection;
using Marco.Pms.Model.ViewModels.DocumentManager;
using Marco.Pms.Model.ViewModels.Master;
using Marco.Pms.Services.Service.ServiceInterfaces;
@ -1583,7 +1580,7 @@ namespace Marco.Pms.Services.Service
{
Id = Guid.NewGuid(),
Name = trimmedName,
Description = model.Description?.Trim() ?? "", // Normalize description
Description = model.Description.Trim(), // Normalize description
TenantId = tenantId,
};
@ -1690,7 +1687,7 @@ namespace Marco.Pms.Services.Service
// Update entity properties
contactTag.Name = trimmedName;
contactTag.Description = model.Description?.Trim() ?? ""; // Normalize description
contactTag.Description = model.Description.Trim(); // Normalize description
// Log update in directory and audit trail
_context.DirectoryUpdateLogs.Add(new DirectoryUpdateLog
@ -2655,6 +2652,8 @@ namespace Marco.Pms.Services.Service
#endregion
#region =================================================================== Document Type APIs ===================================================================
public async Task<ApiResponse<object>> GetDocumentTypeMasterListAsync(Guid? documentCategoryId, Employee loggedInEmployee, Guid tenantId)
{
try
@ -2882,248 +2881,6 @@ namespace Marco.Pms.Services.Service
#endregion
#region =================================================================== Payment Adjustment Head APIs ===================================================================
/// <summary>
/// Retrieves a list of payment adjustment heads for a specific tenant with optional active status filtering.
/// </summary>
/// <param name="isActive">Filter for active/inactive payment adjustment heads</param>
/// <param name="loggedInEmployee">The employee making the request (for auditing/authorization)</param>
/// <param name="tenantId">The tenant identifier to scope the data</param>
/// <returns>An API response containing the list of payment adjustment head view models</returns>
/// <remarks>
/// This method performs database-level filtering and uses projection to minimize data transfer.
/// Consider implementing pagination for tenants with large numbers of payment adjustment heads.
/// </remarks>
public async Task<ApiResponse<object>> GetPaymentAdjustmentHeadListAsync(bool isActive, Employee loggedInEmployee, Guid tenantId)
{
try
{
// Log the request details for auditing and troubleshooting
_logger.LogInfo("Fetching payment adjustment heads for tenant {TenantId} with IsActive={IsActive}", tenantId, isActive);
var paymentAdjustmentHeads = await _context.PaymentAdjustmentHeads
.AsNoTracking() // Improve performance by disabling change tracking for read-only operations
.Where(pah => pah.TenantId == tenantId && pah.IsActive == isActive)
.Select(pah => _mapper.Map<PaymentAdjustmentHeadVM>(pah))
.ToListAsync();
_logger.LogInfo("Successfully retrieved {Count} payment adjustment heads for tenant {TenantId}", paymentAdjustmentHeads.Count, tenantId);
return ApiResponse<object>.SuccessResponse(
paymentAdjustmentHeads.OrderBy(pah => pah.Name).ToList(),
$"Payment Adjustment Heads fetched successfully. Count: {paymentAdjustmentHeads.Count}",
200);
}
catch (Exception ex)
{
// Log the full exception with context for better troubleshooting
_logger.LogError(ex, "Error occurred while fetching payment adjustment heads for tenant {TenantId}. IsActive: {IsActive}", tenantId, isActive);
return ApiResponse<object>.ErrorResponse("An error occurred", "Unable to fetch payment adjustment heads", 500);
}
}
/// <summary>
/// Creates a new payment adjustment head for the specified tenant after permission and uniqueness checks.
/// </summary>
/// <param name="model">DTO containing payment adjustment head data</param>
/// <param name="loggedInEmployee">The employee performing the action</param>
/// <param name="tenantId">The tenant identifier</param>
/// <returns>API response with status and context</returns>
public async Task<ApiResponse<object>> CreatePaymentAdjustmentHeadAsync(PaymentAdjustmentHeadDto model, Employee loggedInEmployee, Guid tenantId)
{
try
{
// Permission validation
var hasManagePermission = await _permission.HasPermission(PermissionsMaster.ManageMasters, loggedInEmployee.Id);
if (!hasManagePermission)
{
_logger.LogWarning("Access denied for employee {EmployeeId} attempting to create payment adjustment head for tenant {TenantId}.", loggedInEmployee.Id, tenantId);
return ApiResponse<object>.ErrorResponse("Access Denied.", "You do not have permission to manage masters.", 403);
}
// Uniqueness check for payment adjustment head name
var nameExists = await _context.PaymentAdjustmentHeads
.AnyAsync(pah => pah.Name == model.Name && pah.TenantId == tenantId);
if (nameExists)
{
_logger.LogInfo("Duplicate payment adjustment head name '{Name}' detected for tenant {TenantId}.", model.Name, tenantId);
return ApiResponse<object>.ErrorResponse("A payment adjustment head with this name already exists.", "Name of payment adjustment head already exists.", 409);
}
// Create and persist new entity
var paymentAdjustmentHead = _mapper.Map<PaymentAdjustmentHead>(model);
paymentAdjustmentHead.IsActive = true;
paymentAdjustmentHead.TenantId = tenantId;
_context.PaymentAdjustmentHeads.Add(paymentAdjustmentHead);
await _context.SaveChangesAsync();
var response = _mapper.Map<PaymentAdjustmentHeadVM>(paymentAdjustmentHead);
_logger.LogInfo("Payment adjustment head '{Name}' created successfully by employee {EmployeeId} for tenant {TenantId}.", paymentAdjustmentHead.Name, loggedInEmployee.Id, tenantId);
return ApiResponse<object>.SuccessResponse(response, "Payment adjustment head created successfully.", 201);
}
catch (Exception ex)
{
// Log full context with exception for easier debugging
_logger.LogError(ex, "Exception while creating payment adjustment head. Employee: {EmployeeId}, Tenant: {TenantId}, Data: {@Model}", loggedInEmployee.Id, tenantId, model);
return ApiResponse<object>.ErrorResponse("An error occurred.", "Unable to create payment adjustment head at this moment.", 500);
}
}
/// <summary>
/// Updates an existing payment adjustment head for a specified tenant after performing
/// necessary validation, permission, and uniqueness checks.
/// </summary>
/// <param name="id">Unique identifier of the payment adjustment head to update</param>
/// <param name="model">DTO containing updated payment adjustment head data</param>
/// <param name="loggedInEmployee">The employee performing the action</param>
/// <param name="tenantId">The tenant identifier</param>
/// <returns>API response object with update result and status message</returns>
public async Task<ApiResponse<object>> UpdatePaymentAdjustmentHeadAsync(Guid id, PaymentAdjustmentHeadDto model, Employee loggedInEmployee, Guid tenantId)
{
try
{
// --- Step 1: Validate request payload correctness ---
if (!model.Id.HasValue || model.Id != id)
{
_logger.LogWarning("Invalid ID provided in request. Model ID: {ModelId}, Route ID: {RouteId}, TenantId: {TenantId}", model.Id ?? Guid.Empty, id, tenantId);
return ApiResponse<object>.ErrorResponse("Invalid request.", "Provided invalid ID.", 400);
}
// --- Step 2: Validate permissions ---
var hasManagePermission = await _permission.HasPermission(PermissionsMaster.ManageMasters, loggedInEmployee.Id);
if (!hasManagePermission)
{
_logger.LogWarning("Access denied for employee {EmployeeId} attempting to update payment adjustment head for tenant {TenantId}.", loggedInEmployee.Id, tenantId);
return ApiResponse<object>.ErrorResponse("Access Denied.", "You do not have permission to manage masters.", 403);
}
// --- Step 3: Validate uniqueness constraint for name ---
var nameExists = await _context.PaymentAdjustmentHeads
.AnyAsync(pah => pah.Name == model.Name && pah.Id != id && pah.TenantId == tenantId);
if (nameExists)
{
_logger.LogInfo("Duplicate payment adjustment head name '{Name}' detected during update for tenant {TenantId}.", model.Name, tenantId);
return ApiResponse<object>.ErrorResponse("Conflict detected.", "A payment adjustment head with this name already exists.", 409);
}
// --- Step 4: Retrieve and validate existing entity ---
var paymentAdjustmentHead = await _context.PaymentAdjustmentHeads
.FirstOrDefaultAsync(pah => pah.Id == id && pah.TenantId == tenantId);
if (paymentAdjustmentHead == null)
{
_logger.LogWarning("Payment adjustment head with ID {Id} not found for tenant {TenantId}.", id, tenantId);
return ApiResponse<object>.ErrorResponse("Not Found.", "Payment adjustment head not found.", 404);
}
// Mapping PaymentAdjustmentHead to BsonDocument
var existingEntityBson = _updateLogHelper.EntityToBsonDocument(paymentAdjustmentHead);
// --- Step 5: Map changes and update entity ---
_mapper.Map(model, paymentAdjustmentHead);
await _context.SaveChangesAsync();
await _updateLogHelper.PushToUpdateLogsAsync(new UpdateLogsObject
{
EntityId = paymentAdjustmentHead.Id.ToString(),
UpdatedById = loggedInEmployee.Id.ToString(),
OldObject = existingEntityBson,
UpdatedAt = DateTime.UtcNow
}, "PaymentAdjustmentHeadModificationLog");
_logger.LogInfo("Payment adjustment head '{Name}' updated successfully by employee {EmployeeId} for tenant {TenantId}.", paymentAdjustmentHead.Name, loggedInEmployee.Id, tenantId);
var response = _mapper.Map<PaymentAdjustmentHeadVM>(paymentAdjustmentHead);
// --- Step 6: Return structured success response ---
return ApiResponse<object>.SuccessResponse(response, "Payment adjustment head updated successfully.", 200);
}
catch (Exception ex)
{
// --- Step 7: Handle and log exceptions with full context ---
_logger.LogError(ex, "Exception while updating payment adjustment head. Employee: {EmployeeId}, Tenant: {TenantId}, Model: {@Model}", loggedInEmployee.Id, tenantId, model);
return ApiResponse<object>.ErrorResponse("An unexpected error occurred.", "Unable to update payment adjustment head at this moment.", 500);
}
}
/// <summary>
/// Activates or deactivates a payment adjustment head (soft delete/restore) for a tenant,
/// including audit logging and permission validation.
/// </summary>
/// <param name="id">Unique identifier of the payment adjustment head</param>
/// <param name="isActive">Flag indicating activation (restore) or deactivation (delete)</param>
/// <param name="loggedInEmployee">Employee requesting the operation</param>
/// <param name="tenantId">The tenant identifier</param>
/// <returns>API response object with operation status</returns>
public async Task<ApiResponse<object>> DeletePaymentAdjustmentHeadAsync(Guid id, bool isActive, Employee loggedInEmployee, Guid tenantId)
{
// Dynamically select operation word for logs and messages
var operation = isActive ? "restore" : "delete";
try
{
// Step 1: Permission check
var hasManagePermission = await _permission.HasPermission(PermissionsMaster.ManageMasters, loggedInEmployee.Id);
if (!hasManagePermission)
{
_logger.LogWarning("Access denied: Employee {EmployeeId} attempted to {Operation} payment adjustment head for tenant {TenantId}.",
loggedInEmployee.Id, operation, tenantId);
return ApiResponse<object>.ErrorResponse("Access Denied.", "You do not have permission to manage masters.", 403);
}
// Step 2: Entity existence check
var paymentAdjustmentHead = await _context.PaymentAdjustmentHeads
.FirstOrDefaultAsync(pah => pah.Id == id && pah.TenantId == tenantId);
if (paymentAdjustmentHead == null)
{
_logger.LogWarning("Payment adjustment head with ID {Id} not found for tenant {TenantId} (attempted {Operation}).",
id, tenantId, operation);
return ApiResponse<object>.ErrorResponse("Not Found.", "Payment adjustment head not found.", 404);
}
// Step 3: Save pre-update state for audit log
var existingEntityBson = _updateLogHelper.EntityToBsonDocument(paymentAdjustmentHead);
// Step 4: Update IsActive status
paymentAdjustmentHead.IsActive = isActive;
await _context.SaveChangesAsync();
// Step 5: Push update action to audit log
await _updateLogHelper.PushToUpdateLogsAsync(new UpdateLogsObject
{
EntityId = paymentAdjustmentHead.Id.ToString(),
UpdatedById = loggedInEmployee.Id.ToString(),
OldObject = existingEntityBson,
UpdatedAt = DateTime.UtcNow
}, "PaymentAdjustmentHeadModificationLog");
_logger.LogInfo(
"Payment adjustment head (ID: {Id}, Name: {Name}) successfully {Operation}d by employee {EmployeeId} for tenant {TenantId}.",
paymentAdjustmentHead.Id, paymentAdjustmentHead.Name, operation, loggedInEmployee.Id, tenantId);
return ApiResponse<object>.SuccessResponse(new { }, $"Payment adjustment head {operation}d successfully.", 200);
}
catch (Exception ex)
{
_logger.LogError(ex,
"Exception occurred while performing {Operation} on payment adjustment head (ID: {Id}) for tenant {TenantId}.",
operation, id, tenantId);
return ApiResponse<object>.ErrorResponse("An error occurred.", $"Exception occurred while trying to {operation} payment adjustment head.", 500);
}
}
#endregion
#region =================================================================== Helper Function ===================================================================
private static object ExceptionMapper(Exception ex)
{

View File

@ -67,7 +67,7 @@ namespace Marco.Pms.Services.Service
_logger.LogInfo("Basic project list requested by EmployeeId {EmployeeId}", loggedInEmployee.Id);
// Step 2: Get the list of project IDs the user has access to
List<Guid> accessibleProjectIds = await _context.Projects.Where(p => p.TenantId == tenantId).Select(p => p.Id).ToListAsync();
List<Guid> accessibleProjectIds = await GetMyProjects(tenantId, loggedInEmployee);
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);
// --- Step 1: Get a list of project IDs the user can access ---
List<Guid> projectIds = await _context.Projects.Where(p => p.TenantId == tenantId).Select(p => p.Id).ToListAsync();
List<Guid> projectIds = await GetMyProjects(tenantId, loggedInEmployee);
if (!projectIds.Any())
{
_logger.LogInfo("User has no assigned projects. Returning empty list.");
@ -201,21 +201,21 @@ namespace Marco.Pms.Services.Service
using var scope = _serviceScopeFactory.CreateScope();
var _permission = scope.ServiceProvider.GetRequiredService<PermissionServices>();
//// Step 1: Check global view project permission
//var hasViewProjectPermission = await _permission.HasPermission(PermissionsMaster.ViewProject, loggedInEmployee.Id, id);
//if (!hasViewProjectPermission)
//{
// _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);
//}
// Step 1: Check global view project permission
var hasViewProjectPermission = await _permission.HasPermission(PermissionsMaster.ViewProject, loggedInEmployee.Id, id);
if (!hasViewProjectPermission)
{
_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);
}
//// Step 2: Check permission for this specific project
//var hasProjectPermission = await _permission.HasProjectPermission(loggedInEmployee, id);
//if (!hasProjectPermission)
//{
// _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);
//}
// Step 2: Check permission for this specific project
var hasProjectPermission = await _permission.HasProjectPermission(loggedInEmployee, id);
if (!hasProjectPermission)
{
_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);
}
// Step 3: Fetch project with status
var projectDetails = await _cache.GetProjectDetails(id);
@ -368,20 +368,17 @@ namespace Marco.Pms.Services.Service
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.
// Run database queries in parallel for better performance.
var promoterTask = Task.Run(async () =>
{
await using var context = await _dbContextFactory.CreateDbContextAsync();
return await context.Organizations.FirstOrDefaultAsync(o => o.Id == promoterId);
return await context.Organizations.FirstOrDefaultAsync(o => o.Id == model.PromoterId);
});
var pmcTask = Task.Run(async () =>
{
await using var context = await _dbContextFactory.CreateDbContextAsync();
return await context.Organizations.FirstOrDefaultAsync(o => o.Id == pmcId);
return await context.Organizations.FirstOrDefaultAsync(o => o.Id == model.PMCId);
});
await Task.WhenAll(promoterTask, pmcTask);
@ -391,20 +388,18 @@ namespace Marco.Pms.Services.Service
if (promoter == null)
{
_logger.LogWarning("Promoter check failed. PromoterId={PromoterId} not found.", promoterId);
_logger.LogWarning("Promoter check failed. PromoterId={PromoterId} not found.", model.PromoterId);
return ApiResponse<object>.ErrorResponse("Promoter not found", "Promoter not found in database.", 404);
}
if (pmc == null)
{
_logger.LogWarning("PMC check failed. PMCId={PMCId} not found.", pmcId);
_logger.LogWarning("PMC check failed. PMCId={PMCId} not found.", model.PMCId);
return ApiResponse<object>.ErrorResponse("PMC not found", "PMC not found in database.", 404);
}
// Step 3: Prepare the project entity.
var loggedInUserId = loggedInEmployee.Id;
var project = _mapper.Map<Project>(model);
project.PromoterId = promoterId;
project.PMCId = pmcId;
project.TenantId = tenantId;
// Step 4: Save the new project to the database.
@ -481,7 +476,6 @@ namespace Marco.Pms.Services.Service
// --- 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.
var existingProject = await _context.Projects
.AsNoTracking()
.Where(p => p.Id == id && p.TenantId == tenantId)
.SingleOrDefaultAsync();
@ -502,20 +496,17 @@ namespace Marco.Pms.Services.Service
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.
// Run database queries in parallel for better performance.
var promoterTask = Task.Run(async () =>
{
await using var context = await _dbContextFactory.CreateDbContextAsync();
return await context.Organizations.FirstOrDefaultAsync(o => o.Id == promoterId);
return await context.Organizations.FirstOrDefaultAsync(o => o.Id == model.PromoterId);
});
var pmcTask = Task.Run(async () =>
{
await using var context = await _dbContextFactory.CreateDbContextAsync();
return await context.Organizations.FirstOrDefaultAsync(o => o.Id == pmcId);
return await context.Organizations.FirstOrDefaultAsync(o => o.Id == model.PMCId);
});
await Task.WhenAll(promoterTask, pmcTask);
@ -525,12 +516,12 @@ namespace Marco.Pms.Services.Service
if (promoter == null)
{
_logger.LogWarning("Promoter check failed. PromoterId={PromoterId} not found.", promoterId);
_logger.LogWarning("Promoter check failed. PromoterId={PromoterId} not found.", model.PromoterId);
return ApiResponse<object>.ErrorResponse("Promoter not found", "Promoter not found in database.", 404);
}
if (pmc == null)
{
_logger.LogWarning("PMC check failed. PMCId={PMCId} not found.", pmcId);
_logger.LogWarning("PMC check failed. PMCId={PMCId} not found.", model.PMCId);
return ApiResponse<object>.ErrorResponse("PMC not found", "PMC not found in database.", 404);
}
@ -547,11 +538,8 @@ namespace Marco.Pms.Services.Service
// This only modifies the properties defined in the mapping, preventing data loss.
_mapper.Map(model, existingProject);
existingProject.PromoterId = promoterId;
existingProject.PMCId = pmcId;
// Mark the entity as modified (if your mapping doesn't do it automatically).
_context.Projects.Update(existingProject);
_context.Entry(existingProject).State = EntityState.Modified;
try
{
@ -728,7 +716,7 @@ namespace Marco.Pms.Services.Service
.Include(pa => pa.Employee)
.ThenInclude(e => e!.Organization)
.Include(pa => pa.Service)
.Where(pa => pa.ProjectId == projectId && pa.TenantId == tenantId && pa.Service != null);
.Where(pa => pa.ProjectId == projectId && pa.TenantId == tenantId);
// Conditionally apply the filter for active allocations.
if (!includeInactive)
@ -750,6 +738,7 @@ namespace Marco.Pms.Services.Service
}
var allocations = await projectAllocationQuery
.Where(pa => pa.Service != null)
.Select(pa => new
{
// Fields from ProjectAllocation
@ -1024,6 +1013,12 @@ namespace Marco.Pms.Services.Service
}
}
var selectedEmployee = await _context.Employees.FirstOrDefaultAsync(e => e.Id == employeeId);
if (selectedEmployee == null)
{
_logger.LogWarning("Employee not found while assigning the projects to employee");
return ApiResponse<List<ProjectAllocationVM>>.ErrorResponse("Employee not found", "Employee not found", 404);
}
// --- Step 2: Fetch all relevant existing data in ONE database call ---
var projectIdsInDto = allocationsDto.Select(p => p.ProjectId).ToList();
@ -1039,6 +1034,11 @@ namespace Marco.Pms.Services.Service
var processedAllocations = new List<ProjectAllocation>();
var serviceProjects = await _context.ProjectOrgMappings
.Include(ps => ps.ProjectService)
.Where(ps => ps.ProjectService != null && projectIdsInDto.Contains(ps.ProjectService.ProjectId) &&
ps.OrganizationId == selectedEmployee.OrganizationId && ps.TenantId == tenantId).ToListAsync();
// --- Step 3: Process all logic IN MEMORY, tracking changes ---
foreach (var dto in allocationsDto)
{
@ -1060,11 +1060,13 @@ namespace Marco.Pms.Services.Service
{
if (existingAllocation == null)
{
var serviceProject = serviceProjects.FirstOrDefault(ps => ps.ProjectService != null && ps.ProjectService.ProjectId == dto.ProjectId);
// Create a new allocation because an active one doesn't exist.
var newAllocation = _mapper.Map<ProjectAllocation>(dto);
newAllocation.EmployeeId = employeeId;
newAllocation.TenantId = tenantId;
newAllocation.AllocationDate = DateTime.UtcNow;
newAllocation.ServiceId = dto.ServiceId ?? serviceProject?.ProjectService?.ServiceId;
newAllocation.IsActive = true;
_context.ProjectAllocations.Add(newAllocation);
processedAllocations.Add(newAllocation);
@ -2691,6 +2693,8 @@ namespace Marco.Pms.Services.Service
.AsNoTracking()
.Include(po => po.ProjectService)
.ThenInclude(ps => ps!.Service)
.Include(po => po.AssignedBy)
.Include(po => po.OrganizationType)
.Include(po => po.Organization)
.Where(po => po.ProjectService != null
&& po.ProjectService.ProjectId == projectId
@ -2707,7 +2711,7 @@ namespace Marco.Pms.Services.Service
// Filter and map the data to the desired view model
var response = projectOrgMappings
.Where(po => po.Organization != null)
.Where(po => po.Organization != null && po.OrganizationType != null)
.Select(po => new ProjectOrganizationVM
{
Id = po.Organization!.Id,
@ -2716,6 +2720,7 @@ namespace Marco.Pms.Services.Service
ContactPerson = po.Organization.ContactPerson,
SPRID = po.Organization.SPRID,
logoImage = po.Organization.logoImage,
OrganizationType = po.OrganizationType!.Name,
AssignedBy = _mapper.Map<BasicEmployeeVM>(po.AssignedBy),
Service = _mapper.Map<ServiceMasterVM>(po.ProjectService!.Service),
AssignedDate = po.AssignedDate,
@ -2745,6 +2750,7 @@ namespace Marco.Pms.Services.Service
ContactPerson = pmc.ContactPerson,
SPRID = pmc.SPRID,
logoImage = pmc.logoImage,
OrganizationType = "PMC",
AssignedBy = assignedBy,
AssignedDate = assignedDate,
CompletionDate = completionDate
@ -2760,6 +2766,7 @@ namespace Marco.Pms.Services.Service
ContactPerson = promoter.ContactPerson,
SPRID = promoter.SPRID,
logoImage = promoter.logoImage,
OrganizationType = "Promotor",
AssignedBy = assignedBy,
AssignedDate = assignedDate,
CompletionDate = completionDate
@ -2775,6 +2782,7 @@ namespace Marco.Pms.Services.Service
ContactPerson = organization.ContactPerson,
SPRID = organization.SPRID,
logoImage = organization.logoImage,
OrganizationType = "Primary",
AssignedBy = assignedBy,
AssignedDate = assignedDate,
CompletionDate = completionDate
@ -2797,6 +2805,128 @@ namespace Marco.Pms.Services.Service
return ApiResponse<object>.ErrorResponse("Internal error", "An internal exception occurred", 500);
}
}
public async Task<ApiResponse<object>> GetAssignedOrganizationsToProjectForDropdownAsync(Guid projectId, Guid tenantId, Employee loggedInEmployee)
{
_logger.LogDebug("Started fetching assigned organizations for ProjectId: {ProjectId} and TenantId: {TenantId} by user {UserId}",
projectId, tenantId, loggedInEmployee.Id);
try
{
// Create a scoped PermissionServices instance for permission checks
using var scope = _serviceScopeFactory.CreateScope();
var permissionService = scope.ServiceProvider.GetRequiredService<PermissionServices>();
// Retrieve the project by projectId and tenantId
var projectTask = Task.Run(async () =>
{
await using var context = await _dbContextFactory.CreateDbContextAsync();
return await context.Projects.AsNoTracking().Include(p => p.Promoter).Include(p => p.PMC).FirstOrDefaultAsync(p => p.Id == projectId && p.TenantId == tenantId);
});
var tenantTask = Task.Run(async () =>
{
await using var context = await _dbContextFactory.CreateDbContextAsync();
return await context.Tenants.AsNoTracking().Include(t => t.Organization).FirstOrDefaultAsync(t => t.Id == tenantId);
});
var projectServiceTask = Task.Run(async () =>
{
await using var context = await _dbContextFactory.CreateDbContextAsync();
return await context.ProjectServiceMappings
.AsNoTracking()
.Include(ps => ps!.Service)
.Where(ps => ps.ProjectId == projectId && ps.TenantId == tenantId).ToListAsync();
});
await Task.WhenAll(projectTask, tenantTask, projectServiceTask);
var project = projectTask.Result;
var tenant = tenantTask.Result;
var projectService = projectServiceTask.Result;
if (project == null || tenant == null)
{
_logger.LogWarning("Project {ProjectId} not found in database for tenant {TenantId}", projectId, tenantId);
return ApiResponse<object>.ErrorResponse("Project not found", "Project not found", 404);
}
// Check if the logged in employee has permission to access the project
var hasPermission = await permissionService.HasProjectPermission(loggedInEmployee, projectId);
if (!hasPermission)
{
_logger.LogWarning("Access denied for user {UserId} on project {ProjectId}", loggedInEmployee.Id, projectId);
return ApiResponse<object>.ErrorResponse("Access Denied", "You do not have permission to access this project.", 403);
}
// Fetch all project-organization mappings with related service and organization data
var projectOrgMappingsQuery = _context.ProjectOrgMappings
.AsNoTracking()
.Include(po => po.ProjectService)
.ThenInclude(ps => ps!.Service)
.Include(po => po.AssignedBy)
.Include(po => po.OrganizationType)
.Include(po => po.Organization)
.Where(po => po.ProjectService != null
&& po.ProjectService.ProjectId == projectId
&& po.TenantId == tenantId);
if (loggedInEmployee.OrganizationId != project.PMCId && loggedInEmployee.OrganizationId != project.PromoterId && loggedInEmployee.OrganizationId != tenant.OrganizationId)
{
projectOrgMappingsQuery = projectOrgMappingsQuery.Where(po => po.ParentOrganizationId == loggedInEmployee.OrganizationId || po.OrganizationId == loggedInEmployee.OrganizationId);
}
var projectOrgMappings = await projectOrgMappingsQuery
.Distinct()
.ToListAsync();
var organizations = projectOrgMappings.Select(po => po.Organization!).ToList();
if (loggedInEmployee.OrganizationId == project.PMCId || loggedInEmployee.OrganizationId == project.PromoterId || loggedInEmployee.OrganizationId == tenant.OrganizationId)
{
var pmc = project.PMC;
var promoter = project.Promoter;
var organization = tenant.Organization;
if (!organizations.Any(r => r.Id == project.PMCId) && pmc != null)
{
organizations.Add(pmc);
}
if (!organizations.Any(r => r.Id == project.PromoterId) && promoter != null)
{
organizations.Add(promoter);
}
if (!organizations.Any(r => r.Id == tenant.OrganizationId) && organization != null)
{
organizations.Add(organization);
}
}
organizations = organizations.DistinctBy(o => o.Id).ToList();
// Filter and map the data to the desired view model
var response = organizations
.Select(o => new ProjectOrganizationVM
{
Id = o.Id,
Name = o.Name,
SPRID = 0
})
.ToList();
_logger.LogInfo("Fetched {Count} assigned organizations for ProjectId: {ProjectId}", response.Count, projectId);
return ApiResponse<object>.SuccessResponse(response, "Successfully fetched the list of organizations assigned to the project", 200);
}
catch (DbUpdateException dbEx)
{
_logger.LogError(dbEx, "Database exception while fetching assigned organizations for ProjectId: {ProjectId}", projectId);
return ApiResponse<object>.ErrorResponse("Internal error", "A database exception occurred", 500);
}
catch (Exception ex)
{
_logger.LogError(ex, "Unhandled exception while fetching assigned organizations for ProjectId: {ProjectId}", projectId);
return ApiResponse<object>.ErrorResponse("Internal error", "An internal exception occurred", 500);
}
}
#endregion
@ -2815,9 +2945,14 @@ namespace Marco.Pms.Services.Service
public async Task<List<ProjectAllocation>> GetTeamByProject(Guid TenantId, Guid ProjectId, Guid? OrganizationId, bool IncludeInactive)
{
var projectAllocationQuery = _context.ProjectAllocations
.Include(pa => pa.Employee)
.ThenInclude(e => e!.Organization)
.Where(pa => pa.TenantId == TenantId && pa.ProjectId == ProjectId);
.Include(pa => pa.Project)
.Include(pa => pa.Employee)
.ThenInclude(e => e!.Organization)
.Include(pa => pa.Employee)
.ThenInclude(e => e!.JobRole)
.Where(pa => pa.TenantId == TenantId &&
pa.ProjectId == ProjectId && pa.Project != null &&
pa.Employee != null && pa.Employee.Organization != null && pa.Employee.JobRole != null);
if (!IncludeInactive)
{
projectAllocationQuery = projectAllocationQuery.Where(pa => pa.IsActive);

View File

@ -1,5 +1,4 @@
using Marco.Pms.Model.Dtos.Activities;
using Marco.Pms.Model.Dtos.Collection;
using Marco.Pms.Model.Dtos.DocumentManager;
using Marco.Pms.Model.Dtos.Master;
using Marco.Pms.Model.Employees;
@ -47,7 +46,6 @@ namespace Marco.Pms.Services.Service.ServiceInterfaces
Task<ApiResponse<object>> DeleteActivityGroupAsync(Guid id, bool isActive, Employee loggedInEmployee, Guid tenantId);
#endregion
#region =================================================================== Contact Category APIs ===================================================================
Task<ApiResponse<object>> CreateContactCategory(CreateContactCategoryDto contactCategoryDto, Employee loggedInEmployee, Guid tenantId);
Task<ApiResponse<object>> UpdateContactCategory(Guid id, UpdateContactCategoryDto contactCategoryDto, Employee loggedInEmployee, Guid tenantId);
@ -106,12 +104,5 @@ namespace Marco.Pms.Services.Service.ServiceInterfaces
Task<ApiResponse<object>> UpdateDocumentTypeMasterAsync(Guid id, CreateDocumentTypeDto model, Employee loggedInEmployee, Guid tenantId);
Task<ApiResponse<object>> DeleteDocumentTypeMasterAsync(Guid id, bool isActive, Employee loggedInEmployee, Guid tenantId);
#endregion
#region =================================================================== Payment Adjustment Head APIs ===================================================================
Task<ApiResponse<object>> GetPaymentAdjustmentHeadListAsync(bool isActive, Employee loggedInEmployee, Guid tenantId);
Task<ApiResponse<object>> CreatePaymentAdjustmentHeadAsync(PaymentAdjustmentHeadDto model, Employee loggedInEmployee, Guid tenantId);
Task<ApiResponse<object>> UpdatePaymentAdjustmentHeadAsync(Guid id, PaymentAdjustmentHeadDto model, Employee loggedInEmployee, Guid tenantId);
Task<ApiResponse<object>> DeletePaymentAdjustmentHeadAsync(Guid id, bool isActive, Employee loggedInEmployee, Guid tenantId);
#endregion
}
}

View File

@ -49,6 +49,7 @@ namespace Marco.Pms.Services.Service.ServiceInterfaces
Task<ApiResponse<object>> DeassignServiceToProjectAsync(DeassignServiceDto model, Guid tenantId, Employee loggedInEmployee);
Task<ApiResponse<object>> GetAssignedOrganizationsToProjectAsync(Guid projectId, Guid tenantId, Employee loggedInEmployee);
Task<ApiResponse<object>> GetAssignedOrganizationsToProjectForDropdownAsync(Guid projectId, Guid tenantId, Employee loggedInEmployee);
}
}

View File

@ -48,7 +48,7 @@
},
"MongoDB": {
"SerilogDatabaseUrl": "mongodb://localhost:27017/DotNetLogs",
"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&replicaSet=rs01&directConnection=true"
"ConnectionString": "mongodb://localhost:27017/MarcoBMS_Caches?socketTimeoutMS=500&serverSelectionTimeoutMS=500&connectTimeoutMS=500",
"ModificationConnectionString": "mongodb://devuser:DevPass123@147.93.98.152:27017/MarcoBMSLocalDev?authSource=admin&eplicaSet=rs01&directConnection=true"
}
}