From bd4bd90e7b67373cfe779537bbd97e19ed7917c7 Mon Sep 17 00:00:00 2001 From: "ashutosh.nehete" Date: Mon, 24 Nov 2025 12:49:54 +0530 Subject: [PATCH] Added the dynamic filter in collection and expense controller --- Marco.Pms.Model/Filters/AdvanceFilter.cs | 2 + Marco.Pms.Model/Filters/DateDynamicFilter.cs | 9 ++ Marco.Pms.Model/Filters/ListDynamicFilter.cs | 8 ++ .../Controllers/CollectionController.cs | 19 +++- .../Extensions/QueryableExtensions.cs | 72 +++++++++++++- Marco.Pms.Services/Service/ExpensesService.cs | 94 ++++++++++++------- 6 files changed, 162 insertions(+), 42 deletions(-) create mode 100644 Marco.Pms.Model/Filters/DateDynamicFilter.cs create mode 100644 Marco.Pms.Model/Filters/ListDynamicFilter.cs diff --git a/Marco.Pms.Model/Filters/AdvanceFilter.cs b/Marco.Pms.Model/Filters/AdvanceFilter.cs index 3b376a4..297318f 100644 --- a/Marco.Pms.Model/Filters/AdvanceFilter.cs +++ b/Marco.Pms.Model/Filters/AdvanceFilter.cs @@ -3,6 +3,8 @@ public class AdvanceFilter { // The dynamic filters from your JSON + public DateDynamicFilter? DateFilter { get; set; } + public List? Filters { get; set; } public List? SortFilters { get; set; } public List? SearchFilters { get; set; } public List? AdvanceFilters { get; set; } diff --git a/Marco.Pms.Model/Filters/DateDynamicFilter.cs b/Marco.Pms.Model/Filters/DateDynamicFilter.cs new file mode 100644 index 0000000..46630c9 --- /dev/null +++ b/Marco.Pms.Model/Filters/DateDynamicFilter.cs @@ -0,0 +1,9 @@ +namespace Marco.Pms.Model.Filters +{ + public class DateDynamicFilter + { + public string Column { get; set; } = string.Empty; + public DateTime StartValue { get; set; } + public DateTime EndValue { get; set; } + } +} diff --git a/Marco.Pms.Model/Filters/ListDynamicFilter.cs b/Marco.Pms.Model/Filters/ListDynamicFilter.cs new file mode 100644 index 0000000..bae0b96 --- /dev/null +++ b/Marco.Pms.Model/Filters/ListDynamicFilter.cs @@ -0,0 +1,8 @@ +namespace Marco.Pms.Model.Filters +{ + public class ListDynamicFilter + { + public string Column { get; set; } = string.Empty; + public List Values { get; set; } = new List(); + } +} diff --git a/Marco.Pms.Services/Controllers/CollectionController.cs b/Marco.Pms.Services/Controllers/CollectionController.cs index 61b18b4..0bc4656 100644 --- a/Marco.Pms.Services/Controllers/CollectionController.cs +++ b/Marco.Pms.Services/Controllers/CollectionController.cs @@ -168,12 +168,23 @@ namespace Marco.Pms.Services.Controllers query = query.ApplyCustomFilters(advanceFilter, "InvoiceDate"); - if (advanceFilter?.SearchFilters != null) + if (advanceFilter != null) { - var invoiceSearchFilter = advanceFilter.SearchFilters.Where(f => f.Column != "ProjectName").ToList(); - if (invoiceSearchFilter.Any()) + if (advanceFilter.Filters != null) { - query = query.ApplySearchFilters(invoiceSearchFilter); + query = query.ApplyListFilters(advanceFilter.Filters); + } + if (advanceFilter.DateFilter != null) + { + query = query.ApplyDateFilter(advanceFilter.DateFilter); + } + if (advanceFilter.SearchFilters != null) + { + var invoiceSearchFilter = advanceFilter.SearchFilters.Where(f => f.Column != "ProjectName").ToList(); + if (invoiceSearchFilter.Any()) + { + query = query.ApplySearchFilters(invoiceSearchFilter); + } } } diff --git a/Marco.Pms.Services/Extensions/QueryableExtensions.cs b/Marco.Pms.Services/Extensions/QueryableExtensions.cs index f0e8277..865203a 100644 --- a/Marco.Pms.Services/Extensions/QueryableExtensions.cs +++ b/Marco.Pms.Services/Extensions/QueryableExtensions.cs @@ -1,4 +1,5 @@ using Marco.Pms.Model.Filters; +using System.Data; using System.Linq.Dynamic.Core; namespace Marco.Pms.Services.Extensions @@ -69,9 +70,16 @@ namespace Marco.Pms.Services.Extensions return query; } + /// + /// Applies search filters to the given IQueryable. + /// + /// The type of the elements in the IQueryable. + /// The IQueryable to apply the filters to. + /// The list of search filters to apply. + /// The filtered IQueryable. public static IQueryable ApplySearchFilters(this IQueryable query, List searchFilters) { - // 1. Apply Search Filters (Contains/Text search) + // Apply search filters to the query if (searchFilters.Any()) { foreach (var search in searchFilters) @@ -86,10 +94,70 @@ namespace Marco.Pms.Services.Extensions return query; } + /// + /// Applies group by filters to the given IQueryable. + /// + /// The type of the elements in the IQueryable. + /// The IQueryable to apply the filters to. + /// The column to group by. + /// The grouped IQueryable. public static IQueryable ApplyGroupByFilters(this IQueryable query, string groupByColumn) { + // Group the query by the specified column and reshape the result to { Key: "Value", Items: [...] } query.GroupBy(groupByColumn, "it") - .Select("new (Key, it as Items)"); // Reshape to { Key: "Value", Items: [...] } + .Select("new (Key, it as Items)"); + return query; + } + + /// + /// Applies list filters to the given IQueryable. + /// + /// The type of the elements in the IQueryable. + /// The IQueryable to apply the filters to. + /// The list of filters to apply. + /// The filtered IQueryable. + public static IQueryable ApplyListFilters(this IQueryable query, List filters) + { + // Check if there are any filters + if (filters == null || !filters.Any()) return query; + + // Apply filters to the query + foreach (var filter in filters) + { + // Skip if column is empty or values are null or empty + if (string.IsNullOrWhiteSpace(filter.Column) || filter.Values == null || !filter.Values.Any()) continue; + + // Apply filter to the query + query = query.Where($"@0.Contains({filter.Column})", filter.Values); + } + + // Return the filtered query + return query; + } + + /// + /// Applies date filters to the given IQueryable. + /// + /// The type of the elements in the IQueryable. + /// The IQueryable to apply the filters to. + /// The date filter to apply. + /// The filtered IQueryable. + public static IQueryable ApplyDateFilter(this IQueryable query, DateDynamicFilter dateFilter) + { + // Check if date filter is null or column is empty + if (dateFilter == null || string.IsNullOrWhiteSpace(dateFilter.Column)) return query; + + // Convert start and end values to date + var startValue = dateFilter.StartValue.Date; + var endValue = dateFilter.EndValue.Date.AddDays(1); + + // Apply a filter to include items with a date greater than or equal to the start value + query = query.Where($"{dateFilter.Column} >= @0", startValue); + + // Apply a filter to include items with a date less than or equal to the end value + query = query.Where($"{dateFilter.Column} < @0", endValue); + + // Return the filtered IQueryable return query; } } diff --git a/Marco.Pms.Services/Service/ExpensesService.cs b/Marco.Pms.Services/Service/ExpensesService.cs index d157f5b..063c409 100644 --- a/Marco.Pms.Services/Service/ExpensesService.cs +++ b/Marco.Pms.Services/Service/ExpensesService.cs @@ -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; @@ -128,7 +129,7 @@ namespace Marco.Pms.Services.Service // 2. --- Deserialize Filter and Apply --- - ExpensesFilter? expenseFilter = TryDeserializeFilter(filter); + AdvanceFilter? advanceFilter = TryDeserializeAdvanceFilter(filter); //var (totalPages, totalCount, cacheList) = await _cache.GetExpenseListAsync(tenantId, loggedInEmployeeId, hasViewAllPermissionTask.Result, hasViewSelfPermissionTask.Result, // pageNumber, pageSize, expenseFilter, searchString); @@ -168,45 +169,28 @@ namespace Marco.Pms.Services.Service _logger.LogInfo("User {EmployeeId} has 'View Self' permission. Restricting query to their expenses.", loggedInEmployeeId); expensesQuery = expensesQuery.Where(e => e.CreatedById == loggedInEmployeeId); } - - if (expenseFilter != null) + expensesQuery = expensesQuery.ApplyCustomFilters(advanceFilter, "CreatedAt"); + if (advanceFilter != null) { - if (expenseFilter.StartDate.HasValue && expenseFilter.EndDate.HasValue) + if (advanceFilter.Filters != null) { - if (expenseFilter.IsTransactionDate) + 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.Where(e => e.TransactionDate.Date >= expenseFilter.StartDate.Value.Date && e.TransactionDate.Date <= expenseFilter.EndDate.Value.Date); + expensesQuery = expensesQuery.ApplySearchFilters(invoiceSearchFilter); } - else - { - expensesQuery = expensesQuery.Where(e => e.CreatedAt.Date >= expenseFilter.StartDate.Value.Date && e.CreatedAt.Date <= expenseFilter.EndDate.Value.Date); - } - } - - if (expenseFilter.ProjectIds?.Any() == true) + if (!string.IsNullOrWhiteSpace(advanceFilter.GroupByColumn)) { - expensesQuery = expensesQuery.Where(e => expenseFilter.ProjectIds.Contains(e.ProjectId)); - } - - if (expenseFilter.StatusIds?.Any() == true) - { - expensesQuery = expensesQuery.Where(e => expenseFilter.StatusIds.Contains(e.StatusId)); - } - - if (expenseFilter.PaidById?.Any() == true) - { - expensesQuery = expensesQuery.Where(e => expenseFilter.PaidById.Contains(e.PaidById)); - } - if (expenseFilter.ExpenseCategoryIds?.Any() == true) - { - expensesQuery = expensesQuery.Where(e => expenseFilter.ExpenseCategoryIds.Contains(e.ExpenseCategoryId)); - } - - // Only allow filtering by 'CreatedBy' if the user has 'View All' permission. - if (expenseFilter.CreatedByIds?.Any() == true && hasViewAllPermissionTask.Result) - { - expensesQuery = expensesQuery.Where(e => expenseFilter.CreatedByIds.Contains(e.CreatedById)); + expensesQuery = expensesQuery.ApplyGroupByFilters(advanceFilter.GroupByColumn); } } @@ -228,7 +212,7 @@ namespace Marco.Pms.Services.Service // 5. --- Execute Query and Map Results --- var expensesList = await expensesQuery - .OrderByDescending(e => e.CreatedAt) + //.OrderByDescending(e => e.CreatedAt) .Skip((pageNumber - 1) * pageSize) .Take(pageSize).ToListAsync(); @@ -302,7 +286,6 @@ namespace Marco.Pms.Services.Service var response = new { - CurrentFilter = expenseFilter, CurrentPage = pageNumber, TotalPages = totalPages, TotalEntites = totalEntites, @@ -3746,6 +3729,45 @@ 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(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(filter, options) ?? ""; + if (!string.IsNullOrWhiteSpace(unescapedJsonString)) + { + advanceFilter = JsonSerializer.Deserialize(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 HasPermissionAsync(Guid permission, Guid employeeId) { using var scope = _serviceScopeFactory.CreateScope();