Added cache to expenses get list and create expense APIs
This commit is contained in:
parent
d536b9c99c
commit
73cf85a1cc
@ -27,6 +27,7 @@ namespace Marco.Pms.Helpers.CacheHelper
|
|||||||
|
|
||||||
var update = Builders<EmployeePermissionMongoDB>.Update
|
var update = Builders<EmployeePermissionMongoDB>.Update
|
||||||
.AddToSetEach(e => e.ApplicationRoleIds, newRoleIds)
|
.AddToSetEach(e => e.ApplicationRoleIds, newRoleIds)
|
||||||
|
.Set(r => r.ExpireAt, DateTime.UtcNow.Date.AddDays(1))
|
||||||
.AddToSetEach(e => e.PermissionIds, newPermissionIds);
|
.AddToSetEach(e => e.PermissionIds, newPermissionIds);
|
||||||
|
|
||||||
var options = new UpdateOptions { IsUpsert = true };
|
var options = new UpdateOptions { IsUpsert = true };
|
||||||
@ -46,6 +47,7 @@ namespace Marco.Pms.Helpers.CacheHelper
|
|||||||
var filter = Builders<EmployeePermissionMongoDB>.Filter.Eq(e => e.Id, employeeId.ToString());
|
var filter = Builders<EmployeePermissionMongoDB>.Filter.Eq(e => e.Id, employeeId.ToString());
|
||||||
|
|
||||||
var update = Builders<EmployeePermissionMongoDB>.Update
|
var update = Builders<EmployeePermissionMongoDB>.Update
|
||||||
|
.Set(r => r.ExpireAt, DateTime.UtcNow.Date.AddDays(1))
|
||||||
.AddToSetEach(e => e.ProjectIds, newprojectIds);
|
.AddToSetEach(e => e.ProjectIds, newprojectIds);
|
||||||
|
|
||||||
var result = await _collection.UpdateOneAsync(filter, update, new UpdateOptions { IsUpsert = true });
|
var result = await _collection.UpdateOneAsync(filter, update, new UpdateOptions { IsUpsert = true });
|
||||||
@ -187,17 +189,12 @@ namespace Marco.Pms.Helpers.CacheHelper
|
|||||||
// A private method to handle the one-time setup of the collection's indexes.
|
// A private method to handle the one-time setup of the collection's indexes.
|
||||||
private async Task InitializeCollectionAsync()
|
private async Task InitializeCollectionAsync()
|
||||||
{
|
{
|
||||||
// 1. Define the TTL (Time-To-Live) index on the 'ExpireAt' field.
|
|
||||||
var indexKeys = Builders<EmployeePermissionMongoDB>.IndexKeys.Ascending(x => x.ExpireAt);
|
var indexKeys = Builders<EmployeePermissionMongoDB>.IndexKeys.Ascending(x => x.ExpireAt);
|
||||||
var indexOptions = new CreateIndexOptions
|
var indexOptions = new CreateIndexOptions
|
||||||
{
|
{
|
||||||
// This tells MongoDB to automatically delete documents when their 'ExpireAt' time is reached.
|
ExpireAfter = TimeSpan.Zero // required for fixed expiration time
|
||||||
ExpireAfter = TimeSpan.FromSeconds(0)
|
|
||||||
};
|
};
|
||||||
var indexModel = new CreateIndexModel<EmployeePermissionMongoDB>(indexKeys, indexOptions);
|
var indexModel = new CreateIndexModel<EmployeePermissionMongoDB>(indexKeys, indexOptions);
|
||||||
|
|
||||||
// 2. Create the index. This is an idempotent operation if the index already exists.
|
|
||||||
// Use CreateOneAsync since we are only creating a single index.
|
|
||||||
await _collection.Indexes.CreateOneAsync(indexModel);
|
await _collection.Indexes.CreateOneAsync(indexModel);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
using Marco.Pms.Model.MongoDBModels.Employees;
|
using Marco.Pms.Model.MongoDBModels.Expenses;
|
||||||
|
using Marco.Pms.Model.Utilities;
|
||||||
using Microsoft.Extensions.Configuration;
|
using Microsoft.Extensions.Configuration;
|
||||||
using MongoDB.Driver;
|
using MongoDB.Driver;
|
||||||
|
|
||||||
@ -6,7 +7,7 @@ namespace Marco.Pms.Helpers.CacheHelper
|
|||||||
{
|
{
|
||||||
public class ExpenseCache
|
public class ExpenseCache
|
||||||
{
|
{
|
||||||
private readonly IMongoCollection<EmployeePermissionMongoDB> _collection;
|
private readonly IMongoCollection<ExpenseDetailsMongoDB> _collection;
|
||||||
public ExpenseCache(IConfiguration configuration)
|
public ExpenseCache(IConfiguration configuration)
|
||||||
{
|
{
|
||||||
|
|
||||||
@ -14,11 +15,91 @@ namespace Marco.Pms.Helpers.CacheHelper
|
|||||||
var mongoUrl = new MongoUrl(connectionString);
|
var mongoUrl = new MongoUrl(connectionString);
|
||||||
var client = new MongoClient(mongoUrl); // Your MongoDB connection string
|
var client = new MongoClient(mongoUrl); // Your MongoDB connection string
|
||||||
var mongoDB = client.GetDatabase(mongoUrl.DatabaseName); // Your MongoDB Database name
|
var mongoDB = client.GetDatabase(mongoUrl.DatabaseName); // Your MongoDB Database name
|
||||||
_collection = mongoDB.GetCollection<EmployeePermissionMongoDB>("Expenses");
|
_collection = mongoDB.GetCollection<ExpenseDetailsMongoDB>("Expenses");
|
||||||
}
|
}
|
||||||
public async Task AddExpenseToCacheAsync()
|
public async Task AddExpenseToCacheAsync(ExpenseDetailsMongoDB expense)
|
||||||
{
|
{
|
||||||
|
await _collection.InsertOneAsync(expense);
|
||||||
|
|
||||||
|
await InitializeCollectionAsync();
|
||||||
|
}
|
||||||
|
public async Task AddExpensesListToCacheAsync(List<ExpenseDetailsMongoDB> expenses)
|
||||||
|
{
|
||||||
|
// 1. Add a guard clause to avoid an unnecessary database call for an empty list.
|
||||||
|
if (expenses == null || !expenses.Any())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Perform the insert operation. This is the only responsibility of this method.
|
||||||
|
await _collection.InsertManyAsync(expenses);
|
||||||
|
await InitializeCollectionAsync();
|
||||||
|
}
|
||||||
|
public async Task<(int totalPages, long totalCount, List<ExpenseDetailsMongoDB> expenseList)> GetExpenseListFromCacheAsync(Guid tenantId, Guid loggedInEmployeeId, bool viewAll,
|
||||||
|
bool viewSelf, int pageNumber, int pageSize, ExpensesFilter? expenseFilter)
|
||||||
|
{
|
||||||
|
var filterBuilder = Builders<ExpenseDetailsMongoDB>.Filter;
|
||||||
|
var filter = filterBuilder.Empty;
|
||||||
|
|
||||||
|
// Permission-based filter
|
||||||
|
if (!viewAll && viewSelf)
|
||||||
|
{
|
||||||
|
filter &= filterBuilder.Eq(e => e.CreatedById, loggedInEmployeeId.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply filters
|
||||||
|
if (expenseFilter != null)
|
||||||
|
{
|
||||||
|
if (expenseFilter.StartDate.HasValue && expenseFilter.EndDate.HasValue)
|
||||||
|
{
|
||||||
|
filter &= filterBuilder.Gte(e => e.CreatedAt, expenseFilter.StartDate.Value.Date)
|
||||||
|
& filterBuilder.Lte(e => e.CreatedAt, expenseFilter.EndDate.Value.Date.AddDays(1).AddTicks(-1));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (expenseFilter.ProjectIds?.Any() == true)
|
||||||
|
{
|
||||||
|
filter &= filterBuilder.In(e => e.ProjectId, expenseFilter.ProjectIds.Select(p => p.ToString()).ToList());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (expenseFilter.StatusIds?.Any() == true)
|
||||||
|
{
|
||||||
|
filter &= filterBuilder.In(e => e.StatusId, expenseFilter.StatusIds.Select(p => p.ToString()).ToList());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (expenseFilter.PaidById?.Any() == true)
|
||||||
|
{
|
||||||
|
filter &= filterBuilder.In(e => e.PaidById, expenseFilter.PaidById.Select(p => p.ToString()).ToList());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (expenseFilter.CreatedByIds?.Any() == true && viewAll)
|
||||||
|
{
|
||||||
|
filter &= filterBuilder.In(e => e.CreatedById, expenseFilter.CreatedByIds.Select(p => p.ToString()).ToList());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Total count
|
||||||
|
var totalCount = await _collection.CountDocumentsAsync(filter);
|
||||||
|
var totalPages = (int)Math.Ceiling((double)totalCount / pageSize);
|
||||||
|
|
||||||
|
// Fetch paginated data
|
||||||
|
var expenses = await _collection
|
||||||
|
.Find(filter)
|
||||||
|
.Skip((pageNumber - 1) * pageSize)
|
||||||
|
.Limit(pageSize)
|
||||||
|
.SortByDescending(e => e.CreatedAt)
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
return (totalPages, totalCount, expenses);
|
||||||
|
}
|
||||||
|
private async Task InitializeCollectionAsync()
|
||||||
|
{
|
||||||
|
var indexKeys = Builders<ExpenseDetailsMongoDB>.IndexKeys.Ascending(x => x.ExpireAt);
|
||||||
|
var indexOptions = new CreateIndexOptions
|
||||||
|
{
|
||||||
|
ExpireAfter = TimeSpan.Zero // required for fixed expiration time
|
||||||
|
};
|
||||||
|
var indexModel = new CreateIndexModel<ExpenseDetailsMongoDB>(indexKeys, indexOptions);
|
||||||
|
await _collection.Indexes.CreateOneAsync(indexModel);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -30,13 +30,7 @@ namespace Marco.Pms.Helpers
|
|||||||
{
|
{
|
||||||
await _projectCollection.InsertOneAsync(projectDetails);
|
await _projectCollection.InsertOneAsync(projectDetails);
|
||||||
|
|
||||||
var indexKeys = Builders<ProjectMongoDB>.IndexKeys.Ascending(x => x.ExpireAt);
|
await InitializeCollectionAsync();
|
||||||
var indexOptions = new CreateIndexOptions
|
|
||||||
{
|
|
||||||
ExpireAfter = TimeSpan.Zero // required for fixed expiration time
|
|
||||||
};
|
|
||||||
var indexModel = new CreateIndexModel<ProjectMongoDB>(indexKeys, indexOptions);
|
|
||||||
await _projectCollection.Indexes.CreateOneAsync(indexModel);
|
|
||||||
|
|
||||||
}
|
}
|
||||||
public async Task AddProjectDetailsListToCache(List<ProjectMongoDB> projectDetailsList)
|
public async Task AddProjectDetailsListToCache(List<ProjectMongoDB> projectDetailsList)
|
||||||
@ -53,17 +47,12 @@ namespace Marco.Pms.Helpers
|
|||||||
}
|
}
|
||||||
private async Task InitializeCollectionAsync()
|
private async Task InitializeCollectionAsync()
|
||||||
{
|
{
|
||||||
// 1. Define the TTL (Time-To-Live) index on the 'ExpireAt' field.
|
|
||||||
var indexKeys = Builders<ProjectMongoDB>.IndexKeys.Ascending(x => x.ExpireAt);
|
var indexKeys = Builders<ProjectMongoDB>.IndexKeys.Ascending(x => x.ExpireAt);
|
||||||
var indexOptions = new CreateIndexOptions
|
var indexOptions = new CreateIndexOptions
|
||||||
{
|
{
|
||||||
// This tells MongoDB to automatically delete documents when their 'ExpireAt' time is reached.
|
ExpireAfter = TimeSpan.Zero // required for fixed expiration time
|
||||||
ExpireAfter = TimeSpan.FromSeconds(0)
|
|
||||||
};
|
};
|
||||||
var indexModel = new CreateIndexModel<ProjectMongoDB>(indexKeys, indexOptions);
|
var indexModel = new CreateIndexModel<ProjectMongoDB>(indexKeys, indexOptions);
|
||||||
|
|
||||||
// 2. Create the index. This is an idempotent operation if the index already exists.
|
|
||||||
// Use CreateOneAsync since we are only creating a single index.
|
|
||||||
await _projectCollection.Indexes.CreateOneAsync(indexModel);
|
await _projectCollection.Indexes.CreateOneAsync(indexModel);
|
||||||
}
|
}
|
||||||
public async Task<bool> UpdateProjectDetailsOnlyToCache(Project project, StatusMaster projectStatus)
|
public async Task<bool> UpdateProjectDetailsOnlyToCache(Project project, StatusMaster projectStatus)
|
||||||
|
@ -5,7 +5,6 @@
|
|||||||
public Guid EmpID { get; set; }
|
public Guid EmpID { get; set; }
|
||||||
public Guid JobRoleId { get; set; }
|
public Guid JobRoleId { get; set; }
|
||||||
public Guid ProjectId { get; set; }
|
public Guid ProjectId { get; set; }
|
||||||
|
|
||||||
public bool Status { get; set; }
|
public bool Status { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -14,7 +13,6 @@
|
|||||||
{
|
{
|
||||||
public Guid ProjectId { get; set; }
|
public Guid ProjectId { get; set; }
|
||||||
public Guid JobRoleId { get; set; }
|
public Guid JobRoleId { get; set; }
|
||||||
|
|
||||||
public bool Status { get; set; }
|
public bool Status { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,13 @@
|
|||||||
|
namespace Marco.Pms.Model.MongoDBModels.Employees
|
||||||
|
{
|
||||||
|
public class BasicEmployeeMongoDB
|
||||||
|
{
|
||||||
|
public string Id { get; set; } = string.Empty;
|
||||||
|
public string? FirstName { get; set; }
|
||||||
|
public string? LastName { get; set; }
|
||||||
|
public byte[]? Photo { get; set; }
|
||||||
|
public string? JobRoleId { get; set; }
|
||||||
|
public string? JobRoleName { get; set; }
|
||||||
|
public string TenantId { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
|
}
|
@ -2,5 +2,27 @@
|
|||||||
{
|
{
|
||||||
public class ExpenseDetailsMongoDB
|
public class ExpenseDetailsMongoDB
|
||||||
{
|
{
|
||||||
|
public string Id { get; set; } = string.Empty;
|
||||||
|
public string ProjectId { get; set; } = string.Empty;
|
||||||
|
public string ExpensesTypeId { get; set; } = string.Empty;
|
||||||
|
public string PaymentModeId { get; set; } = string.Empty;
|
||||||
|
public string PaidById { get; set; } = string.Empty;
|
||||||
|
public string CreatedById { get; set; } = string.Empty;
|
||||||
|
public DateTime TransactionDate { get; set; }
|
||||||
|
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 StatusId { get; set; } = string.Empty;
|
||||||
|
public bool PreApproved { get; set; } = false;
|
||||||
|
public string? TransactionId { get; set; }
|
||||||
|
public string Description { get; set; } = string.Empty;
|
||||||
|
public string? Location { get; set; }
|
||||||
|
public List<string> S3Key { get; set; } = new List<string>();
|
||||||
|
public List<string>? ThumbS3Key { get; set; }
|
||||||
|
public string? GSTNumber { get; set; }
|
||||||
|
public int? NoOfPersons { get; set; }
|
||||||
|
public bool IsActive { get; set; } = true;
|
||||||
|
public string TenantId { get; set; } = string.Empty;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,13 @@
|
|||||||
|
namespace Marco.Pms.Model.MongoDBModels.Masters
|
||||||
|
{
|
||||||
|
public class ExpensesStatusMasterMongoDB
|
||||||
|
{
|
||||||
|
public string Id { get; set; } = string.Empty;
|
||||||
|
public string Name { get; set; } = string.Empty;
|
||||||
|
public string DisplayName { get; set; } = string.Empty;
|
||||||
|
public string Description { get; set; } = string.Empty;
|
||||||
|
public string? Color { get; set; }
|
||||||
|
public bool IsSystem { get; set; } = false;
|
||||||
|
public string TenantId { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,11 @@
|
|||||||
|
namespace Marco.Pms.Model.MongoDBModels.Masters
|
||||||
|
{
|
||||||
|
public class ExpensesTypeMasterMongoDB
|
||||||
|
{
|
||||||
|
public string Id { get; set; } = string.Empty;
|
||||||
|
public string Name { get; set; } = string.Empty;
|
||||||
|
public bool NoOfPersonsRequired { get; set; }
|
||||||
|
public string Description { get; set; } = string.Empty;
|
||||||
|
public string TenantId { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,10 @@
|
|||||||
|
namespace Marco.Pms.Model.MongoDBModels.Masters
|
||||||
|
{
|
||||||
|
public class PaymentModeMatserMongoDB
|
||||||
|
{
|
||||||
|
public string Id { get; set; } = string.Empty;
|
||||||
|
public string Name { get; set; } = string.Empty;
|
||||||
|
public string Description { get; set; } = string.Empty;
|
||||||
|
public string TenantId { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
|
}
|
10
Marco.Pms.Model/MongoDBModels/Project/ProjectBasicMongoDB.cs
Normal file
10
Marco.Pms.Model/MongoDBModels/Project/ProjectBasicMongoDB.cs
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
namespace Marco.Pms.Model.MongoDBModels.Project
|
||||||
|
{
|
||||||
|
public class ProjectBasicMongoDB
|
||||||
|
{
|
||||||
|
public string Id { get; set; } = string.Empty;
|
||||||
|
public string Name { get; set; } = string.Empty;
|
||||||
|
public string ShortName { get; set; } = string.Empty;
|
||||||
|
public string TenantId { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
|
}
|
@ -1,40 +1,58 @@
|
|||||||
using Marco.Pms.Helpers;
|
using AutoMapper;
|
||||||
using Marco.Pms.Helpers.CacheHelper;
|
|
||||||
using Marco.Pms.DataAccess.Data;
|
using Marco.Pms.DataAccess.Data;
|
||||||
|
using Marco.Pms.Helpers;
|
||||||
|
using Marco.Pms.Helpers.CacheHelper;
|
||||||
|
using Marco.Pms.Model.Expenses;
|
||||||
using Marco.Pms.Model.Master;
|
using Marco.Pms.Model.Master;
|
||||||
|
using Marco.Pms.Model.MongoDBModels.Expenses;
|
||||||
using Marco.Pms.Model.MongoDBModels.Masters;
|
using Marco.Pms.Model.MongoDBModels.Masters;
|
||||||
using Marco.Pms.Model.MongoDBModels.Project;
|
using Marco.Pms.Model.MongoDBModels.Project;
|
||||||
using Marco.Pms.Model.MongoDBModels.Utility;
|
using Marco.Pms.Model.MongoDBModels.Utility;
|
||||||
using Marco.Pms.Model.Projects;
|
using Marco.Pms.Model.Projects;
|
||||||
|
using Marco.Pms.Model.Utilities;
|
||||||
using MarcoBMS.Services.Service;
|
using MarcoBMS.Services.Service;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using MongoDB.Driver;
|
||||||
using Project = Marco.Pms.Model.Projects.Project;
|
using Project = Marco.Pms.Model.Projects.Project;
|
||||||
|
|
||||||
namespace Marco.Pms.Services.Helpers
|
namespace Marco.Pms.Services.Helpers
|
||||||
{
|
{
|
||||||
public class CacheUpdateHelper
|
public class CacheUpdateHelper
|
||||||
{
|
{
|
||||||
|
private readonly IDbContextFactory<ApplicationDbContext> _dbContextFactory;
|
||||||
|
private readonly ApplicationDbContext _context;
|
||||||
|
private readonly IMapper _mapper;
|
||||||
private readonly ProjectCache _projectCache;
|
private readonly ProjectCache _projectCache;
|
||||||
private readonly EmployeeCache _employeeCache;
|
private readonly EmployeeCache _employeeCache;
|
||||||
private readonly ReportCache _reportCache;
|
private readonly ReportCache _reportCache;
|
||||||
|
private readonly ExpenseCache _expenseCache;
|
||||||
private readonly ILoggingService _logger;
|
private readonly ILoggingService _logger;
|
||||||
private readonly IDbContextFactory<ApplicationDbContext> _dbContextFactory;
|
|
||||||
private readonly ApplicationDbContext _context;
|
|
||||||
private readonly GeneralHelper _generalHelper;
|
private readonly GeneralHelper _generalHelper;
|
||||||
|
private static readonly Guid Draft = Guid.Parse("297e0d8f-f668-41b5-bfea-e03b354251c8");
|
||||||
|
private static readonly Guid Rejected = Guid.Parse("d1ee5eec-24b6-4364-8673-a8f859c60729");
|
||||||
|
|
||||||
public CacheUpdateHelper(ProjectCache projectCache, EmployeeCache employeeCache, ReportCache reportCache, ILoggingService logger,
|
public CacheUpdateHelper(
|
||||||
IDbContextFactory<ApplicationDbContext> dbContextFactory, ApplicationDbContext context, GeneralHelper generalHelper)
|
IMapper mapper,
|
||||||
|
ProjectCache projectCache,
|
||||||
|
EmployeeCache employeeCache,
|
||||||
|
ReportCache reportCache,
|
||||||
|
ExpenseCache expenseCache,
|
||||||
|
ILoggingService logger,
|
||||||
|
IDbContextFactory<ApplicationDbContext> dbContextFactory,
|
||||||
|
ApplicationDbContext context,
|
||||||
|
GeneralHelper generalHelper)
|
||||||
{
|
{
|
||||||
|
_mapper = mapper;
|
||||||
_projectCache = projectCache;
|
_projectCache = projectCache;
|
||||||
_employeeCache = employeeCache;
|
_employeeCache = employeeCache;
|
||||||
_reportCache = reportCache;
|
_reportCache = reportCache;
|
||||||
|
_expenseCache = expenseCache;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_dbContextFactory = dbContextFactory;
|
_dbContextFactory = dbContextFactory;
|
||||||
_context = context;
|
_context = context;
|
||||||
_generalHelper = generalHelper;
|
_generalHelper = generalHelper;
|
||||||
}
|
}
|
||||||
|
#region ======================================================= Project Details Cache =======================================================
|
||||||
// ------------------------------------ Project Details Cache ---------------------------------------
|
|
||||||
|
|
||||||
public async Task AddProjectDetails(Project project)
|
public async Task AddProjectDetails(Project project)
|
||||||
{
|
{
|
||||||
@ -507,7 +525,9 @@ namespace Marco.Pms.Services.Helpers
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ------------------------------------ Project Infrastructure Cache ---------------------------------------
|
#endregion
|
||||||
|
|
||||||
|
#region ======================================================= Project Infrastructure Cache =======================================================
|
||||||
|
|
||||||
public async Task AddBuildngInfra(Guid projectId, Building? building = null, Floor? floor = null, WorkArea? workArea = null, Guid? buildingId = null)
|
public async Task AddBuildngInfra(Guid projectId, Building? building = null, Floor? floor = null, WorkArea? workArea = null, Guid? buildingId = null)
|
||||||
{
|
{
|
||||||
@ -573,7 +593,9 @@ namespace Marco.Pms.Services.Helpers
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ------------------------------------------------------- WorkItem -------------------------------------------------------
|
#endregion
|
||||||
|
|
||||||
|
#region ======================================================= WorkItem Cache =======================================================
|
||||||
|
|
||||||
public async Task<List<WorkItemMongoDB>?> GetWorkItemsByWorkAreaIds(List<Guid> workAreaIds)
|
public async Task<List<WorkItemMongoDB>?> GetWorkItemsByWorkAreaIds(List<Guid> workAreaIds)
|
||||||
{
|
{
|
||||||
@ -680,8 +702,10 @@ namespace Marco.Pms.Services.Helpers
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region ======================================================= Employee Profile Cache =======================================================
|
||||||
|
|
||||||
// ------------------------------------ Employee Profile Cache ---------------------------------------
|
|
||||||
public async Task AddApplicationRole(Guid employeeId, List<Guid> roleIds)
|
public async Task AddApplicationRole(Guid employeeId, List<Guid> roleIds)
|
||||||
{
|
{
|
||||||
// 1. Guard Clause: Avoid unnecessary database work if there are no roles to add.
|
// 1. Guard Clause: Avoid unnecessary database work if there are no roles to add.
|
||||||
@ -839,8 +863,146 @@ namespace Marco.Pms.Services.Helpers
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
// ------------------------------------ Report Cache ---------------------------------------
|
#region ======================================================= Expenses Cache =======================================================
|
||||||
|
public async Task AddExpenseByObjectAsync(Expenses expense)
|
||||||
|
{
|
||||||
|
var expenseCache = _mapper.Map<ExpenseDetailsMongoDB>(expense);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var billAttachments = await _context.BillAttachments
|
||||||
|
.Include(ba => ba.Document)
|
||||||
|
.AsNoTracking()
|
||||||
|
.Where(ba => ba.ExpensesId == expense.Id && ba.Document != null)
|
||||||
|
.GroupBy(ba => ba.ExpensesId)
|
||||||
|
.Select(g => new
|
||||||
|
{
|
||||||
|
S3Keys = g.Select(ba => ba.Document!.S3Key).ToList(),
|
||||||
|
ThumbS3Keys = g.Select(ba => ba.Document!.ThumbS3Key ?? ba.Document.S3Key).ToList()
|
||||||
|
})
|
||||||
|
.FirstOrDefaultAsync(); ;
|
||||||
|
if (billAttachments != null)
|
||||||
|
{
|
||||||
|
expenseCache.S3Key = billAttachments.S3Keys;
|
||||||
|
expenseCache.ThumbS3Key = billAttachments.ThumbS3Keys;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error occurd while fetched expense related tables to save in cahce");
|
||||||
|
}
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _expenseCache.AddExpenseToCacheAsync(expenseCache);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error occurd while storing expense related table in cahce");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
public async Task AddExpenseByIdAsync(Guid Id, Guid tenantId)
|
||||||
|
{
|
||||||
|
var expense = await _context.Expenses.AsNoTracking().FirstOrDefaultAsync(e => e.Id == Id && e.TenantId == tenantId);
|
||||||
|
var expenseCache = _mapper.Map<ExpenseDetailsMongoDB>(expense);
|
||||||
|
if (expense != null)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var billAttachments = await _context.BillAttachments
|
||||||
|
.Include(ba => ba.Document)
|
||||||
|
.AsNoTracking()
|
||||||
|
.Where(ba => ba.ExpensesId == expense.Id && ba.Document != null)
|
||||||
|
.GroupBy(ba => ba.ExpensesId)
|
||||||
|
.Select(g => new
|
||||||
|
{
|
||||||
|
S3Keys = g.Select(ba => ba.Document!.S3Key).ToList(),
|
||||||
|
ThumbS3Keys = g.Select(ba => ba.Document!.ThumbS3Key ?? ba.Document.S3Key).ToList()
|
||||||
|
})
|
||||||
|
.FirstOrDefaultAsync();
|
||||||
|
if (billAttachments != null)
|
||||||
|
{
|
||||||
|
expenseCache.S3Key = billAttachments.S3Keys;
|
||||||
|
expenseCache.ThumbS3Key = billAttachments.ThumbS3Keys;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error occurd while fetched expense related tables to save in cahce");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _expenseCache.AddExpenseToCacheAsync(expenseCache);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error occurd while storing expense related table in cahce");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
public async Task AddExpensesListToCache(List<Expenses> expenses)
|
||||||
|
{
|
||||||
|
var expensesCache = _mapper.Map<List<ExpenseDetailsMongoDB>>(expenses);
|
||||||
|
var expenseIds = expenses.Select(e => e.Id).ToList();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var billAttachments = await _context.BillAttachments
|
||||||
|
.Include(ba => ba.Document)
|
||||||
|
.AsNoTracking()
|
||||||
|
.Where(ba => expenseIds.Contains(ba.ExpensesId) && ba.Document != null)
|
||||||
|
.GroupBy(ba => ba.ExpensesId)
|
||||||
|
.Select(g => new
|
||||||
|
{
|
||||||
|
ExpensesId = g.Key,
|
||||||
|
S3Keys = g.Select(ba => ba.Document!.S3Key).ToList(),
|
||||||
|
ThumbS3Keys = g.Select(ba => ba.Document!.ThumbS3Key ?? ba.Document.S3Key).ToList()
|
||||||
|
})
|
||||||
|
.ToListAsync();
|
||||||
|
foreach (var expenseCache in expensesCache)
|
||||||
|
{
|
||||||
|
expenseCache.S3Key = billAttachments.Where(ba => ba.ExpensesId == Guid.Parse(expenseCache.Id)).Select(ba => ba.S3Keys).FirstOrDefault() ?? new List<string>();
|
||||||
|
expenseCache.ThumbS3Key = billAttachments.Where(ba => ba.ExpensesId == Guid.Parse(expenseCache.Id)).Select(ba => ba.ThumbS3Keys).FirstOrDefault();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error occurd while fetched expense related tables to save in cahce");
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _expenseCache.AddExpensesListToCacheAsync(expensesCache);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error occured while saving the list of expenses to Cache");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<(int totalPages, long totalCount, List<ExpenseDetailsMongoDB>? expenseList)> GetExpenseListAsync(Guid tenantId, Guid loggedInEmployeeId, bool viewAll,
|
||||||
|
bool viewSelf, int pageNumber, int pageSize, ExpensesFilter? filter)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var (totalPages, totalCount, expenseList) = await _expenseCache.GetExpenseListFromCacheAsync(tenantId, loggedInEmployeeId, viewAll, viewSelf, pageNumber, pageSize, filter);
|
||||||
|
if (expenseList.Any())
|
||||||
|
{
|
||||||
|
return (totalPages, totalCount, expenseList);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error occured while fetching the list of expenses to Cache");
|
||||||
|
}
|
||||||
|
return (0, 0, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region ======================================================= Report Cache =======================================================
|
||||||
|
|
||||||
public async Task<List<ProjectReportEmailMongoDB>?> GetProjectReportMail(bool IsSend)
|
public async Task<List<ProjectReportEmailMongoDB>?> GetProjectReportMail(bool IsSend)
|
||||||
{
|
{
|
||||||
@ -866,5 +1028,7 @@ namespace Marco.Pms.Services.Helpers
|
|||||||
_logger.LogError(ex, "Error occured while adding project report mail bodys");
|
_logger.LogError(ex, "Error occured while adding project report mail bodys");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,6 +5,8 @@ using Marco.Pms.Model.Dtos.Project;
|
|||||||
using Marco.Pms.Model.Employees;
|
using Marco.Pms.Model.Employees;
|
||||||
using Marco.Pms.Model.Expenses;
|
using Marco.Pms.Model.Expenses;
|
||||||
using Marco.Pms.Model.Master;
|
using Marco.Pms.Model.Master;
|
||||||
|
using Marco.Pms.Model.MongoDBModels.Employees;
|
||||||
|
using Marco.Pms.Model.MongoDBModels.Expenses;
|
||||||
using Marco.Pms.Model.MongoDBModels.Masters;
|
using Marco.Pms.Model.MongoDBModels.Masters;
|
||||||
using Marco.Pms.Model.MongoDBModels.Project;
|
using Marco.Pms.Model.MongoDBModels.Project;
|
||||||
using Marco.Pms.Model.Projects;
|
using Marco.Pms.Model.Projects;
|
||||||
@ -36,6 +38,23 @@ namespace Marco.Pms.Services.MappingProfiles
|
|||||||
opt => opt.MapFrom(src => new Guid(src.Id))
|
opt => opt.MapFrom(src => new Guid(src.Id))
|
||||||
);
|
);
|
||||||
|
|
||||||
|
CreateMap<Project, ProjectBasicMongoDB>()
|
||||||
|
.ForMember(
|
||||||
|
dest => dest.Id,
|
||||||
|
// Explicitly and safely convert Guid Id to string Id
|
||||||
|
opt => opt.MapFrom(src => src.Id.ToString()))
|
||||||
|
.ForMember(
|
||||||
|
dest => dest.TenantId,
|
||||||
|
opt => opt.MapFrom(src => src.TenantId.ToString()));
|
||||||
|
CreateMap<ProjectBasicMongoDB, ProjectInfoVM>()
|
||||||
|
.ForMember(
|
||||||
|
dest => dest.Id,
|
||||||
|
opt => opt.MapFrom(src => new Guid(src.Id)))
|
||||||
|
.ForMember(
|
||||||
|
dest => dest.ProjectStatusId,
|
||||||
|
opt => opt.MapFrom(src => Guid.Empty)
|
||||||
|
);
|
||||||
|
|
||||||
CreateMap<ProjectMongoDB, Project>()
|
CreateMap<ProjectMongoDB, Project>()
|
||||||
.ForMember(
|
.ForMember(
|
||||||
dest => dest.Id,
|
dest => dest.Id,
|
||||||
@ -73,6 +92,27 @@ namespace Marco.Pms.Services.MappingProfiles
|
|||||||
.ForMember(
|
.ForMember(
|
||||||
dest => dest.JobRoleName,
|
dest => dest.JobRoleName,
|
||||||
opt => opt.MapFrom(src => src.JobRole != null ? src.JobRole.Name : ""));
|
opt => opt.MapFrom(src => src.JobRole != null ? src.JobRole.Name : ""));
|
||||||
|
|
||||||
|
CreateMap<Employee, BasicEmployeeMongoDB>()
|
||||||
|
.ForMember(
|
||||||
|
dest => dest.Id,
|
||||||
|
opt => opt.MapFrom(src => src.Id.ToString()))
|
||||||
|
.ForMember(
|
||||||
|
dest => dest.JobRoleName,
|
||||||
|
opt => opt.MapFrom(src => src.JobRole != null ? src.JobRole.Name : ""))
|
||||||
|
.ForMember(
|
||||||
|
dest => dest.JobRoleId,
|
||||||
|
opt => opt.MapFrom(src => src.JobRoleId.ToString()))
|
||||||
|
.ForMember(
|
||||||
|
dest => dest.TenantId,
|
||||||
|
opt => opt.MapFrom(src => src.TenantId.ToString()));
|
||||||
|
CreateMap<BasicEmployeeMongoDB, BasicEmployeeVM>()
|
||||||
|
.ForMember(
|
||||||
|
dest => dest.Id,
|
||||||
|
opt => opt.MapFrom(src => Guid.Parse(src.Id)))
|
||||||
|
.ForMember(
|
||||||
|
dest => dest.JobRoleId,
|
||||||
|
opt => opt.MapFrom(src => Guid.Parse(src.JobRoleId ?? "")));
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region ======================================================= Expenses =======================================================
|
#region ======================================================= Expenses =======================================================
|
||||||
@ -80,6 +120,62 @@ namespace Marco.Pms.Services.MappingProfiles
|
|||||||
CreateMap<Expenses, ExpenseList>();
|
CreateMap<Expenses, ExpenseList>();
|
||||||
CreateMap<CreateExpensesDto, Expenses>();
|
CreateMap<CreateExpensesDto, Expenses>();
|
||||||
CreateMap<UpdateExpensesDto, Expenses>();
|
CreateMap<UpdateExpensesDto, Expenses>();
|
||||||
|
CreateMap<Expenses, ExpenseDetailsMongoDB>()
|
||||||
|
.ForMember(
|
||||||
|
dest => dest.Id,
|
||||||
|
opt => opt.MapFrom(src => src.Id.ToString()))
|
||||||
|
.ForMember(
|
||||||
|
dest => dest.ProjectId,
|
||||||
|
opt => opt.MapFrom(src => src.ProjectId.ToString()))
|
||||||
|
.ForMember(
|
||||||
|
dest => dest.ExpensesTypeId,
|
||||||
|
opt => opt.MapFrom(src => src.ExpensesTypeId.ToString()))
|
||||||
|
.ForMember(
|
||||||
|
dest => dest.PaymentModeId,
|
||||||
|
opt => opt.MapFrom(src => src.PaymentModeId.ToString()))
|
||||||
|
.ForMember(
|
||||||
|
dest => dest.PaidById,
|
||||||
|
opt => opt.MapFrom(src => src.PaidById.ToString()))
|
||||||
|
.ForMember(
|
||||||
|
dest => dest.CreatedById,
|
||||||
|
opt => opt.MapFrom(src => src.CreatedById.ToString()))
|
||||||
|
.ForMember(
|
||||||
|
dest => dest.StatusId,
|
||||||
|
opt => opt.MapFrom(src => src.StatusId.ToString()))
|
||||||
|
.ForMember(
|
||||||
|
dest => dest.TenantId,
|
||||||
|
opt => opt.MapFrom(src => src.TenantId.ToString()));
|
||||||
|
|
||||||
|
CreateMap<ExpenseDetailsMongoDB, Expenses>()
|
||||||
|
.ForMember(
|
||||||
|
dest => dest.Id,
|
||||||
|
opt => opt.MapFrom(src => Guid.Parse(src.Id)))
|
||||||
|
.ForMember(
|
||||||
|
dest => dest.ProjectId,
|
||||||
|
opt => opt.MapFrom(src => Guid.Parse(src.ProjectId)))
|
||||||
|
.ForMember(
|
||||||
|
dest => dest.ExpensesTypeId,
|
||||||
|
opt => opt.MapFrom(src => Guid.Parse(src.ExpensesTypeId)))
|
||||||
|
.ForMember(
|
||||||
|
dest => dest.PaymentModeId,
|
||||||
|
opt => opt.MapFrom(src => Guid.Parse(src.PaymentModeId)))
|
||||||
|
.ForMember(
|
||||||
|
dest => dest.PaidById,
|
||||||
|
opt => opt.MapFrom(src => Guid.Parse(src.PaidById)))
|
||||||
|
.ForMember(
|
||||||
|
dest => dest.CreatedById,
|
||||||
|
opt => opt.MapFrom(src => Guid.Parse(src.CreatedById)))
|
||||||
|
.ForMember(
|
||||||
|
dest => dest.StatusId,
|
||||||
|
opt => opt.MapFrom(src => Guid.Parse(src.StatusId)))
|
||||||
|
.ForMember(
|
||||||
|
dest => dest.TenantId,
|
||||||
|
opt => opt.MapFrom(src => Guid.Parse(src.TenantId)));
|
||||||
|
|
||||||
|
CreateMap<ExpenseDetailsMongoDB, ExpenseList>()
|
||||||
|
.ForMember(
|
||||||
|
dest => dest.Id,
|
||||||
|
opt => opt.MapFrom(src => Guid.Parse(src.Id)));
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
@ -93,6 +189,44 @@ namespace Marco.Pms.Services.MappingProfiles
|
|||||||
CreateMap<ExpensesTypeMaster, ExpensesTypeMasterVM>();
|
CreateMap<ExpensesTypeMaster, ExpensesTypeMasterVM>();
|
||||||
CreateMap<ExpensesStatusMaster, ExpensesStatusMasterVM>();
|
CreateMap<ExpensesStatusMaster, ExpensesStatusMasterVM>();
|
||||||
CreateMap<PaymentModeMatser, PaymentModeMatserVM>();
|
CreateMap<PaymentModeMatser, PaymentModeMatserVM>();
|
||||||
|
|
||||||
|
CreateMap<ExpensesTypeMaster, ExpensesTypeMasterMongoDB>()
|
||||||
|
.ForMember(
|
||||||
|
dest => dest.Id,
|
||||||
|
opt => opt.MapFrom(src => src.Id.ToString()))
|
||||||
|
.ForMember(
|
||||||
|
dest => dest.TenantId,
|
||||||
|
opt => opt.MapFrom(src => src.TenantId.ToString()));
|
||||||
|
CreateMap<ExpensesTypeMasterMongoDB, ExpensesTypeMasterVM>()
|
||||||
|
.ForMember(
|
||||||
|
dest => dest.Id,
|
||||||
|
opt => opt.MapFrom(src => Guid.Parse(src.Id)));
|
||||||
|
|
||||||
|
CreateMap<ExpensesStatusMaster, ExpensesStatusMasterMongoDB>()
|
||||||
|
.ForMember(
|
||||||
|
dest => dest.Id,
|
||||||
|
opt => opt.MapFrom(src => src.Id.ToString()))
|
||||||
|
.ForMember(
|
||||||
|
dest => dest.TenantId,
|
||||||
|
opt => opt.MapFrom(src => src.TenantId.ToString()));
|
||||||
|
CreateMap<ExpensesStatusMasterMongoDB, ExpensesStatusMasterVM>()
|
||||||
|
.ForMember(
|
||||||
|
dest => dest.Id,
|
||||||
|
opt => opt.MapFrom(src => Guid.Parse(src.Id)));
|
||||||
|
|
||||||
|
CreateMap<PaymentModeMatser, PaymentModeMatserMongoDB>()
|
||||||
|
.ForMember(
|
||||||
|
dest => dest.Id,
|
||||||
|
opt => opt.MapFrom(src => src.Id.ToString()))
|
||||||
|
.ForMember(
|
||||||
|
dest => dest.TenantId,
|
||||||
|
opt => opt.MapFrom(src => src.TenantId.ToString()));
|
||||||
|
CreateMap<PaymentModeMatserMongoDB, PaymentModeMatserVM>()
|
||||||
|
.ForMember(
|
||||||
|
dest => dest.Id,
|
||||||
|
opt => opt.MapFrom(src => Guid.Parse(src.Id)));
|
||||||
|
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
using AutoMapper;
|
using AutoMapper;
|
||||||
using Marco.Pms.Helpers;
|
|
||||||
using Marco.Pms.DataAccess.Data;
|
using Marco.Pms.DataAccess.Data;
|
||||||
using Marco.Pms.Model.DocumentManager;
|
using Marco.Pms.Helpers;
|
||||||
using Marco.Pms.Model.Dtos.Expenses;
|
using Marco.Pms.Model.Dtos.Expenses;
|
||||||
using Marco.Pms.Model.Employees;
|
using Marco.Pms.Model.Employees;
|
||||||
using Marco.Pms.Model.Entitlements;
|
using Marco.Pms.Model.Entitlements;
|
||||||
@ -12,10 +11,13 @@ using Marco.Pms.Model.ViewModels.Activities;
|
|||||||
using Marco.Pms.Model.ViewModels.Expanses;
|
using Marco.Pms.Model.ViewModels.Expanses;
|
||||||
using Marco.Pms.Model.ViewModels.Master;
|
using Marco.Pms.Model.ViewModels.Master;
|
||||||
using Marco.Pms.Model.ViewModels.Projects;
|
using Marco.Pms.Model.ViewModels.Projects;
|
||||||
|
using Marco.Pms.Services.Helpers;
|
||||||
using Marco.Pms.Services.Service.ServiceInterfaces;
|
using Marco.Pms.Services.Service.ServiceInterfaces;
|
||||||
using MarcoBMS.Services.Service;
|
using MarcoBMS.Services.Service;
|
||||||
|
using Microsoft.CodeAnalysis;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
|
using Document = Marco.Pms.Model.DocumentManager.Document;
|
||||||
|
|
||||||
namespace Marco.Pms.Services.Service
|
namespace Marco.Pms.Services.Service
|
||||||
{
|
{
|
||||||
@ -27,6 +29,7 @@ namespace Marco.Pms.Services.Service
|
|||||||
private readonly S3UploadService _s3Service;
|
private readonly S3UploadService _s3Service;
|
||||||
private readonly IServiceScopeFactory _serviceScopeFactory;
|
private readonly IServiceScopeFactory _serviceScopeFactory;
|
||||||
private readonly UpdateLogHelper _updateLogHelper;
|
private readonly UpdateLogHelper _updateLogHelper;
|
||||||
|
private readonly CacheUpdateHelper _cache;
|
||||||
private readonly IMapper _mapper;
|
private readonly IMapper _mapper;
|
||||||
private static readonly Guid Draft = Guid.Parse("297e0d8f-f668-41b5-bfea-e03b354251c8");
|
private static readonly Guid Draft = Guid.Parse("297e0d8f-f668-41b5-bfea-e03b354251c8");
|
||||||
private static readonly Guid Rejected = Guid.Parse("d1ee5eec-24b6-4364-8673-a8f859c60729");
|
private static readonly Guid Rejected = Guid.Parse("d1ee5eec-24b6-4364-8673-a8f859c60729");
|
||||||
@ -36,6 +39,7 @@ namespace Marco.Pms.Services.Service
|
|||||||
ApplicationDbContext context,
|
ApplicationDbContext context,
|
||||||
IServiceScopeFactory serviceScopeFactory,
|
IServiceScopeFactory serviceScopeFactory,
|
||||||
UpdateLogHelper updateLogHelper,
|
UpdateLogHelper updateLogHelper,
|
||||||
|
CacheUpdateHelper cache,
|
||||||
ILoggingService logger,
|
ILoggingService logger,
|
||||||
S3UploadService s3Service,
|
S3UploadService s3Service,
|
||||||
IMapper mapper)
|
IMapper mapper)
|
||||||
@ -43,6 +47,7 @@ namespace Marco.Pms.Services.Service
|
|||||||
_dbContextFactory = dbContextFactory;
|
_dbContextFactory = dbContextFactory;
|
||||||
_context = context;
|
_context = context;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
|
_cache = cache;
|
||||||
_serviceScopeFactory = serviceScopeFactory;
|
_serviceScopeFactory = serviceScopeFactory;
|
||||||
_updateLogHelper = updateLogHelper;
|
_updateLogHelper = updateLogHelper;
|
||||||
_s3Service = s3Service;
|
_s3Service = s3Service;
|
||||||
@ -73,7 +78,8 @@ namespace Marco.Pms.Services.Service
|
|||||||
return ApiResponse<object>.ErrorResponse("User not found or not authenticated.", 403);
|
return ApiResponse<object>.ErrorResponse("User not found or not authenticated.", 403);
|
||||||
}
|
}
|
||||||
Guid loggedInEmployeeId = loggedInEmployee.Id;
|
Guid loggedInEmployeeId = loggedInEmployee.Id;
|
||||||
|
List<ExpenseList> expenseVM = new List<ExpenseList>();
|
||||||
|
var totalEntites = 0;
|
||||||
var hasViewSelfPermissionTask = Task.Run(async () =>
|
var hasViewSelfPermissionTask = Task.Run(async () =>
|
||||||
{
|
{
|
||||||
using var scope = _serviceScopeFactory.CreateScope();
|
using var scope = _serviceScopeFactory.CreateScope();
|
||||||
@ -90,120 +96,103 @@ namespace Marco.Pms.Services.Service
|
|||||||
|
|
||||||
await Task.WhenAll(hasViewSelfPermissionTask, hasViewAllPermissionTask);
|
await Task.WhenAll(hasViewSelfPermissionTask, hasViewAllPermissionTask);
|
||||||
|
|
||||||
// 2. --- Build Base Query and Apply Permissions ---
|
if (!hasViewAllPermissionTask.Result && !hasViewSelfPermissionTask.Result)
|
||||||
// Start with a base IQueryable. Filters will be chained onto this.
|
|
||||||
var expensesQuery = _context.Expenses
|
|
||||||
.Include(e => e.ExpensesType)
|
|
||||||
.Include(e => e.Project)
|
|
||||||
.Include(e => e.PaidBy)
|
|
||||||
.ThenInclude(e => e!.JobRole)
|
|
||||||
.Include(e => e.PaymentMode)
|
|
||||||
.Include(e => e.Status)
|
|
||||||
.Include(e => e.CreatedBy)
|
|
||||||
.Where(e => e.TenantId == tenantId); // Always filter by TenantId first.
|
|
||||||
|
|
||||||
// Apply permission-based filtering BEFORE any other filters or pagination.
|
|
||||||
if (hasViewAllPermissionTask.Result)
|
|
||||||
{
|
|
||||||
// User has 'View All' permission, no initial restriction on who created the expense.
|
|
||||||
_logger.LogInfo("User {EmployeeId} has 'View All' permission.", loggedInEmployeeId);
|
|
||||||
}
|
|
||||||
else if (hasViewSelfPermissionTask.Result)
|
|
||||||
{
|
|
||||||
// User only has 'View Self' permission, so restrict the query to their own expenses.
|
|
||||||
_logger.LogInfo("User {EmployeeId} has 'View Self' permission. Restricting query to their expenses.", loggedInEmployeeId);
|
|
||||||
expensesQuery = expensesQuery.Where(e => e.CreatedById == loggedInEmployeeId);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
{
|
||||||
// User has neither required permission. Deny access.
|
// User has neither required permission. Deny access.
|
||||||
_logger.LogWarning("Access DENIED for employee {EmployeeId} attempting to get expenses list.", loggedInEmployeeId);
|
_logger.LogWarning("Access DENIED for employee {EmployeeId} attempting to get expenses list.", loggedInEmployeeId);
|
||||||
return ApiResponse<object>.SuccessResponse(new List<ExpenseList>(), "You do not have permission to view any expenses.", 200);
|
return ApiResponse<object>.SuccessResponse(new List<ExpenseList>(), "You do not have permission to view any expenses.", 200);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. --- Deserialize Filter and Apply ---
|
|
||||||
|
// 2. --- Deserialize Filter and Apply ---
|
||||||
ExpensesFilter? expenseFilter = TryDeserializeFilter(filter);
|
ExpensesFilter? expenseFilter = TryDeserializeFilter(filter);
|
||||||
|
|
||||||
if (expenseFilter != null)
|
var (totalPages, totalCount, expenseList) = await _cache.GetExpenseListAsync(tenantId, loggedInEmployeeId, hasViewAllPermissionTask.Result, hasViewSelfPermissionTask.Result,
|
||||||
|
pageNumber, pageSize, expenseFilter);
|
||||||
|
|
||||||
|
if (expenseList == null)
|
||||||
{
|
{
|
||||||
// CRITICAL FIX: Apply filters cumulatively using multiple `if` statements, not `if-else if`.
|
|
||||||
if (expenseFilter.StartDate.HasValue && expenseFilter.EndDate.HasValue)
|
// 3. --- Build Base Query and Apply Permissions ---
|
||||||
|
// Start with a base IQueryable. Filters will be chained onto this.
|
||||||
|
var expensesQuery = _context.Expenses
|
||||||
|
.Where(e => e.TenantId == tenantId); // Always filter by TenantId first.
|
||||||
|
|
||||||
|
await _cache.AddExpensesListToCache(expenses: await expensesQuery.ToListAsync());
|
||||||
|
|
||||||
|
// Apply permission-based filtering BEFORE any other filters or pagination.
|
||||||
|
|
||||||
|
if (!hasViewAllPermissionTask.Result && hasViewSelfPermissionTask.Result)
|
||||||
{
|
{
|
||||||
expensesQuery = expensesQuery.Where(e => e.CreatedAt.Date >= expenseFilter.StartDate.Value.Date && e.CreatedAt.Date <= expenseFilter.EndDate.Value.Date);
|
// User only has 'View Self' permission, so restrict the query to their own expenses.
|
||||||
|
_logger.LogInfo("User {EmployeeId} has 'View Self' permission. Restricting query to their expenses.", loggedInEmployeeId);
|
||||||
|
expensesQuery = expensesQuery.Where(e => e.CreatedById == loggedInEmployeeId);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (expenseFilter.ProjectIds?.Any() == true)
|
if (expenseFilter != null)
|
||||||
{
|
{
|
||||||
expensesQuery = expensesQuery.Where(e => expenseFilter.ProjectIds.Contains(e.ProjectId));
|
// CRITICAL FIX: Apply filters cumulatively using multiple `if` statements, not `if-else if`.
|
||||||
|
if (expenseFilter.StartDate.HasValue && expenseFilter.EndDate.HasValue)
|
||||||
|
{
|
||||||
|
expensesQuery = expensesQuery.Where(e => e.CreatedAt.Date >= expenseFilter.StartDate.Value.Date && e.CreatedAt.Date <= expenseFilter.EndDate.Value.Date);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (expenseFilter.ProjectIds?.Any() == true)
|
||||||
|
{
|
||||||
|
expensesQuery = expensesQuery.Where(e => expenseFilter.ProjectIds.Contains(e.ProjectId));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (expenseFilter.StatusIds?.Any() == true)
|
||||||
|
{
|
||||||
|
expensesQuery = expensesQuery.Where(e => expenseFilter.StatusIds.Contains(e.StatusId));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (expenseFilter.PaidById?.Any() == true)
|
||||||
|
{
|
||||||
|
expensesQuery = expensesQuery.Where(e => expenseFilter.PaidById.Contains(e.PaidById));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only allow filtering by 'CreatedBy' if the user has 'View All' permission.
|
||||||
|
if (expenseFilter.CreatedByIds?.Any() == true && hasViewAllPermissionTask.Result)
|
||||||
|
{
|
||||||
|
expensesQuery = expensesQuery.Where(e => expenseFilter.CreatedByIds.Contains(e.CreatedById));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (expenseFilter.StatusIds?.Any() == true)
|
// 4. --- Apply Ordering and Pagination ---
|
||||||
|
// This should be the last step before executing the query.
|
||||||
|
|
||||||
|
totalEntites = await expensesQuery.CountAsync();
|
||||||
|
|
||||||
|
var paginatedQuery = expensesQuery
|
||||||
|
.OrderByDescending(e => e.CreatedAt)
|
||||||
|
.Skip((pageNumber - 1) * pageSize)
|
||||||
|
.Take(pageSize);
|
||||||
|
|
||||||
|
// 5. --- Execute Query and Map Results ---
|
||||||
|
var expensesList = await paginatedQuery.ToListAsync();
|
||||||
|
|
||||||
|
if (!expensesList.Any())
|
||||||
{
|
{
|
||||||
expensesQuery = expensesQuery.Where(e => expenseFilter.StatusIds.Contains(e.StatusId));
|
_logger.LogInfo("No expenses found matching the criteria for employee {EmployeeId}.", loggedInEmployeeId);
|
||||||
|
return ApiResponse<object>.SuccessResponse(new List<ExpenseList>(), "No expenses found for the given criteria.", 200);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (expenseFilter.PaidById?.Any() == true)
|
expenseVM = await GetAllExpnesRelatedTables(expensesList);
|
||||||
{
|
|
||||||
expensesQuery = expensesQuery.Where(e => expenseFilter.PaidById.Contains(e.PaidById));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Only allow filtering by 'CreatedBy' if the user has 'View All' permission.
|
|
||||||
if (expenseFilter.CreatedByIds?.Any() == true && hasViewAllPermissionTask.Result)
|
|
||||||
{
|
|
||||||
expensesQuery = expensesQuery.Where(e => expenseFilter.CreatedByIds.Contains(e.CreatedById));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
else
|
||||||
// 4. --- Apply Ordering and Pagination ---
|
|
||||||
// This should be the last step before executing the query.
|
|
||||||
|
|
||||||
var totalEntites = await expensesQuery.CountAsync();
|
|
||||||
|
|
||||||
var paginatedQuery = expensesQuery
|
|
||||||
.OrderByDescending(e => e.CreatedAt)
|
|
||||||
.Skip((pageNumber - 1) * pageSize)
|
|
||||||
.Take(pageSize);
|
|
||||||
|
|
||||||
// 5. --- Execute Query and Map Results ---
|
|
||||||
var expensesList = await paginatedQuery.ToListAsync();
|
|
||||||
|
|
||||||
if (!expensesList.Any())
|
|
||||||
{
|
{
|
||||||
_logger.LogInfo("No expenses found matching the criteria for employee {EmployeeId}.", loggedInEmployeeId);
|
expenseVM = await GetAllExpnesRelatedTables(_mapper.Map<List<Expenses>>(expenseList));
|
||||||
return ApiResponse<object>.SuccessResponse(new List<ExpenseList>(), "No expenses found for the given criteria.", 200);
|
totalEntites = (int)totalCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
var expenseVM = _mapper.Map<List<ExpenseList>>(expensesList);
|
|
||||||
|
|
||||||
// 6. --- Efficiently Fetch and Append 'Next Status' Information ---
|
|
||||||
var statusIds = expensesList.Select(e => e.StatusId).Distinct().ToList();
|
|
||||||
|
|
||||||
var statusMappings = await _context.ExpensesStatusMapping
|
|
||||||
.Include(sm => sm.NextStatus)
|
|
||||||
.Where(sm => statusIds.Contains(sm.StatusId))
|
|
||||||
.ToListAsync();
|
|
||||||
|
|
||||||
// Use a Lookup for efficient O(1) mapping. This is much better than repeated `.Where()` in a loop.
|
|
||||||
var statusMapLookup = statusMappings.ToLookup(sm => sm.StatusId);
|
|
||||||
|
|
||||||
foreach (var expense in expenseVM)
|
|
||||||
{
|
|
||||||
if (expense.Status?.Id != null && statusMapLookup.Contains(expense.Status.Id))
|
|
||||||
{
|
|
||||||
expense.NextStatus = statusMapLookup[expense.Status.Id]
|
|
||||||
.Select(sm => _mapper.Map<ExpensesStatusMasterVM>(sm.NextStatus))
|
|
||||||
.ToList();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
expense.NextStatus = new List<ExpensesStatusMasterVM>(); // Ensure it's never null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 7. --- Return Final Success Response ---
|
// 7. --- Return Final Success Response ---
|
||||||
var message = $"{expenseVM.Count} expense records fetched successfully.";
|
var message = $"{expenseVM.Count} expense records fetched successfully.";
|
||||||
_logger.LogInfo(message);
|
_logger.LogInfo(message);
|
||||||
var totalPages = (int)Math.Ceiling((double)totalEntites / pageSize);
|
|
||||||
var response = new
|
var response = new
|
||||||
{
|
{
|
||||||
CurrentPage = pageNumber,
|
CurrentPage = pageNumber,
|
||||||
@ -211,7 +200,6 @@ namespace Marco.Pms.Services.Service
|
|||||||
TotalEntites = totalEntites,
|
TotalEntites = totalEntites,
|
||||||
Data = expenseVM,
|
Data = expenseVM,
|
||||||
};
|
};
|
||||||
|
|
||||||
return ApiResponse<object>.SuccessResponse(response, message, 200);
|
return ApiResponse<object>.SuccessResponse(response, message, 200);
|
||||||
}
|
}
|
||||||
catch (DbUpdateException dbEx)
|
catch (DbUpdateException dbEx)
|
||||||
@ -381,6 +369,8 @@ namespace Marco.Pms.Services.Service
|
|||||||
// 6. Transaction Commit
|
// 6. Transaction Commit
|
||||||
await transaction.CommitAsync();
|
await transaction.CommitAsync();
|
||||||
|
|
||||||
|
await _cache.AddExpenseByObjectAsync(expense);
|
||||||
|
|
||||||
var response = _mapper.Map<ExpenseList>(expense);
|
var response = _mapper.Map<ExpenseList>(expense);
|
||||||
response.PaidBy = _mapper.Map<BasicEmployeeVM>(paidBy);
|
response.PaidBy = _mapper.Map<BasicEmployeeVM>(paidBy);
|
||||||
response.Project = _mapper.Map<ProjectInfoVM>(project);
|
response.Project = _mapper.Map<ProjectInfoVM>(project);
|
||||||
@ -728,6 +718,86 @@ namespace Marco.Pms.Services.Service
|
|||||||
}
|
}
|
||||||
#region =================================================================== Helper Functions ===================================================================
|
#region =================================================================== Helper Functions ===================================================================
|
||||||
|
|
||||||
|
private async Task<List<ExpenseList>> GetAllExpnesRelatedTables(List<Expenses> model)
|
||||||
|
{
|
||||||
|
List<ExpenseList> expenseList = new List<ExpenseList>();
|
||||||
|
var projectIds = model.Select(m => m.ProjectId).ToList();
|
||||||
|
var statusIds = model.Select(m => m.StatusId).ToList();
|
||||||
|
var expensesTypeIds = model.Select(m => m.ExpensesTypeId).ToList();
|
||||||
|
var paymentModeIds = model.Select(m => m.PaymentModeId).ToList();
|
||||||
|
var createdByIds = model.Select(m => m.CreatedById).ToList();
|
||||||
|
var paidByIds = model.Select(m => m.PaidById).ToList();
|
||||||
|
|
||||||
|
var projectTask = Task.Run(async () =>
|
||||||
|
{
|
||||||
|
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
|
||||||
|
return await dbContext.Projects.AsNoTracking().Where(p => projectIds.Contains(p.Id)).ToListAsync();
|
||||||
|
});
|
||||||
|
var paidByTask = Task.Run(async () =>
|
||||||
|
{
|
||||||
|
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
|
||||||
|
return await dbContext.Employees.AsNoTracking().Where(e => paidByIds.Contains(e.Id)).ToListAsync();
|
||||||
|
});
|
||||||
|
var createdByTask = Task.Run(async () =>
|
||||||
|
{
|
||||||
|
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
|
||||||
|
return await dbContext.Employees.AsNoTracking().Where(e => createdByIds.Contains(e.Id)).ToListAsync();
|
||||||
|
});
|
||||||
|
var expenseTypeTask = Task.Run(async () =>
|
||||||
|
{
|
||||||
|
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
|
||||||
|
return await dbContext.ExpensesTypeMaster.AsNoTracking().Where(et => expensesTypeIds.Contains(et.Id)).ToListAsync();
|
||||||
|
});
|
||||||
|
var paymentModeTask = Task.Run(async () =>
|
||||||
|
{
|
||||||
|
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
|
||||||
|
return await dbContext.PaymentModeMatser.AsNoTracking().Where(pm => paymentModeIds.Contains(pm.Id)).ToListAsync();
|
||||||
|
});
|
||||||
|
var statusMappingTask = Task.Run(async () =>
|
||||||
|
{
|
||||||
|
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
|
||||||
|
return await dbContext.ExpensesStatusMapping
|
||||||
|
.Include(s => s.Status)
|
||||||
|
.Include(s => s.NextStatus)
|
||||||
|
.AsNoTracking()
|
||||||
|
.Where(es => statusIds.Contains(es.StatusId) && es.Status != null)
|
||||||
|
.GroupBy(s => s.StatusId)
|
||||||
|
.Select(g => new
|
||||||
|
{
|
||||||
|
StatusId = g.Key,
|
||||||
|
Status = g.Select(s => s.Status).FirstOrDefault(),
|
||||||
|
NextStatus = g.Select(s => s.NextStatus).ToList()
|
||||||
|
}).ToListAsync();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Await all prerequisite checks at once.
|
||||||
|
await Task.WhenAll(projectTask, expenseTypeTask, paymentModeTask, statusMappingTask, paidByTask, createdByTask);
|
||||||
|
|
||||||
|
var projects = await projectTask;
|
||||||
|
var expenseTypes = await expenseTypeTask;
|
||||||
|
var paymentModes = await paymentModeTask;
|
||||||
|
var statusMappings = await statusMappingTask;
|
||||||
|
var paidBys = await paidByTask;
|
||||||
|
var createdBys = await createdByTask;
|
||||||
|
|
||||||
|
expenseList = model.Select(m =>
|
||||||
|
{
|
||||||
|
var response = _mapper.Map<ExpenseList>(m);
|
||||||
|
|
||||||
|
response.Project = projects.Where(p => p.Id == m.ProjectId).Select(p => _mapper.Map<ProjectInfoVM>(p)).FirstOrDefault();
|
||||||
|
response.PaidBy = paidBys.Where(p => p.Id == m.PaidById).Select(p => _mapper.Map<BasicEmployeeVM>(p)).FirstOrDefault();
|
||||||
|
response.CreatedBy = createdBys.Where(e => e.Id == m.CreatedById).Select(e => _mapper.Map<BasicEmployeeVM>(e)).FirstOrDefault();
|
||||||
|
response.Status = statusMappings.Where(s => s.StatusId == m.StatusId).Select(s => _mapper.Map<ExpensesStatusMasterVM>(s.Status)).FirstOrDefault();
|
||||||
|
response.NextStatus = statusMappings.Where(s => s.StatusId == m.StatusId).Select(s => _mapper.Map<List<ExpensesStatusMasterVM>>(s.NextStatus)).FirstOrDefault();
|
||||||
|
response.PaymentMode = paymentModes.Where(pm => pm.Id == m.PaymentModeId).Select(pm => _mapper.Map<PaymentModeMatserVM>(pm)).FirstOrDefault();
|
||||||
|
response.ExpensesType = expenseTypes.Where(et => et.Id == m.ExpensesTypeId).Select(et => _mapper.Map<ExpensesTypeMasterVM>(et)).FirstOrDefault();
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}).ToList();
|
||||||
|
|
||||||
|
return expenseList;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Deserializes the filter string, handling multiple potential formats (e.g., direct JSON vs. escaped JSON string).
|
/// Deserializes the filter string, handling multiple potential formats (e.g., direct JSON vs. escaped JSON string).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user