Added an new API to filter expense by advance filter

This commit is contained in:
ashutosh.nehete 2025-12-08 10:02:59 +05:30
parent a0a65fc08c
commit 82c9f249ef
5 changed files with 335 additions and 12 deletions

View File

@ -3,7 +3,7 @@
public class AdvanceItem
{
public string Column { get; set; } = string.Empty;
public string Opration { get; set; } = string.Empty; // "greater than", "equal to", etc.
public string Operation { get; set; } = string.Empty; // "greater than", "equal to", etc.
public string Value { get; set; } = string.Empty;
}
}

View File

@ -46,6 +46,14 @@ namespace Marco.Pms.Services.Controllers
return StatusCode(response.StatusCode, response);
}
[HttpGet("list-dynamic")]
public async Task<IActionResult> GetExpensesListDynamic([FromQuery] string? searchString, [FromQuery] string? filter, [FromQuery] int pageSize = 20, [FromQuery] int pageNumber = 1)
{
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
var response = await _expensesService.GetExpensesListDynamicAsync(loggedInEmployee, tenantId, searchString, filter, pageSize, pageNumber);
return StatusCode(response.StatusCode, response);
}
[HttpGet("details/{id?}")]
public async Task<IActionResult> GetExpenseDetails(Guid? id, [FromQuery] string? expenseUId)
{

View File

@ -18,19 +18,62 @@ namespace Marco.Pms.Services.Extensions
{
if (string.IsNullOrWhiteSpace(advanceFilter.Column)) continue;
string op = advanceFilter.Opration.ToLower().Trim();
string op = advanceFilter.Operation.ToLower().Trim();
string predicate = "";
// Map your custom strings to Dynamic LINQ operators
switch (op)
{
case "greater than": predicate = $"{advanceFilter.Column} > @0"; break;
case "less than": predicate = $"{advanceFilter.Column} < @0"; break;
case "equal to": predicate = $"{advanceFilter.Column} == @0"; break;
case "not equal": predicate = $"{advanceFilter.Column} != @0"; break;
case "greater or equal": predicate = $"{advanceFilter.Column} >= @0"; break;
case "smaller or equal": predicate = $"{advanceFilter.Column} <= @0"; break;
default: continue;
// --- Equality ---
case "eq":
case "equal to":
predicate = $"{advanceFilter.Column} == @0";
break;
case "neq":
case "not equal":
predicate = $"{advanceFilter.Column} != @0";
break;
// --- Numeric / Date Comparison ---
case "gt":
case "greater than":
case "after": // Date specific
predicate = $"{advanceFilter.Column} > @0";
break;
case "gte":
case "greater or equal":
predicate = $"{advanceFilter.Column} >= @0";
break;
case "lt": // Added for consistency
case "less than":
case "before": // Date specific
predicate = $"{advanceFilter.Column} < @0";
break;
case "lte":
case "less or equal":
case "smaller or equal":
predicate = $"{advanceFilter.Column} <= @0";
break;
// --- Text Specific ---
case "contains":
predicate = $"{advanceFilter.Column}.Contains(@0)";
break;
case "starts":
predicate = $"{advanceFilter.Column}.StartsWith(@0)";
break;
case "ends":
predicate = $"{advanceFilter.Column}.EndsWith(@0)";
break;
default:
continue;
}
if (!string.IsNullOrEmpty(predicate))

View File

@ -21,6 +21,7 @@ using Marco.Pms.Model.ViewModels.Expenses;
using Marco.Pms.Model.ViewModels.Expenses.Masters;
using Marco.Pms.Model.ViewModels.Master;
using Marco.Pms.Model.ViewModels.Projects;
using Marco.Pms.Services.Extensions;
using Marco.Pms.Services.Helpers;
using Marco.Pms.Services.Service.ServiceInterfaces;
using MarcoBMS.Services.Service;
@ -38,7 +39,6 @@ namespace Marco.Pms.Services.Service
private readonly ILoggingService _logger;
private readonly S3UploadService _s3Service;
private readonly IServiceScopeFactory _serviceScopeFactory;
private readonly UtilityMongoDBHelper _updateLogHelper;
private readonly CacheUpdateHelper _cache;
private readonly IMapper _mapper;
@ -65,7 +65,6 @@ namespace Marco.Pms.Services.Service
IDbContextFactory<ApplicationDbContext> dbContextFactory,
ApplicationDbContext context,
IServiceScopeFactory serviceScopeFactory,
UtilityMongoDBHelper updateLogHelper,
CacheUpdateHelper cache,
ILoggingService logger,
S3UploadService s3Service,
@ -76,7 +75,6 @@ namespace Marco.Pms.Services.Service
_logger = logger;
_cache = cache;
_serviceScopeFactory = serviceScopeFactory;
_updateLogHelper = updateLogHelper;
_s3Service = s3Service;
_mapper = mapper;
}
@ -321,6 +319,219 @@ namespace Marco.Pms.Services.Service
return ApiResponse<object>.ErrorResponse("Error Occured", ExceptionMapper(ex), 500);
}
}
public async Task<ApiResponse<object>> GetExpensesListDynamicAsync(Employee loggedInEmployee, Guid tenantId, string? searchString, string? filter, int pageSize, int pageNumber)
{
try
{
_logger.LogInfo(
"Attempting to fetch expenses list for PageNumber: {PageNumber}, PageSize: {PageSize} with Filter: {Filter}",
pageNumber, pageSize, filter ?? "");
// 1. --- Get User Permissions ---
if (loggedInEmployee == null)
{
// This is an authentication/authorization issue. The user should be logged in.
_logger.LogWarning("Could not find an employee for the current logged-in user.");
return ApiResponse<object>.ErrorResponse("User not found or not authenticated.", 403);
}
Guid loggedInEmployeeId = loggedInEmployee.Id;
List<ExpenseList> expenseVM = new List<ExpenseList>();
var totalEntites = 0;
var hasViewSelfPermissionTask = HasPermissionAsync(PermissionsMaster.ExpenseViewSelf, loggedInEmployee.Id);
var hasViewAllPermissionTask = HasPermissionAsync(PermissionsMaster.ExpenseViewAll, loggedInEmployee.Id);
await Task.WhenAll(hasViewSelfPermissionTask, hasViewAllPermissionTask);
var hasViewAllPermission = hasViewAllPermissionTask.Result;
var hasViewSelfPermission = hasViewSelfPermissionTask.Result;
if (!hasViewAllPermission && !hasViewSelfPermission)
{
// User has neither required permission. Deny access.
_logger.LogWarning("Access DENIED for employee {EmployeeId} attempting to get expenses list.", loggedInEmployeeId);
return ApiResponse<object>.SuccessResponse(new List<ExpenseList>(), "You do not have permission to view any expenses.", 200);
}
// 2. --- Deserialize Filter and Apply ---
AdvanceFilter? advanceFilter = TryDeserializeAdvanceFilter(filter);
//var (totalPages, totalCount, cacheList) = await _cache.GetExpenseListAsync(tenantId, loggedInEmployeeId, hasViewAllPermissionTask.Result, hasViewSelfPermissionTask.Result,
// pageNumber, pageSize, expenseFilter, searchString);
List<ExpenseDetailsMongoDB>? cacheList = null;
var totalPages = 0;
var totalCount = 0;
// 3. --- Build Base Query and Apply Permissions ---
// Start with a base IQueryable. Filters will be chained onto this.
var expensesQuery = _context.Expenses
.Include(e => e.PaidBy)
.Include(e => e.CreatedBy)
.Include(e => e.ProcessedBy)
.Include(e => e.ApprovedBy)
.Include(e => e.ReviewedBy)
.Include(e => e.PaymentMode)
.Include(e => e.PaymentMode)
.Include(e => e.ExpenseCategory)
.Include(e => e.PaymentRequest)
.Include(e => e.Status)
.Include(e => e.Currency)
.Where(e => e.TenantId == tenantId); // Always filter by TenantId first.
if (cacheList == null)
{
//await _cache.AddExpensesListToCache(expenses: await expensesQuery.ToListAsync(), tenantId);
// Apply permission-based filtering BEFORE any other filters or pagination.
if (hasViewAllPermission)
{
expensesQuery = expensesQuery.Where(e => e.CreatedById == loggedInEmployeeId || e.StatusId != Draft);
}
else if (hasViewSelfPermission)
{
// User only has 'View Self' permission, so restrict the query to their own expenses.
_logger.LogInfo("User {EmployeeId} has 'View Self' permission. Restricting query to their expenses.", loggedInEmployeeId);
expensesQuery = expensesQuery.Where(e => e.CreatedById == loggedInEmployeeId);
}
expensesQuery = expensesQuery.ApplyCustomFilters(advanceFilter, "CreatedAt");
if (advanceFilter != null)
{
if (advanceFilter.Filters != null)
{
expensesQuery = expensesQuery.ApplyListFilters(advanceFilter.Filters);
}
if (advanceFilter.DateFilter != null)
{
expensesQuery = expensesQuery.ApplyDateFilter(advanceFilter.DateFilter);
}
if (advanceFilter.SearchFilters != null)
{
var invoiceSearchFilter = advanceFilter.SearchFilters.Where(f => f.Column != "ProjectName").ToList();
if (invoiceSearchFilter.Any())
{
expensesQuery = expensesQuery.ApplySearchFilters(invoiceSearchFilter);
}
}
if (!string.IsNullOrWhiteSpace(advanceFilter.GroupByColumn))
{
expensesQuery = expensesQuery.ApplyGroupByFilters(advanceFilter.GroupByColumn);
}
}
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)) ||
(e.UIDPrefix + "/" + e.UIDPostfix.ToString().PadLeft(5, '0')).Contains(searchString));
}
// 4. --- Apply Ordering and Pagination ---
// This should be the last step before executing the query.
totalEntites = await expensesQuery.CountAsync();
// 5. --- Execute Query and Map Results ---
var expensesList = await expensesQuery
//.OrderByDescending(e => e.CreatedAt)
.Skip((pageNumber - 1) * pageSize)
.Take(pageSize).ToListAsync();
if (!expensesList.Any())
{
_logger.LogInfo("No expenses found matching the criteria for employee {EmployeeId}.", loggedInEmployeeId);
return ApiResponse<object>.SuccessResponse(new List<ExpenseList>(), "No expenses found for the given criteria.", 200);
}
var projectIds = expensesList.Select(e => e.ProjectId).ToList();
var infraProjectTask = Task.Run(async () =>
{
await using var context = await _dbContextFactory.CreateDbContextAsync();
return await context.Projects.Where(p => projectIds.Contains(p.Id) && p.TenantId == tenantId).Select(p => _mapper.Map<BasicProjectVM>(p)).ToListAsync();
});
var serviceProjectTask = Task.Run(async () =>
{
await using var context = await _dbContextFactory.CreateDbContextAsync();
return await context.ServiceProjects.Where(sp => projectIds.Contains(sp.Id) && sp.TenantId == tenantId).Select(sp => _mapper.Map<BasicProjectVM>(sp)).ToListAsync();
});
await Task.WhenAll(infraProjectTask, serviceProjectTask);
var projects = infraProjectTask.Result;
projects.AddRange(serviceProjectTask.Result);
//expenseVM = await GetAllExpnesRelatedTables(expensesList, tenantId);
expenseVM = expensesList.Select(e =>
{
var result = _mapper.Map<ExpenseList>(e);
result.ExpenseUId = $"{e.UIDPrefix}/{e.UIDPostfix:D5}";
if (e.PaymentRequest != null)
result.PaymentRequestUID = $"{e.PaymentRequest.UIDPrefix}/{e.PaymentRequest.UIDPostfix:D5}";
result.Project = projects.FirstOrDefault(p => p.Id == e.ProjectId);
return result;
}).ToList();
totalPages = (int)Math.Ceiling((double)totalEntites / pageSize);
}
else
{
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 response = new
{
CurrentPage = pageNumber,
TotalPages = totalPages,
TotalEntites = totalEntites,
Data = expenseVM,
};
return ApiResponse<object>.SuccessResponse(response, message, 200);
}
catch (DbUpdateException dbEx)
{
_logger.LogError(dbEx, "Databsae Exception occured while fetching list expenses");
return ApiResponse<object>.ErrorResponse("Databsae Exception", ExceptionMapper(dbEx), 500);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error occured while fetching list expenses");
return ApiResponse<object>.ErrorResponse("Error Occured", ExceptionMapper(ex), 500);
}
}
public async Task<ApiResponse<object>> GetExpenseDetailsAsync(Guid? id, string? expenseUId, Employee loggedInEmployee, Guid tenantId)
{
try
@ -733,6 +944,7 @@ namespace Marco.Pms.Services.Service
{
using var scope = _serviceScopeFactory.CreateScope();
var _firebase = scope.ServiceProvider.GetRequiredService<IFirebaseService>();
var _updateLogHelper = scope.ServiceProvider.GetRequiredService<UtilityMongoDBHelper>();
// 1. Fetch Existing Expense with Related Entities (Single Query)
var expense = await _context.Expenses
@ -1061,6 +1273,8 @@ namespace Marco.Pms.Services.Service
return ApiResponse<object>.ErrorResponse("The employee Id in the path does not match the Id in the request body.",
"The employee Id in the path does not match the Id in the request body.", 400);
}
using var scope = _serviceScopeFactory.CreateScope();
var _updateLogHelper = scope.ServiceProvider.GetRequiredService<UtilityMongoDBHelper>();
// Check if the employee has the required permission
var hasManagePermission = await HasPermissionAsync(PermissionsMaster.ExpenseManage, loggedInEmployee.Id);
@ -1294,6 +1508,9 @@ namespace Marco.Pms.Services.Service
.Select(ba => ba.DocumentId)
.ToListAsync();
using var scope = _serviceScopeFactory.CreateScope();
var _updateLogHelper = scope.ServiceProvider.GetRequiredService<UtilityMongoDBHelper>();
var existingEntityBson = _updateLogHelper.EntityToBsonDocument(existingExpense);
_context.Expenses.Remove(existingExpense);
@ -2130,6 +2347,8 @@ namespace Marco.Pms.Services.Service
return ApiResponse<object>.ErrorResponse("You do not have permission for this action.", "Access Denied", 403);
}
var _updateLogHelper = scope.ServiceProvider.GetRequiredService<UtilityMongoDBHelper>();
// 5. Prepare for update (Audit snapshot)
var paymentRequestStateBeforeChange = _updateLogHelper.EntityToBsonDocument(paymentRequest);
@ -2599,6 +2818,9 @@ namespace Marco.Pms.Services.Service
bool isVariableRecurring = paymentRequest.RecurringPayment?.IsVariable ?? false;
using var scope = _serviceScopeFactory.CreateScope();
var _updateLogHelper = scope.ServiceProvider.GetRequiredService<UtilityMongoDBHelper>();
// Capture existing state for auditing
var existingEntityBson = _updateLogHelper.EntityToBsonDocument(paymentRequest);
@ -2778,6 +3000,9 @@ namespace Marco.Pms.Services.Service
.Select(ba => ba.DocumentId)
.ToListAsync();
using var scope = _serviceScopeFactory.CreateScope();
var _updateLogHelper = scope.ServiceProvider.GetRequiredService<UtilityMongoDBHelper>();
var existingEntityBson = _updateLogHelper.EntityToBsonDocument(paymentRequest);
paymentRequest.IsActive = false;
@ -3765,6 +3990,46 @@ namespace Marco.Pms.Services.Service
#endregion
#region =================================================================== Helper Functions ===================================================================
private AdvanceFilter? TryDeserializeAdvanceFilter(string? filter)
{
if (string.IsNullOrWhiteSpace(filter))
{
return null;
}
var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true };
AdvanceFilter? advanceFilter = null;
try
{
// First, try to deserialize directly. This is the expected case (e.g., from a web client).
advanceFilter = JsonSerializer.Deserialize<AdvanceFilter>(filter, options);
}
catch (JsonException ex)
{
_logger.LogError(ex, "[{MethodName}] Failed to directly deserialize filter. Attempting to unescape and re-parse. Filter: {Filter}", nameof(TryDeserializeFilter), filter);
// If direct deserialization fails, it might be an escaped string (common with tools like Postman or some mobile clients).
try
{
// Unescape the string first, then deserialize the result.
string unescapedJsonString = JsonSerializer.Deserialize<string>(filter, options) ?? "";
if (!string.IsNullOrWhiteSpace(unescapedJsonString))
{
advanceFilter = JsonSerializer.Deserialize<AdvanceFilter>(unescapedJsonString, options);
}
}
catch (JsonException ex1)
{
// If both attempts fail, log the final error and return null.
_logger.LogError(ex1, "[{MethodName}] All attempts to deserialize the filter failed. Filter will be ignored. Filter: {Filter}", nameof(TryDeserializeFilter), filter);
return null;
}
}
return advanceFilter;
}
private async Task<bool> HasPermissionAsync(Guid permission, Guid employeeId)
{
using var scope = _serviceScopeFactory.CreateScope();
@ -4135,6 +4400,9 @@ namespace Marco.Pms.Services.Service
private async Task DeleteAttachemnts(List<Guid> documentIds)
{
using var scope = _serviceScopeFactory.CreateScope();
var _updateLogHelper = scope.ServiceProvider.GetRequiredService<UtilityMongoDBHelper>();
var attachmentTask = Task.Run(async () =>
{
await using var context = await _dbContextFactory.CreateDbContextAsync();
@ -4176,6 +4444,9 @@ namespace Marco.Pms.Services.Service
}
private async Task DeletePaymentRequestAttachemnts(List<Guid> documentIds)
{
using var scope = _serviceScopeFactory.CreateScope();
var _updateLogHelper = scope.ServiceProvider.GetRequiredService<UtilityMongoDBHelper>();
var attachmentTask = Task.Run(async () =>
{
await using var context = await _dbContextFactory.CreateDbContextAsync();

View File

@ -8,6 +8,7 @@ namespace Marco.Pms.Services.Service.ServiceInterfaces
{
#region =================================================================== Expenses Functions ===================================================================
Task<ApiResponse<object>> GetExpensesListAsync(Employee loggedInEmployee, Guid tenantId, string? searchString, string? filter, int pageSize, int pageNumber);
Task<ApiResponse<object>> GetExpensesListDynamicAsync(Employee loggedInEmployee, Guid tenantId, string? searchString, string? filter, int pageSize, int pageNumber);
Task<ApiResponse<object>> GetExpenseDetailsAsync(Guid? id, string? expenseUId, Employee loggedInEmployee, Guid tenantId);
Task<ApiResponse<object>> GetSupplerNameListAsync(Employee loggedInEmployee, Guid tenantId);
Task<ApiResponse<object>> GetFilterObjectAsync(Employee loggedInEmployee, Guid tenantId);