Added the search funcationality abd chnaged the cache object

This commit is contained in:
ashutosh.nehete 2025-07-31 10:28:45 +05:30
parent 1a0641162c
commit 1c9008ca62
9 changed files with 530 additions and 386 deletions

View File

@ -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<ExpenseDetailsMongoDB> 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<ExpenseDetailsMongoDB>.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<FilterDefinition<ExpenseDetailsMongoDB>>
{
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);

View File

@ -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<ExpensesStatusMasterMongoDB> NextStatus { get; set; } = new List<ExpensesStatusMasterMongoDB>();
public bool PreApproved { get; set; } = false;
public string? TransactionId { get; set; }
public string Description { get; set; } = string.Empty;

View File

@ -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<string> PermissionIds { get; set; } = new List<string>();
public string? Color { get; set; }
public bool IsSystem { get; set; } = false;
}

View File

@ -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<ExpensesStatusMasterVM>? NextStatus { get; set; }

View File

@ -39,10 +39,10 @@ namespace Marco.Pms.Services.Controllers
/// <returns>A paginated list of expenses.</returns>
[HttpGet("list")]
public async Task<IActionResult> GetExpensesList(string? filter, int pageSize = 20, int pageNumber = 1)
public async Task<IActionResult> 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<IActionResult> GetFilterObject()
{
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
var response = await _expensesService.GetFilterObjectAsync(loggedInEmployee, tenantId);
return StatusCode(response.StatusCode, response);
}
[HttpPost("create")]
public async Task<IActionResult> 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);
}

View File

@ -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<ExpenseDetailsMongoDB>(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<ExpenseDetailsMongoDB?> 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)
{
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> expenses)
public async Task AddExpensesListToCache(List<Expenses> expenses, Guid tenantId)
{
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,
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<DocumentMongoDB>();
}
}
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<ExpenseDetailsMongoDB>? 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<List<ExpenseDetailsMongoDB>> GetAllExpnesRelatedTablesForList(List<Expenses> model, Guid tenantId)
{
List<ExpenseDetailsMongoDB> expenseList = new List<ExpenseDetailsMongoDB>();
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<ExpenseDetailsMongoDB>(m);
response.Project = projects.Where(p => p.Id == m.ProjectId).Select(p => _mapper.Map<ProjectBasicMongoDB>(p)).FirstOrDefault() ?? new ProjectBasicMongoDB();
response.PaidBy = paidBys.Where(p => p.Id == m.PaidById).Select(p => _mapper.Map<BasicEmployeeMongoDB>(p)).FirstOrDefault() ?? new BasicEmployeeMongoDB();
response.CreatedBy = createdBys.Where(e => e.Id == m.CreatedById).Select(e => _mapper.Map<BasicEmployeeMongoDB>(e)).FirstOrDefault() ?? new BasicEmployeeMongoDB();
response.ReviewedBy = reviewedBys.Where(e => e.Id == m.CreatedById).Select(e => _mapper.Map<BasicEmployeeMongoDB>(e)).FirstOrDefault() ?? new BasicEmployeeMongoDB();
response.ApprovedBy = approvedBys.Where(e => e.Id == m.CreatedById).Select(e => _mapper.Map<BasicEmployeeMongoDB>(e)).FirstOrDefault() ?? new BasicEmployeeMongoDB();
response.ProcessedBy = processedBy.Where(e => e.Id == m.CreatedById).Select(e => _mapper.Map<BasicEmployeeMongoDB>(e)).FirstOrDefault() ?? new BasicEmployeeMongoDB();
response.Status = statusMappings.Where(s => s.StatusId == m.StatusId).Select(s => _mapper.Map<ExpensesStatusMasterMongoDB>(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<ExpensesStatusMasterMongoDB>(s)).FirstOrDefault() ?? new ExpensesStatusMasterMongoDB();
}
response.NextStatus = statusMappings.Where(s => s.StatusId == m.StatusId).Select(s => _mapper.Map<List<ExpensesStatusMasterMongoDB>>(s.NextStatus)).FirstOrDefault() ?? new List<ExpensesStatusMasterMongoDB>();
response.PaymentMode = paymentModes.Where(pm => pm.Id == m.PaymentModeId).Select(pm => _mapper.Map<PaymentModeMatserMongoDB>(pm)).FirstOrDefault() ?? new PaymentModeMatserMongoDB();
response.ExpensesType = expenseTypes.Where(et => et.Id == m.ExpensesTypeId).Select(et => _mapper.Map<ExpensesTypeMasterMongoDB>(et)).FirstOrDefault() ?? new ExpensesTypeMasterMongoDB();
response.Documents = billAttachments.Where(ba => ba.ExpensesId == m.Id).Select(ba => ba.Documents).FirstOrDefault() ?? new List<DocumentMongoDB>();
return response;
}).ToList();
return expenseList;
}
private async Task<ExpenseDetailsMongoDB> GetAllExpnesRelatedTablesForSingle(Expenses model, Guid tenantId)
{
var projectTask = Task.Run(async () =>
{
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
return await dbContext.Projects.AsNoTracking().FirstOrDefaultAsync(p => p.Id == model.ProjectId && p.TenantId == tenantId);
});
var paidByTask = Task.Run(async () =>
{
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
return await dbContext.Employees.Include(e => e.JobRole).AsNoTracking().FirstOrDefaultAsync(e => e.Id == model.PaidById && e.TenantId == tenantId);
});
var createdByTask = Task.Run(async () =>
{
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
return await dbContext.Employees.Include(e => e.JobRole).AsNoTracking().FirstOrDefaultAsync(e => e.Id == model.CreatedById && e.TenantId == tenantId);
});
var reviewedByTask = Task.Run(async () =>
{
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
return await dbContext.Employees.Include(e => e.JobRole).AsNoTracking().FirstOrDefaultAsync(e => e.Id == model.ReviewedById && e.TenantId == tenantId);
});
var approvedByTask = Task.Run(async () =>
{
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
return await dbContext.Employees.Include(e => e.JobRole).AsNoTracking().FirstOrDefaultAsync(e => e.Id == model.ApprovedById && e.TenantId == tenantId);
});
var processedByTask = Task.Run(async () =>
{
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
return await dbContext.Employees.Include(e => e.JobRole).AsNoTracking().FirstOrDefaultAsync(e => e.Id == model.ProcessedById && e.TenantId == tenantId);
});
var expenseTypeTask = Task.Run(async () =>
{
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
return await dbContext.ExpensesTypeMaster.AsNoTracking().FirstOrDefaultAsync(et => et.Id == model.ExpensesTypeId && et.TenantId == tenantId);
});
var paymentModeTask = Task.Run(async () =>
{
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
return await dbContext.PaymentModeMatser.AsNoTracking().FirstOrDefaultAsync(pm => pm.Id == model.PaymentModeId && pm.TenantId == tenantId);
});
var statusMappingTask = Task.Run(async () =>
{
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
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<ExpenseDetailsMongoDB>(model);
response.Project = _mapper.Map<ProjectBasicMongoDB>(project);
response.PaidBy = _mapper.Map<BasicEmployeeMongoDB>(paidBy);
response.CreatedBy = _mapper.Map<BasicEmployeeMongoDB>(createdBy);
response.ReviewedBy = _mapper.Map<BasicEmployeeMongoDB>(reviewedBy);
response.ApprovedBy = _mapper.Map<BasicEmployeeMongoDB>(approvedBy);
response.ProcessedBy = _mapper.Map<BasicEmployeeMongoDB>(processedBy);
if (statusMapping != null)
{
response.Status = _mapper.Map<ExpensesStatusMasterMongoDB>(statusMapping.Status);
response.NextStatus = _mapper.Map<List<ExpensesStatusMasterMongoDB>>(statusMapping.NextStatus);
}
if (response.Status == null)
{
var status = statusTask.Result;
response.Status = _mapper.Map<ExpensesStatusMasterMongoDB>(status);
}
response.PaymentMode = _mapper.Map<PaymentModeMatserMongoDB>(paymentMode);
response.ExpensesType = _mapper.Map<ExpensesTypeMasterMongoDB>(expenseType);
if (billAttachment != null) response.Documents = billAttachment.Documents;
return response;
}
#endregion
}
}

View File

@ -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)));

View File

@ -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
/// <param name="pageSize">The number of records to return per page.</param>
/// <param name="pageNumber">The page number to retrieve.</param>
/// <returns>A paginated list of expenses.</returns>
public async Task<ApiResponse<object>> GetExpensesListAsync(Employee loggedInEmployee, Guid tenantId, string? filter, int pageSize, int pageNumber)
public async Task<ApiResponse<object>> 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<List<Expenses>>(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<ExpenseList>(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<object>.SuccessResponse(response, message, 200);
@ -242,7 +271,50 @@ namespace Marco.Pms.Services.Service
return ApiResponse<object>.ErrorResponse("Expense Not Found", "Expense Not Found", 404);
}
}
var vm = await GetAllExpnesRelatedTablesFromMongoDB(expenseDetails, tenantId);
var vm = _mapper.Map<ExpenseDetailsVM>(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<ExpensesReimburseVM>(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<object>.SuccessResponse(vm, "Successfully fetched the details of expense", 200);
@ -268,6 +340,81 @@ namespace Marco.Pms.Services.Service
return ApiResponse<object>.ErrorResponse("Databsae Exception", ExceptionMapper(dbEx), 500);
}
}
public async Task<ApiResponse<object>> GetFilterObjectAsync(Employee loggedInEmployee, Guid tenantId)
{
try
{
using var scope = _serviceScopeFactory.CreateScope();
var projectHelper = scope.ServiceProvider.GetRequiredService<IProjectServices>();
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<object>.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<object>.ErrorResponse("Internal Exception Occured", ExceptionMapper(ex), 500);
}
}
#endregion
@ -1038,217 +1185,7 @@ namespace Marco.Pms.Services.Service
return expenseList;
}
private async Task<ExpenseDetailsVM> 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<ExpenseDetailsVM>(model);
response.Project = _mapper.Map<ProjectInfoVM>(project);
response.PaidBy = _mapper.Map<BasicEmployeeVM>(paidBy);
response.CreatedBy = _mapper.Map<BasicEmployeeVM>(createdBy);
if (reviewedBy != null) response.ReviewedBy = _mapper.Map<BasicEmployeeVM>(reviewedBy);
if (approvedBy != null) response.ApprovedBy = _mapper.Map<BasicEmployeeVM>(approvedBy);
if (processedBy != null) response.ProcessedBy = _mapper.Map<BasicEmployeeVM>(processedBy);
response.PaymentMode = _mapper.Map<PaymentModeMatserVM>(paymentMode);
response.ExpensesType = _mapper.Map<ExpensesTypeMasterVM>(expenseType);
response.ExpensesReimburse = _mapper.Map<ExpensesReimburseVM>(expensesReimburse);
if (statusMapping != null)
{
response.Status = _mapper.Map<ExpensesStatusMasterVM>(statusMapping.Status);
response.NextStatus = _mapper.Map<List<ExpensesStatusMasterVM>>(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<ExpensesStatusMasterVM>(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<object> 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
};
}
/// <summary>
/// Deserializes the filter string, handling multiple potential formats (e.g., direct JSON vs. escaped JSON string).

View File

@ -6,9 +6,10 @@ namespace Marco.Pms.Services.Service.ServiceInterfaces
{
public interface IExpensesService
{
Task<ApiResponse<object>> GetExpensesListAsync(Employee loggedInEmployee, Guid tenantId, string? filter, int pageSize, int pageNumber);
Task<ApiResponse<object>> GetExpensesListAsync(Employee loggedInEmployee, Guid tenantId, string? searchString, string? filter, int pageSize, int pageNumber);
Task<ApiResponse<object>> GetExpenseDetailsAsync(Guid id, Employee loggedInEmployee, Guid tenantId);
Task<ApiResponse<object>> GetSupplerNameListAsync(Employee loggedInEmployee, Guid tenantId);
Task<ApiResponse<object>> GetFilterObjectAsync(Employee loggedInEmployee, Guid tenantId);
Task<ApiResponse<object>> CreateExpenseAsync(CreateExpensesDto dto, Employee loggedInEmployee, Guid tenantId);
Task<ApiResponse<object>> ChangeStatusAsync(ExpenseRecordDto model, Employee loggedInEmployee, Guid tenantId);
Task<ApiResponse<object>> UpdateExpanseAsync(Guid id, UpdateExpensesDto model, Employee loggedInEmployee, Guid tenantId);