From 1c9008ca627b9f05eb0a3c6c82aad9af68532dbf Mon Sep 17 00:00:00 2001 From: "ashutosh.nehete" Date: Thu, 31 Jul 2025 10:28:45 +0530 Subject: [PATCH] Added the search funcationality abd chnaged the cache object --- Marco.Pms.Helpers/CacheHelper/ExpenseCache.cs | 51 ++- .../Expenses/ExpenseDetailsMongoDB.cs | 25 +- .../Masters/ExpensesStatusMasterMongoDB.cs | 1 + .../ViewModels/Expenses/ExpenseList.cs | 2 + .../Controllers/ExpenseController.cs | 22 +- .../Helpers/CacheUpdateHelper.cs | 381 +++++++++++++----- .../MappingProfiles/MappingProfile.cs | 54 --- Marco.Pms.Services/Service/ExpensesService.cs | 377 ++++++++--------- .../ServiceInterfaces/IExpensesService.cs | 3 +- 9 files changed, 530 insertions(+), 386 deletions(-) diff --git a/Marco.Pms.Helpers/CacheHelper/ExpenseCache.cs b/Marco.Pms.Helpers/CacheHelper/ExpenseCache.cs index 5d29088..fc670d6 100644 --- a/Marco.Pms.Helpers/CacheHelper/ExpenseCache.cs +++ b/Marco.Pms.Helpers/CacheHelper/ExpenseCache.cs @@ -1,6 +1,7 @@ using Marco.Pms.Model.MongoDBModels.Expenses; using Marco.Pms.Model.Utilities; using Microsoft.Extensions.Configuration; +using MongoDB.Bson; using MongoDB.Driver; namespace Marco.Pms.Helpers.CacheHelper @@ -36,7 +37,7 @@ namespace Marco.Pms.Helpers.CacheHelper await InitializeCollectionAsync(); } public async Task<(int totalPages, long totalCount, List expenseList)> GetExpenseListFromCacheAsync(Guid tenantId, Guid loggedInEmployeeId, bool viewAll, - bool viewSelf, int pageNumber, int pageSize, ExpensesFilter? expenseFilter) + bool viewSelf, int pageNumber, int pageSize, ExpensesFilter? expenseFilter, string? searchString) { var filterBuilder = Builders.Filter; var filter = filterBuilder.Empty; @@ -44,10 +45,11 @@ namespace Marco.Pms.Helpers.CacheHelper // Permission-based filter if (!viewAll && viewSelf) { - filter &= filterBuilder.Eq(e => e.CreatedById, loggedInEmployeeId.ToString()); + filter &= filterBuilder.Eq(e => e.CreatedBy.Id, loggedInEmployeeId.ToString()); } // Apply filters + if (expenseFilter != null) { if (expenseFilter.StartDate.HasValue && expenseFilter.EndDate.HasValue) @@ -58,25 +60,62 @@ namespace Marco.Pms.Helpers.CacheHelper if (expenseFilter.ProjectIds?.Any() == true) { - filter &= filterBuilder.In(e => e.ProjectId, expenseFilter.ProjectIds.Select(p => p.ToString()).ToList()); + filter &= filterBuilder.In(e => e.Project.Id, 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()); + filter &= filterBuilder.In(e => e.Status.Id, 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()); + filter &= filterBuilder.In(e => e.PaidBy.Id, 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()); + filter &= filterBuilder.In(e => e.CreatedBy.Id, expenseFilter.CreatedByIds.Select(p => p.ToString()).ToList()); } } + if (!string.IsNullOrWhiteSpace(searchString)) + { + var searchPattern = new BsonRegularExpression(searchString, "i"); + + // The base text searches remain the same + var searchClauses = new List> + { + filterBuilder.Regex(e => e.Description, searchPattern), + filterBuilder.Regex(e => e.TransactionId, searchPattern) + }; + + // Build the complex filter for PaidBy.FullName + var paidByFilter = new BsonDocument("$expr", + new BsonDocument("$regexMatch", new BsonDocument + { + { "input", new BsonDocument("$concat", new BsonArray { "$PaidBy.FirstName", " ", "$PaidBy.LastName" }) }, + { "regex", searchString }, // BsonRegularExpression can't be used here, pass the string + { "options", "i" } // Case-insensitivity option + }) + ); + searchClauses.Add(paidByFilter); + + // Build the complex filter for CreatedBy.FullName + var createdByFilter = new BsonDocument("$expr", + new BsonDocument("$regexMatch", new BsonDocument + { + { "input", new BsonDocument("$concat", new BsonArray { "$CreatedBy.FirstName", " ", "$CreatedBy.LastName" }) }, + { "regex", searchString }, + { "options", "i" } + }) + ); + searchClauses.Add(createdByFilter); + + // Combine all clauses with an OR + filter &= filterBuilder.Or(searchClauses); + } + // Total count var totalCount = await _collection.CountDocumentsAsync(filter); var totalPages = (int)Math.Ceiling((double)totalCount / pageSize); diff --git a/Marco.Pms.Model/MongoDBModels/Expenses/ExpenseDetailsMongoDB.cs b/Marco.Pms.Model/MongoDBModels/Expenses/ExpenseDetailsMongoDB.cs index 9dad1ce..c2618b9 100644 --- a/Marco.Pms.Model/MongoDBModels/Expenses/ExpenseDetailsMongoDB.cs +++ b/Marco.Pms.Model/MongoDBModels/Expenses/ExpenseDetailsMongoDB.cs @@ -1,22 +1,27 @@ -namespace Marco.Pms.Model.MongoDBModels.Expenses +using Marco.Pms.Model.MongoDBModels.Employees; +using Marco.Pms.Model.MongoDBModels.Masters; +using Marco.Pms.Model.MongoDBModels.Project; + +namespace Marco.Pms.Model.MongoDBModels.Expenses { 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 string? ReviewedById { get; set; } - public string? ApprovedById { get; set; } - public string? ProcessedById { get; set; } + public ProjectBasicMongoDB Project { get; set; } = new ProjectBasicMongoDB(); + public ExpensesTypeMasterMongoDB ExpensesType { get; set; } = new ExpensesTypeMasterMongoDB(); + public PaymentModeMatserMongoDB PaymentMode { get; set; } = new PaymentModeMatserMongoDB(); + public BasicEmployeeMongoDB PaidBy { get; set; } = new BasicEmployeeMongoDB(); + public BasicEmployeeMongoDB CreatedBy { get; set; } = new BasicEmployeeMongoDB(); + public BasicEmployeeMongoDB? ReviewedBy { get; set; } + public BasicEmployeeMongoDB? ApprovedBy { get; set; } + public BasicEmployeeMongoDB? ProcessedBy { get; set; } 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 ExpensesStatusMasterMongoDB Status { get; set; } = new ExpensesStatusMasterMongoDB(); + public List NextStatus { get; set; } = new List(); public bool PreApproved { get; set; } = false; public string? TransactionId { get; set; } public string Description { get; set; } = string.Empty; diff --git a/Marco.Pms.Model/MongoDBModels/Masters/ExpensesStatusMasterMongoDB.cs b/Marco.Pms.Model/MongoDBModels/Masters/ExpensesStatusMasterMongoDB.cs index 3e4a52a..03e512d 100644 --- a/Marco.Pms.Model/MongoDBModels/Masters/ExpensesStatusMasterMongoDB.cs +++ b/Marco.Pms.Model/MongoDBModels/Masters/ExpensesStatusMasterMongoDB.cs @@ -6,6 +6,7 @@ public string Name { get; set; } = string.Empty; public string DisplayName { get; set; } = string.Empty; public string Description { get; set; } = string.Empty; + //public List PermissionIds { get; set; } = new List(); public string? Color { get; set; } public bool IsSystem { get; set; } = false; } diff --git a/Marco.Pms.Model/ViewModels/Expenses/ExpenseList.cs b/Marco.Pms.Model/ViewModels/Expenses/ExpenseList.cs index 9bc3b08..c29b020 100644 --- a/Marco.Pms.Model/ViewModels/Expenses/ExpenseList.cs +++ b/Marco.Pms.Model/ViewModels/Expenses/ExpenseList.cs @@ -18,6 +18,8 @@ namespace Marco.Pms.Model.ViewModels.Expanses public DateTime TransactionDate { get; set; } public DateTime CreatedAt { get; set; } public string SupplerName { get; set; } = string.Empty; + public string Description { get; set; } = string.Empty; + public string TransactionId { get; set; } = string.Empty; public double Amount { get; set; } public ExpensesStatusMasterVM? Status { get; set; } public List? NextStatus { get; set; } diff --git a/Marco.Pms.Services/Controllers/ExpenseController.cs b/Marco.Pms.Services/Controllers/ExpenseController.cs index 5bbcf2c..36b0f74 100644 --- a/Marco.Pms.Services/Controllers/ExpenseController.cs +++ b/Marco.Pms.Services/Controllers/ExpenseController.cs @@ -39,10 +39,10 @@ namespace Marco.Pms.Services.Controllers /// A paginated list of expenses. [HttpGet("list")] - public async Task GetExpensesList(string? filter, int pageSize = 20, int pageNumber = 1) + public async Task GetExpensesList([FromQuery] string? searchString, [FromQuery] string? filter, [FromQuery] int pageSize = 20, [FromQuery] int pageNumber = 1) { var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); - var response = await _expensesService.GetExpensesListAsync(loggedInEmployee, tenantId, filter, pageSize, pageNumber); + var response = await _expensesService.GetExpensesListAsync(loggedInEmployee, tenantId, searchString, filter, pageSize, pageNumber); return StatusCode(response.StatusCode, response); } @@ -62,11 +62,24 @@ namespace Marco.Pms.Services.Controllers return StatusCode(response.StatusCode, response); } + [HttpGet("filter")] + public async Task GetFilterObject() + { + var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); + var response = await _expensesService.GetFilterObjectAsync(loggedInEmployee, tenantId); + return StatusCode(response.StatusCode, response); + } + [HttpPost("create")] public async Task CreateExpense([FromBody] CreateExpensesDto model) { var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); var response = await _expensesService.CreateExpenseAsync(model, loggedInEmployee, tenantId); + if (response.Success) + { + var notification = new { LoggedInUserId = loggedInEmployee.Id, Keyword = "Expanse", Response = response.Data }; + await _signalR.SendNotificationAsync(notification); + } return StatusCode(response.StatusCode, response); } @@ -101,6 +114,11 @@ namespace Marco.Pms.Services.Controllers { var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); var response = await _expensesService.DeleteExpanseAsync(id, loggedInEmployee, tenantId); + if (response.Success) + { + var notification = new { LoggedInUserId = loggedInEmployee.Id, Keyword = "Expanse", Response = response.Data }; + await _signalR.SendNotificationAsync(notification); + } return StatusCode(response.StatusCode, response); } diff --git a/Marco.Pms.Services/Helpers/CacheUpdateHelper.cs b/Marco.Pms.Services/Helpers/CacheUpdateHelper.cs index 9acf08f..6aa5305 100644 --- a/Marco.Pms.Services/Helpers/CacheUpdateHelper.cs +++ b/Marco.Pms.Services/Helpers/CacheUpdateHelper.cs @@ -5,6 +5,7 @@ using Marco.Pms.Helpers.CacheHelper; using Marco.Pms.Model.Expenses; using Marco.Pms.Model.Master; using Marco.Pms.Model.MongoDBModels; +using Marco.Pms.Model.MongoDBModels.Employees; using Marco.Pms.Model.MongoDBModels.Expenses; using Marco.Pms.Model.MongoDBModels.Masters; using Marco.Pms.Model.MongoDBModels.Project; @@ -869,36 +870,8 @@ namespace Marco.Pms.Services.Helpers #region ======================================================= Expenses Cache ======================================================= public async Task AddExpenseByObjectAsync(Expenses expense) { - var expenseCache = _mapper.Map(expense); + var expenseCache = await GetAllExpnesRelatedTablesForSingle(expense, expense.TenantId); - try - { - var billAttachment = await _context.BillAttachments - .Include(ba => ba.Document) - .AsNoTracking() - .Where(ba => ba.ExpensesId == expense.Id && ba.Document != null) - .GroupBy(ba => ba.ExpensesId) - .Select(g => new - { - Documents = g.Select(ba => new DocumentMongoDB - { - DocumentId = ba.Document!.Id.ToString(), - FileName = ba.Document.FileName, - ContentType = ba.Document.ContentType, - S3Key = ba.Document.S3Key, - ThumbS3Key = ba.Document.ThumbS3Key ?? ba.Document.S3Key - }).ToList() - }) - .FirstOrDefaultAsync(); ; - if (billAttachment != null) - { - expenseCache.Documents = billAttachment.Documents; - } - } - catch (Exception ex) - { - _logger.LogError(ex, "Error occurd while fetched expense related tables to save in cahce"); - } try { await _expenseCache.AddExpenseToCacheAsync(expenseCache); @@ -914,40 +887,13 @@ namespace Marco.Pms.Services.Helpers 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(expense); + if (expense == null) { return 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 - { - Documents = g.Select(ba => new DocumentMongoDB - { - DocumentId = ba.Document!.Id.ToString(), - FileName = ba.Document.FileName, - ContentType = ba.Document.ContentType, - S3Key = ba.Document.S3Key, - ThumbS3Key = ba.Document.ThumbS3Key ?? ba.Document.S3Key - }).ToList() - }) - .FirstOrDefaultAsync(); - if (billAttachments != null) - { - expenseCache.Documents = billAttachments.Documents; - } - } - catch (Exception ex) - { - _logger.LogError(ex, "Error occurd while fetched expense related tables to save in cahce"); - return null; - } + var expenseCache = await GetAllExpnesRelatedTablesForSingle(expense, expense.TenantId); + try { await _expenseCache.AddExpenseToCacheAsync(expenseCache); @@ -962,39 +908,9 @@ namespace Marco.Pms.Services.Helpers } - public async Task AddExpensesListToCache(List expenses) + public async Task AddExpensesListToCache(List expenses, Guid tenantId) { - var expensesCache = _mapper.Map>(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, - Documents = g.Select(ba => new DocumentMongoDB - { - DocumentId = ba.Document!.Id.ToString(), - FileName = ba.Document.FileName, - ContentType = ba.Document.ContentType, - S3Key = ba.Document.S3Key, - ThumbS3Key = ba.Document.ThumbS3Key ?? ba.Document.S3Key - }).ToList() - }) - .ToListAsync(); - foreach (var expenseCache in expensesCache) - { - expenseCache.Documents = billAttachments.Where(ba => ba.ExpensesId == Guid.Parse(expenseCache.Id)).Select(ba => ba.Documents).FirstOrDefault() ?? new List(); - } - } - catch (Exception ex) - { - _logger.LogError(ex, "Error occurd while fetched expense related tables to save in cahce"); - } + var expensesCache = await GetAllExpnesRelatedTablesForList(expenses, tenantId); try { @@ -1007,15 +923,18 @@ namespace Marco.Pms.Services.Helpers } public async Task<(int totalPages, long totalCount, List? expenseList)> GetExpenseListAsync(Guid tenantId, Guid loggedInEmployeeId, bool viewAll, - bool viewSelf, int pageNumber, int pageSize, ExpensesFilter? filter) + bool viewSelf, int pageNumber, int pageSize, ExpensesFilter? filter, string? searchString) { try { - var (totalPages, totalCount, expenseList) = await _expenseCache.GetExpenseListFromCacheAsync(tenantId, loggedInEmployeeId, viewAll, viewSelf, pageNumber, pageSize, filter); + var (totalPages, totalCount, expenseList) = await _expenseCache.GetExpenseListFromCacheAsync(tenantId, loggedInEmployeeId, viewAll, viewSelf, pageNumber, pageSize, filter, searchString); if (expenseList.Any()) { + + return (totalPages, totalCount, expenseList); } + } catch (Exception ex) { @@ -1101,5 +1020,281 @@ namespace Marco.Pms.Services.Helpers } #endregion + + + #region ======================================================= Helper Functions ======================================================= + private async Task> GetAllExpnesRelatedTablesForList(List model, Guid tenantId) + { + List expenseList = new List(); + var expenseIds = model.Select(m => m.Id).ToList(); + 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 reviewedByIds = model.Select(m => m.ReviewedById).ToList(); + var approvedByIds = model.Select(m => m.ApprovedById).ToList(); + var processedByIds = model.Select(m => m.ProcessedById).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) && p.TenantId == tenantId).ToListAsync(); + }); + var paidByTask = Task.Run(async () => + { + await using var dbContext = await _dbContextFactory.CreateDbContextAsync(); + return await dbContext.Employees.Include(e => e.JobRole).AsNoTracking().Where(e => paidByIds.Contains(e.Id) && e.TenantId == tenantId).ToListAsync(); + }); + var createdByTask = Task.Run(async () => + { + await using var dbContext = await _dbContextFactory.CreateDbContextAsync(); + return await dbContext.Employees.Include(e => e.JobRole).AsNoTracking().Where(e => createdByIds.Contains(e.Id) && e.TenantId == tenantId).ToListAsync(); + }); + var reviewedByTask = Task.Run(async () => + { + await using var dbContext = await _dbContextFactory.CreateDbContextAsync(); + return await dbContext.Employees.Include(e => e.JobRole).AsNoTracking().Where(e => reviewedByIds.Contains(e.Id) && e.TenantId == tenantId).ToListAsync(); + }); + var approvedByTask = Task.Run(async () => + { + await using var dbContext = await _dbContextFactory.CreateDbContextAsync(); + return await dbContext.Employees.Include(e => e.JobRole).AsNoTracking().Where(e => approvedByIds.Contains(e.Id) && e.TenantId == tenantId).ToListAsync(); + }); + var processedByTask = Task.Run(async () => + { + await using var dbContext = await _dbContextFactory.CreateDbContextAsync(); + return await dbContext.Employees.Include(e => e.JobRole).AsNoTracking().Where(e => processedByIds.Contains(e.Id) && e.TenantId == tenantId).ToListAsync(); + }); + var expenseTypeTask = Task.Run(async () => + { + await using var dbContext = await _dbContextFactory.CreateDbContextAsync(); + return await dbContext.ExpensesTypeMaster.AsNoTracking().Where(et => expensesTypeIds.Contains(et.Id) && et.TenantId == tenantId).ToListAsync(); + }); + var paymentModeTask = Task.Run(async () => + { + await using var dbContext = await _dbContextFactory.CreateDbContextAsync(); + return await dbContext.PaymentModeMatser.AsNoTracking().Where(pm => paymentModeIds.Contains(pm.Id) && pm.TenantId == tenantId).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(); + }); + var statusTask = Task.Run(async () => + { + await using var dbContext = await _dbContextFactory.CreateDbContextAsync(); + return await dbContext.ExpensesStatusMaster + .AsNoTracking() + .Where(es => statusIds.Contains(es.Id)) + .ToListAsync(); + }); + var billAttachmentsTask = Task.Run(async () => + { + await using var dbContext = await _dbContextFactory.CreateDbContextAsync(); + return await dbContext.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, + Documents = g.Select(ba => new DocumentMongoDB + { + DocumentId = ba.Document!.Id.ToString(), + FileName = ba.Document.FileName, + ContentType = ba.Document.ContentType, + S3Key = ba.Document.S3Key, + ThumbS3Key = ba.Document.ThumbS3Key ?? ba.Document.S3Key + }).ToList() + }) + .ToListAsync(); + }); + + // Await all prerequisite checks at once. + await Task.WhenAll(projectTask, expenseTypeTask, paymentModeTask, statusMappingTask, paidByTask, createdByTask, reviewedByTask, approvedByTask, + processedByTask, statusTask, billAttachmentsTask); + + var projects = projectTask.Result; + var expenseTypes = expenseTypeTask.Result; + var paymentModes = paymentModeTask.Result; + var statusMappings = statusMappingTask.Result; + var paidBys = paidByTask.Result; + var createdBys = createdByTask.Result; + var reviewedBys = reviewedByTask.Result; + var approvedBys = approvedByTask.Result; + var processedBy = processedByTask.Result; + var billAttachments = billAttachmentsTask.Result; + + expenseList = model.Select(m => + { + var response = _mapper.Map(m); + + response.Project = projects.Where(p => p.Id == m.ProjectId).Select(p => _mapper.Map(p)).FirstOrDefault() ?? new ProjectBasicMongoDB(); + response.PaidBy = paidBys.Where(p => p.Id == m.PaidById).Select(p => _mapper.Map(p)).FirstOrDefault() ?? new BasicEmployeeMongoDB(); + response.CreatedBy = createdBys.Where(e => e.Id == m.CreatedById).Select(e => _mapper.Map(e)).FirstOrDefault() ?? new BasicEmployeeMongoDB(); + response.ReviewedBy = reviewedBys.Where(e => e.Id == m.CreatedById).Select(e => _mapper.Map(e)).FirstOrDefault() ?? new BasicEmployeeMongoDB(); + response.ApprovedBy = approvedBys.Where(e => e.Id == m.CreatedById).Select(e => _mapper.Map(e)).FirstOrDefault() ?? new BasicEmployeeMongoDB(); + response.ProcessedBy = processedBy.Where(e => e.Id == m.CreatedById).Select(e => _mapper.Map(e)).FirstOrDefault() ?? new BasicEmployeeMongoDB(); + response.Status = statusMappings.Where(s => s.StatusId == m.StatusId).Select(s => _mapper.Map(s.Status)).FirstOrDefault() ?? new ExpensesStatusMasterMongoDB(); + if (response.Status.Id == string.Empty) + { + var status = statusTask.Result; + response.Status = status.Where(s => s.Id == m.StatusId).Select(s => _mapper.Map(s)).FirstOrDefault() ?? new ExpensesStatusMasterMongoDB(); + } + + response.NextStatus = statusMappings.Where(s => s.StatusId == m.StatusId).Select(s => _mapper.Map>(s.NextStatus)).FirstOrDefault() ?? new List(); + response.PaymentMode = paymentModes.Where(pm => pm.Id == m.PaymentModeId).Select(pm => _mapper.Map(pm)).FirstOrDefault() ?? new PaymentModeMatserMongoDB(); + response.ExpensesType = expenseTypes.Where(et => et.Id == m.ExpensesTypeId).Select(et => _mapper.Map(et)).FirstOrDefault() ?? new ExpensesTypeMasterMongoDB(); + response.Documents = billAttachments.Where(ba => ba.ExpensesId == m.Id).Select(ba => ba.Documents).FirstOrDefault() ?? new List(); + return response; + }).ToList(); + + return expenseList; + } + private async Task 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(); + return await dbContext.ExpensesStatusMapping + .Include(s => s.Status) + .Include(s => s.NextStatus) + .AsNoTracking() + .Where(es => es.StatusId == model.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() + }).FirstOrDefaultAsync(); + }); + var statusTask = Task.Run(async () => + { + await using var dbContext = await _dbContextFactory.CreateDbContextAsync(); + return await dbContext.ExpensesStatusMaster + .AsNoTracking() + .FirstOrDefaultAsync(es => es.Id == model.StatusId); + }); + var billAttachmentsTask = Task.Run(async () => + { + await using var dbContext = await _dbContextFactory.CreateDbContextAsync(); + return await dbContext.BillAttachments + .Include(ba => ba.Document) + .AsNoTracking() + .Where(ba => ba.ExpensesId == model.Id && ba.Document != null) + .GroupBy(ba => ba.ExpensesId) + .Select(g => new + { + ExpensesId = g.Key, + Documents = g.Select(ba => new DocumentMongoDB + { + DocumentId = ba.Document!.Id.ToString(), + FileName = ba.Document.FileName, + ContentType = ba.Document.ContentType, + S3Key = ba.Document.S3Key, + ThumbS3Key = ba.Document.ThumbS3Key ?? ba.Document.S3Key + }).ToList() + }) + .FirstOrDefaultAsync(); + }); + + // Await all prerequisite checks at once. + 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(model); + + response.Project = _mapper.Map(project); + response.PaidBy = _mapper.Map(paidBy); + response.CreatedBy = _mapper.Map(createdBy); + response.ReviewedBy = _mapper.Map(reviewedBy); + response.ApprovedBy = _mapper.Map(approvedBy); + response.ProcessedBy = _mapper.Map(processedBy); + if (statusMapping != null) + { + response.Status = _mapper.Map(statusMapping.Status); + response.NextStatus = _mapper.Map>(statusMapping.NextStatus); + } + if (response.Status == null) + { + var status = statusTask.Result; + response.Status = _mapper.Map(status); + } + response.PaymentMode = _mapper.Map(paymentMode); + response.ExpensesType = _mapper.Map(expenseType); + if (billAttachment != null) response.Documents = billAttachment.Documents; + + return response; + + } + + #endregion } } diff --git a/Marco.Pms.Services/MappingProfiles/MappingProfile.cs b/Marco.Pms.Services/MappingProfiles/MappingProfile.cs index 7be54f2..a4c58b3 100644 --- a/Marco.Pms.Services/MappingProfiles/MappingProfile.cs +++ b/Marco.Pms.Services/MappingProfiles/MappingProfile.cs @@ -130,33 +130,6 @@ namespace Marco.Pms.Services.MappingProfiles 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.ReviewedById, - opt => opt.MapFrom(src => src.ReviewedById.ToString())) - .ForMember( - dest => dest.ApprovedById, - opt => opt.MapFrom(src => src.ApprovedById.ToString())) - .ForMember( - dest => dest.ProcessedById, - opt => opt.MapFrom(src => src.ProcessedById.ToString())) - .ForMember( - dest => dest.StatusId, - opt => opt.MapFrom(src => src.StatusId.ToString())) - .ForMember( dest => dest.TenantId, opt => opt.MapFrom(src => src.TenantId.ToString())); @@ -165,33 +138,6 @@ namespace Marco.Pms.Services.MappingProfiles 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.ReviewedById, - opt => opt.MapFrom(src => src.ReviewedById != null ? Guid.Parse(src.ReviewedById) : Guid.Empty)) - .ForMember( - dest => dest.ApprovedById, - opt => opt.MapFrom(src => src.ApprovedById != null ? Guid.Parse(src.ApprovedById) : Guid.Empty)) - .ForMember( - dest => dest.ProcessedById, - opt => opt.MapFrom(src => src.ProcessedById != null ? Guid.Parse(src.ProcessedById) : Guid.Empty)) - .ForMember( - dest => dest.StatusId, - opt => opt.MapFrom(src => Guid.Parse(src.StatusId))) - .ForMember( dest => dest.TenantId, opt => opt.MapFrom(src => Guid.Parse(src.TenantId))); diff --git a/Marco.Pms.Services/Service/ExpensesService.cs b/Marco.Pms.Services/Service/ExpensesService.cs index 5458486..871059a 100644 --- a/Marco.Pms.Services/Service/ExpensesService.cs +++ b/Marco.Pms.Services/Service/ExpensesService.cs @@ -5,7 +5,6 @@ using Marco.Pms.Model.Dtos.Expenses; using Marco.Pms.Model.Employees; using Marco.Pms.Model.Entitlements; using Marco.Pms.Model.Expenses; -using Marco.Pms.Model.MongoDBModels.Expenses; using Marco.Pms.Model.MongoDBModels.Utility; using Marco.Pms.Model.Utilities; using Marco.Pms.Model.ViewModels.Activities; @@ -70,7 +69,7 @@ namespace Marco.Pms.Services.Service /// The number of records to return per page. /// The page number to retrieve. /// A paginated list of expenses. - public async Task> GetExpensesListAsync(Employee loggedInEmployee, Guid tenantId, string? filter, int pageSize, int pageNumber) + public async Task> GetExpensesListAsync(Employee loggedInEmployee, Guid tenantId, string? searchString, string? filter, int pageSize, int pageNumber) { try { @@ -115,10 +114,10 @@ namespace Marco.Pms.Services.Service // 2. --- Deserialize Filter and Apply --- ExpensesFilter? expenseFilter = TryDeserializeFilter(filter); - var (totalPages, totalCount, expenseList) = await _cache.GetExpenseListAsync(tenantId, loggedInEmployeeId, hasViewAllPermissionTask.Result, hasViewSelfPermissionTask.Result, - pageNumber, pageSize, expenseFilter); + var (totalPages, totalCount, cacheList) = await _cache.GetExpenseListAsync(tenantId, loggedInEmployeeId, hasViewAllPermissionTask.Result, hasViewSelfPermissionTask.Result, + pageNumber, pageSize, expenseFilter, searchString); - if (expenseList == null) + if (cacheList == null) { // 3. --- Build Base Query and Apply Permissions --- @@ -126,7 +125,7 @@ namespace Marco.Pms.Services.Service var expensesQuery = _context.Expenses .Where(e => e.TenantId == tenantId); // Always filter by TenantId first. - await _cache.AddExpensesListToCache(expenses: await expensesQuery.ToListAsync()); + await _cache.AddExpensesListToCache(expenses: await expensesQuery.ToListAsync(), tenantId); // Apply permission-based filtering BEFORE any other filters or pagination. @@ -174,6 +173,16 @@ namespace Marco.Pms.Services.Service } } + if (!string.IsNullOrWhiteSpace(searchString)) + { + var searchStringLower = searchString.ToLower(); + expensesQuery = expensesQuery.Include(e => e.PaidBy).Include(e => e.CreatedBy) + .Where(e => e.Description.ToLower().Contains(searchStringLower) || + (e.TransactionId != null && e.TransactionId.ToLower().Contains(searchStringLower)) || + (e.PaidBy != null && (e.PaidBy.FirstName + " " + e.PaidBy.LastName).ToLower().Contains(searchStringLower)) || + (e.CreatedBy != null && (e.CreatedBy.FirstName + " " + e.CreatedBy.LastName).ToLower().Contains(searchStringLower))); + } + // 4. --- Apply Ordering and Pagination --- // This should be the last step before executing the query. @@ -199,20 +208,40 @@ namespace Marco.Pms.Services.Service } else { - expenseVM = await GetAllExpnesRelatedTables(_mapper.Map>(expenseList), tenantId); + var permissionStatusMapping = await _context.StatusPermissionMapping + .GroupBy(ps => ps.StatusId) + .Select(g => new + { + StatusId = g.Key, + PermissionIds = g.Select(ps => ps.PermissionId).ToList() + }).ToListAsync(); + + expenseVM = cacheList.Select(m => + { + var response = _mapper.Map(m); + if (response.Status != null && (response.NextStatus?.Any() ?? false)) + { + response.Status.PermissionIds = permissionStatusMapping.Where(ps => ps.StatusId == Guid.Parse(m.Status.Id)).Select(ps => ps.PermissionIds).FirstOrDefault(); + foreach (var status in response.NextStatus) + { + status.PermissionIds = permissionStatusMapping.Where(ps => ps.StatusId == status.Id).Select(ps => ps.PermissionIds).FirstOrDefault(); + } + } + return response; + }).ToList(); totalEntites = (int)totalCount; } // 7. --- Return Final Success Response --- var message = $"{expenseVM.Count} expense records fetched successfully."; _logger.LogInfo(message); - var defaultFilter = await GetObjectForfilter(tenantId); + + var response = new { CurrentFilter = expenseFilter, CurrentPage = pageNumber, TotalPages = totalPages, TotalEntites = totalEntites, - DefaultFilter = defaultFilter, Data = expenseVM, }; return ApiResponse.SuccessResponse(response, message, 200); @@ -242,7 +271,50 @@ namespace Marco.Pms.Services.Service return ApiResponse.ErrorResponse("Expense Not Found", "Expense Not Found", 404); } } - var vm = await GetAllExpnesRelatedTablesFromMongoDB(expenseDetails, tenantId); + var vm = _mapper.Map(expenseDetails); + + var permissionStatusMappingTask = Task.Run(async () => + { + await using var dbContext = await _dbContextFactory.CreateDbContextAsync(); + return await dbContext.StatusPermissionMapping + .GroupBy(ps => ps.StatusId) + .Select(g => new + { + StatusId = g.Key, + PermissionIds = g.Select(ps => ps.PermissionId).ToList() + }).ToListAsync(); + }); + var expenseReimburseTask = Task.Run(async () => + { + await using var dbContext = await _dbContextFactory.CreateDbContextAsync(); + return await dbContext.ExpensesReimburseMapping + .Include(er => er.ExpensesReimburse) + .ThenInclude(er => er!.ReimburseBy) + .ThenInclude(e => e!.JobRole) + .Where(er => er.TenantId == tenantId && er.ExpensesId == vm.Id) + .Select(er => er.ExpensesReimburse).FirstOrDefaultAsync(); + }); + var permissionStatusMappings = permissionStatusMappingTask.Result; + var expensesReimburse = expenseReimburseTask.Result; + + if (vm.Status != null && (vm.NextStatus?.Any() ?? false)) + { + vm.Status.PermissionIds = permissionStatusMappings.Where(ps => ps.StatusId == vm.Status.Id).Select(ps => ps.PermissionIds).FirstOrDefault(); + foreach (var status in vm.NextStatus) + { + status.PermissionIds = permissionStatusMappings.Where(ps => ps.StatusId == status.Id).Select(ps => ps.PermissionIds).FirstOrDefault(); + } + } + vm.ExpensesReimburse = _mapper.Map(expensesReimburse); + + foreach (var document in expenseDetails.Documents) + { + var response = vm.Documents.FirstOrDefault(d => d.DocumentId == Guid.Parse(document.DocumentId)); + + response!.PreSignedUrl = _s3Service.GeneratePreSignedUrl(document.S3Key); + response!.ThumbPreSignedUrl = _s3Service.GeneratePreSignedUrl(document.ThumbS3Key); + } + _logger.LogInfo("Employee {EmployeeId} successfully fetched expense details with ID {ExpenseId}", loggedInEmployee.Id, vm.Id); return ApiResponse.SuccessResponse(vm, "Successfully fetched the details of expense", 200); @@ -268,6 +340,81 @@ namespace Marco.Pms.Services.Service return ApiResponse.ErrorResponse("Databsae Exception", ExceptionMapper(dbEx), 500); } } + public async Task> GetFilterObjectAsync(Employee loggedInEmployee, Guid tenantId) + { + try + { + using var scope = _serviceScopeFactory.CreateScope(); + var projectHelper = scope.ServiceProvider.GetRequiredService(); + var projectIds = await projectHelper.GetMyProjectIdsAsync(tenantId, loggedInEmployee); + + // 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 && projectIds.Contains(e.ProjectId)) + .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. + return ApiResponse.SuccessResponse(new + { + Projects = await projectsTask, + PaidBy = await paidByTask, + CreatedBy = await createdByTask, + Status = await statusTask + }, "Successfully fetched the filter list", 200); + } + catch (Exception ex) + { + _logger.LogError(ex, "Exception occured while fetching the list filters for expenses"); + return ApiResponse.ErrorResponse("Internal Exception Occured", ExceptionMapper(ex), 500); + } + } #endregion @@ -1038,217 +1185,7 @@ namespace Marco.Pms.Services.Service return expenseList; } - private async Task GetAllExpnesRelatedTablesFromMongoDB(ExpenseDetailsMongoDB model, Guid tenantId) - { - var reviewedById = model.ReviewedById != null ? Guid.Parse(model.ReviewedById) : Guid.Empty; - var approvedById = model.ApprovedById != null ? Guid.Parse(model.ApprovedById) : Guid.Empty; - var processedById = model.ProcessedById != null ? Guid.Parse(model.ProcessedById) : Guid.Empty; - var projectTask = Task.Run(async () => - { - await using var dbContext = await _dbContextFactory.CreateDbContextAsync(); - return await dbContext.Projects.AsNoTracking().FirstOrDefaultAsync(p => p.Id == Guid.Parse(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 == Guid.Parse(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 == Guid.Parse(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 == 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 == 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 == 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 == Guid.Parse(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 == Guid.Parse(model.PaymentModeId) && pm.TenantId == tenantId); - }); - 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 => es.StatusId == Guid.Parse(model.StatusId) && es.Status != null) - .GroupBy(s => s.StatusId) - .Select(g => new - { - Status = g.Select(s => s.Status).FirstOrDefault(), - NextStatus = g.Select(s => s.NextStatus).ToList() - }).FirstOrDefaultAsync(); - }); - var statusTask = Task.Run(async () => - { - await using var dbContext = await _dbContextFactory.CreateDbContextAsync(); - return await dbContext.ExpensesStatusMaster - .AsNoTracking() - .FirstOrDefaultAsync(es => es.Id == Guid.Parse(model.StatusId)); - }); - var permissionStatusMappingTask = Task.Run(async () => - { - await using var dbContext = await _dbContextFactory.CreateDbContextAsync(); - return await dbContext.StatusPermissionMapping - .GroupBy(ps => ps.StatusId) - .Select(g => new - { - StatusId = g.Key, - PermissionIds = g.Select(ps => ps.PermissionId).ToList() - }).ToListAsync(); - }); - var expenseReimburseTask = Task.Run(async () => - { - await using var dbContext = await _dbContextFactory.CreateDbContextAsync(); - return await dbContext.ExpensesReimburseMapping - .Include(er => er.ExpensesReimburse) - .ThenInclude(er => er!.ReimburseBy) - .ThenInclude(e => e!.JobRole) - .Where(er => er.TenantId == tenantId && er.ExpensesId == Guid.Parse(model.Id)) - .Select(er => er.ExpensesReimburse).FirstOrDefaultAsync(); - }); - - // Await all prerequisite checks at once. - await Task.WhenAll(projectTask, expenseTypeTask, paymentModeTask, statusMappingTask, paidByTask, createdByTask, reviewedByTask, approvedByTask, processedByTask, - statusTask, permissionStatusMappingTask, expenseReimburseTask); - - var project = projectTask.Result; - var expenseType = expenseTypeTask.Result; - var paymentMode = paymentModeTask.Result; - var statusMapping = statusMappingTask.Result; - var permissionStatusMappings = permissionStatusMappingTask.Result; - var paidBy = paidByTask.Result; - var createdBy = createdByTask.Result; - var reviewedBy = reviewedByTask.Result; - var approvedBy = approvedByTask.Result; - var processedBy = processedByTask.Result; - var expensesReimburse = expenseReimburseTask.Result; - - var response = _mapper.Map(model); - - response.Project = _mapper.Map(project); - response.PaidBy = _mapper.Map(paidBy); - response.CreatedBy = _mapper.Map(createdBy); - if (reviewedBy != null) response.ReviewedBy = _mapper.Map(reviewedBy); - if (approvedBy != null) response.ApprovedBy = _mapper.Map(approvedBy); - if (processedBy != null) response.ProcessedBy = _mapper.Map(processedBy); - response.PaymentMode = _mapper.Map(paymentMode); - response.ExpensesType = _mapper.Map(expenseType); - response.ExpensesReimburse = _mapper.Map(expensesReimburse); - if (statusMapping != null) - { - response.Status = _mapper.Map(statusMapping.Status); - - response.NextStatus = _mapper.Map>(statusMapping.NextStatus); - if (response.NextStatus != null) - { - foreach (var status in response.NextStatus) - { - status.PermissionIds = permissionStatusMappings.Where(ps => ps.StatusId == status.Id).Select(ps => ps.PermissionIds).FirstOrDefault(); - } - } - } - if (response.Status == null) - { - var status = statusTask.Result; - response.Status = _mapper.Map(status); - } - response.Status.PermissionIds = permissionStatusMappings.Where(ps => ps.StatusId == Guid.Parse(model.StatusId)).Select(ps => ps.PermissionIds).FirstOrDefault(); - - foreach (var document in model.Documents) - { - var vm = response.Documents.FirstOrDefault(d => d.DocumentId == Guid.Parse(document.DocumentId)); - - vm!.PreSignedUrl = _s3Service.GeneratePreSignedUrl(document.S3Key); - vm!.ThumbPreSignedUrl = _s3Service.GeneratePreSignedUrl(document.ThumbS3Key); - } - - return response; - } - - private async Task GetObjectForfilter(Guid tenantId) - { - // 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. - return new - { - Projects = await projectsTask, - PaidBy = await paidByTask, - CreatedBy = await createdByTask, - Status = await statusTask - }; - } /// /// Deserializes the filter string, handling multiple potential formats (e.g., direct JSON vs. escaped JSON string). diff --git a/Marco.Pms.Services/Service/ServiceInterfaces/IExpensesService.cs b/Marco.Pms.Services/Service/ServiceInterfaces/IExpensesService.cs index 673c26c..5d84eab 100644 --- a/Marco.Pms.Services/Service/ServiceInterfaces/IExpensesService.cs +++ b/Marco.Pms.Services/Service/ServiceInterfaces/IExpensesService.cs @@ -6,9 +6,10 @@ namespace Marco.Pms.Services.Service.ServiceInterfaces { public interface IExpensesService { - Task> GetExpensesListAsync(Employee loggedInEmployee, Guid tenantId, string? filter, int pageSize, int pageNumber); + Task> GetExpensesListAsync(Employee loggedInEmployee, Guid tenantId, string? searchString, string? filter, int pageSize, int pageNumber); Task> GetExpenseDetailsAsync(Guid id, Employee loggedInEmployee, Guid tenantId); Task> GetSupplerNameListAsync(Employee loggedInEmployee, Guid tenantId); + Task> GetFilterObjectAsync(Employee loggedInEmployee, Guid tenantId); Task> CreateExpenseAsync(CreateExpensesDto dto, Employee loggedInEmployee, Guid tenantId); Task> ChangeStatusAsync(ExpenseRecordDto model, Employee loggedInEmployee, Guid tenantId); Task> UpdateExpanseAsync(Guid id, UpdateExpensesDto model, Employee loggedInEmployee, Guid tenantId);