Added an API to get list of purchase invoices wth filter
This commit is contained in:
parent
b6baff7d00
commit
886a32b3e3
@ -9,8 +9,8 @@ namespace Marco.Pms.Model.Dtos.Project
|
||||
public Guid WorkAreaID { get; set; }
|
||||
public Guid WorkCategoryId { get; set; }
|
||||
public Guid ActivityID { get; set; }
|
||||
public int PlannedWork { get; set; }
|
||||
public int CompletedWork { get; set; }
|
||||
public double PlannedWork { get; set; }
|
||||
public double CompletedWork { get; set; }
|
||||
public Guid? ParentTaskId { get; set; }
|
||||
public string? Comment { get; set; }
|
||||
}
|
||||
|
||||
@ -3,6 +3,8 @@
|
||||
public class AdvanceFilter
|
||||
{
|
||||
// The dynamic filters from your JSON
|
||||
public DateDynamicFilter? DateFilter { get; set; }
|
||||
public List<ListDynamicFilter>? Filters { get; set; }
|
||||
public List<SortItem>? SortFilters { get; set; }
|
||||
public List<SearchItem>? SearchFilters { get; set; }
|
||||
public List<AdvanceItem>? AdvanceFilters { get; set; }
|
||||
|
||||
9
Marco.Pms.Model/Filters/DateDynamicFilter.cs
Normal file
9
Marco.Pms.Model/Filters/DateDynamicFilter.cs
Normal file
@ -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; }
|
||||
}
|
||||
}
|
||||
8
Marco.Pms.Model/Filters/ListDynamicFilter.cs
Normal file
8
Marco.Pms.Model/Filters/ListDynamicFilter.cs
Normal file
@ -0,0 +1,8 @@
|
||||
namespace Marco.Pms.Model.Filters
|
||||
{
|
||||
public class ListDynamicFilter
|
||||
{
|
||||
public string Column { get; set; } = string.Empty;
|
||||
public List<Guid> Values { get; set; } = new List<Guid>();
|
||||
}
|
||||
}
|
||||
@ -11,21 +11,48 @@ namespace Marco.Pms.Services.Controllers
|
||||
{
|
||||
private readonly UserHelper _userHelper;
|
||||
private readonly IPurchaseInvoiceService _purchaseInvoiceService;
|
||||
private readonly ISignalRService _signalR;
|
||||
private readonly Guid tenantId;
|
||||
|
||||
public PurchaseInvoiceController(UserHelper userHelper, IPurchaseInvoiceService purchaseInvoiceService)
|
||||
public PurchaseInvoiceController(UserHelper userHelper, IPurchaseInvoiceService purchaseInvoiceService, ISignalRService signalR)
|
||||
{
|
||||
_userHelper = userHelper;
|
||||
_purchaseInvoiceService = purchaseInvoiceService;
|
||||
tenantId = _userHelper.GetTenantId();
|
||||
_signalR = signalR;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves a list of purchase invoices based on search string, filter, activity status, page size, and page number.
|
||||
/// </summary>
|
||||
/// <param name="searchString">Optional search string to filter invoices by.</param>
|
||||
/// <param name="filter">Optional filter to apply to the invoices.</param>
|
||||
/// <param name="isActive">Optional flag to filter invoices by activity status.</param>
|
||||
/// <param name="pageSize">The number of invoices to display per page.</param>
|
||||
/// <param name="pageNumber">The requested page number (1-based).</param>
|
||||
/// <param name="cancellationToken">Token to propagate notification that operations should be canceled.</param>
|
||||
/// <returns>A HTTP 200 OK response with a list of purchase invoices or an appropriate HTTP error code.</returns>
|
||||
[HttpGet("list")]
|
||||
public async Task<IActionResult> GetPurchaseInvoiceListAsync([FromQuery] string? searchString, [FromQuery] string? filter, CancellationToken cancellationToken, [FromQuery] bool isActive = true,
|
||||
[FromQuery] int pageSize = 20, [FromQuery] int pageNumber = 1)
|
||||
{
|
||||
// Get the currently logged-in employee
|
||||
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
|
||||
|
||||
// Retrieve the purchase invoice list using the service
|
||||
var response = await _purchaseInvoiceService.GetPurchaseInvoiceListAsync(searchString, filter, isActive, pageSize, pageNumber, loggedInEmployee, tenantId, cancellationToken);
|
||||
|
||||
// Return the response with the appropriate HTTP status code
|
||||
return StatusCode(response.StatusCode, response);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Creates a purchase invoice.
|
||||
/// </summary>
|
||||
/// <param name="model">The purchase invoice model.</param>
|
||||
/// <param name="ct">The cancellation token.</param>
|
||||
/// <returns>The HTTP response with the purchase invoice status and data.</returns>
|
||||
/// <returns>The HTTP response for the creation of the purchase invoice.</returns>
|
||||
[HttpPost("create")]
|
||||
public async Task<IActionResult> CreatePurchaseInvoice([FromBody] PurchaseInvoiceDto model, CancellationToken ct)
|
||||
{
|
||||
@ -35,7 +62,19 @@ namespace Marco.Pms.Services.Controllers
|
||||
// Create a purchase invoice using the purchase invoice service
|
||||
var response = await _purchaseInvoiceService.CreatePurchaseInvoiceAsync(model, loggedInEmployee, tenantId, ct);
|
||||
|
||||
// Return the HTTP response with the purchase invoice status and data
|
||||
// If the creation is successful, send a notification to the SignalR service
|
||||
if (response.Success)
|
||||
{
|
||||
var notification = new
|
||||
{
|
||||
LoggedInUserId = loggedInEmployee.Id,
|
||||
Keyword = "Purchase_Invoice",
|
||||
Response = response.Data
|
||||
};
|
||||
await _signalR.SendNotificationAsync(notification);
|
||||
}
|
||||
|
||||
// Return the HTTP response
|
||||
return StatusCode(response.StatusCode, response);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Applies search filters to the given IQueryable.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of the elements in the IQueryable.</typeparam>
|
||||
/// <param name="query">The IQueryable to apply the filters to.</param>
|
||||
/// <param name="searchFilters">The list of search filters to apply.</param>
|
||||
/// <returns>The filtered IQueryable.</returns>
|
||||
public static IQueryable<T> ApplySearchFilters<T>(this IQueryable<T> query, List<SearchItem> 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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Applies group by filters to the given IQueryable.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of the elements in the IQueryable.</typeparam>
|
||||
/// <param name="query">The IQueryable to apply the filters to.</param>
|
||||
/// <param name="groupByColumn">The column to group by.</param>
|
||||
/// <returns>The grouped IQueryable.</returns>
|
||||
public static IQueryable<T> ApplyGroupByFilters<T>(this IQueryable<T> 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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Applies list filters to the given IQueryable.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of the elements in the IQueryable.</typeparam>
|
||||
/// <param name="query">The IQueryable to apply the filters to.</param>
|
||||
/// <param name="filters">The list of filters to apply.</param>
|
||||
/// <returns>The filtered IQueryable.</returns>
|
||||
public static IQueryable<T> ApplyListFilters<T>(this IQueryable<T> query, List<ListDynamicFilter> 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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Applies date filters to the given IQueryable.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of the elements in the IQueryable.</typeparam>
|
||||
/// <param name="query">The IQueryable to apply the filters to.</param>
|
||||
/// <param name="dateFilter">The date filter to apply.</param>
|
||||
/// <returns>The filtered IQueryable.</returns>
|
||||
public static IQueryable<T> ApplyDateFilter<T>(this IQueryable<T> 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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -167,13 +167,9 @@ namespace Marco.Pms.Services.Service
|
||||
{
|
||||
try
|
||||
{
|
||||
// 1. INPUT SANITIZATION
|
||||
// Ensure valid pagination limits to prevent performance degradation or DoS attacks.
|
||||
int actualPage = pageNumber < 1 ? 1 : pageNumber;
|
||||
int actualSize = pageSize > 50 ? 50 : (pageSize < 1 ? 10 : pageSize);
|
||||
|
||||
// 1. VALIDATION
|
||||
_logger.LogInfo("Fetching Organization list. Page: {Page}, Size: {Size}, Search: {Search}, User: {UserId}",
|
||||
actualPage, actualSize, searchString ?? "<empty>", loggedInEmployee.Id);
|
||||
pageNumber, pageSize, searchString ?? "<empty>", loggedInEmployee.Id);
|
||||
|
||||
// 2. QUERY BUILDING
|
||||
// Use AsNoTracking() for read-only scenarios to reduce overhead.
|
||||
@ -202,22 +198,22 @@ namespace Marco.Pms.Services.Service
|
||||
// rather than fetching the whole Entity and discarding data in memory.
|
||||
var items = await query
|
||||
.OrderBy(o => o.Name)
|
||||
.Skip((actualPage - 1) * actualSize)
|
||||
.Take(actualSize)
|
||||
.Skip((pageNumber - 1) * pageSize)
|
||||
.Take(pageSize)
|
||||
.ProjectTo<BasicOrganizationVm>(_mapper.ConfigurationProvider) // Requires AutoMapper.QueryableExtensions
|
||||
.ToListAsync(ct);
|
||||
|
||||
// 7. PREPARE RESPONSE
|
||||
var totalPages = (int)Math.Ceiling((double)totalCount / actualSize);
|
||||
var totalPages = (int)Math.Ceiling((double)totalCount / pageSize);
|
||||
|
||||
var pagedResult = new
|
||||
{
|
||||
CurrentPage = actualPage,
|
||||
PageSize = actualSize,
|
||||
CurrentPage = pageNumber,
|
||||
PageSize = pageSize,
|
||||
TotalPages = totalPages,
|
||||
TotalCount = totalCount,
|
||||
HasPrevious = actualPage > 1,
|
||||
HasNext = actualPage < totalPages,
|
||||
HasPrevious = pageNumber > 1,
|
||||
HasNext = pageNumber < totalPages,
|
||||
Data = items
|
||||
};
|
||||
|
||||
|
||||
@ -1,17 +1,22 @@
|
||||
using AutoMapper;
|
||||
using Marco.Pms.DataAccess.Data;
|
||||
using Marco.Pms.Model.DocumentManager;
|
||||
using Marco.Pms.Model.Dtos.PurchaseInvoice;
|
||||
using Marco.Pms.Model.Employees;
|
||||
using Marco.Pms.Model.Filters;
|
||||
using Marco.Pms.Model.OrganizationModel;
|
||||
using Marco.Pms.Model.Projects;
|
||||
using Marco.Pms.Model.PurchaseInvoice;
|
||||
using Marco.Pms.Model.ServiceProject;
|
||||
using Marco.Pms.Model.Utilities;
|
||||
using Marco.Pms.Model.ViewModels.Organization;
|
||||
using Marco.Pms.Model.ViewModels.Projects;
|
||||
using Marco.Pms.Model.ViewModels.PurchaseInvoice;
|
||||
using Marco.Pms.Services.Extensions;
|
||||
using Marco.Pms.Services.Service.ServiceInterfaces;
|
||||
using MarcoBMS.Services.Service;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using System.Text.Json;
|
||||
using Document = Marco.Pms.Model.DocumentManager.Document;
|
||||
|
||||
namespace Marco.Pms.Services.Service
|
||||
{
|
||||
@ -37,6 +42,362 @@ namespace Marco.Pms.Services.Service
|
||||
_mapper = mapper;
|
||||
}
|
||||
|
||||
//public async Task<ApiResponse<object>> GetPurchaseInvoiceListAsync(string? searchString, string? filter, bool isActive, int pageSize, int pageNumber,
|
||||
// Employee loggedInEmployee, Guid tenantId, CancellationToken ct)
|
||||
//{
|
||||
// _logger.LogInfo("GetPurchaseInvoiceListAsync called for TenantId: {TenantId}, EmployeeId: {EmployeeId}", tenantId, loggedInEmployee.Id);
|
||||
|
||||
// await using var _context = await _dbContextFactory.CreateDbContextAsync(ct);
|
||||
|
||||
// //var purchaseInvoices = _context.PurchaseInvoiceDetails
|
||||
// var query = _context.PurchaseInvoiceDetails
|
||||
// .Include(pid => pid.Organization)
|
||||
// .Include(pid => pid.Supplier)
|
||||
// .Include(pid => pid.Status)
|
||||
// .Where(pid => pid.IsActive == isActive && pid.TenantId == tenantId);
|
||||
|
||||
// var advanceFilter = TryDeserializeFilter(filter);
|
||||
|
||||
// query = query.ApplyCustomFilters(advanceFilter, "CreatedAt");
|
||||
|
||||
// if (advanceFilter != null)
|
||||
// {
|
||||
// if (advanceFilter.Filters != null)
|
||||
// {
|
||||
// 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" || f.Column != "Project").ToList();
|
||||
// if (invoiceSearchFilter.Any())
|
||||
// {
|
||||
// query = query.ApplySearchFilters(invoiceSearchFilter);
|
||||
// }
|
||||
// }
|
||||
// if (!string.IsNullOrWhiteSpace(advanceFilter.GroupByColumn))
|
||||
// {
|
||||
// query = query.ApplyGroupByFilters(advanceFilter.GroupByColumn);
|
||||
// }
|
||||
// }
|
||||
|
||||
// bool isProjectFilter = false;
|
||||
|
||||
// var infraProjectTask = Task.Run(async () =>
|
||||
// {
|
||||
// await using var context = await _dbContextFactory.CreateDbContextAsync();
|
||||
|
||||
// var infraProjectsQuery = context.Projects.Where(p => p.TenantId == tenantId);
|
||||
|
||||
// if (advanceFilter?.SearchFilters != null && advanceFilter.SearchFilters.Any())
|
||||
// {
|
||||
// var projectSearchFilter = advanceFilter.SearchFilters
|
||||
// .Where(f => f.Column == "ProjectName" || f.Column == "Project")
|
||||
// .Select(f => new SearchItem { Column = "Name", Value = f.Value })
|
||||
// .ToList();
|
||||
// if (projectSearchFilter.Any())
|
||||
// {
|
||||
// infraProjectsQuery = infraProjectsQuery.ApplySearchFilters(projectSearchFilter);
|
||||
// isProjectFilter = true;
|
||||
// }
|
||||
// }
|
||||
// return await infraProjectsQuery.Select(p => _mapper.Map<BasicProjectVM>(p)).ToListAsync();
|
||||
// });
|
||||
|
||||
// var serviceProjectTask = Task.Run(async () =>
|
||||
// {
|
||||
// await using var context = await _dbContextFactory.CreateDbContextAsync();
|
||||
|
||||
// var serviceProjectsQuery = context.ServiceProjects.Where(sp => sp.TenantId == tenantId);
|
||||
|
||||
// if (advanceFilter?.SearchFilters != null && advanceFilter.SearchFilters.Any())
|
||||
// {
|
||||
// var projectSearchFilter = advanceFilter.SearchFilters
|
||||
// .Where(f => f.Column == "ProjectName" || f.Column == "Project")
|
||||
// .Select(f => new SearchItem { Column = "Name", Value = f.Value })
|
||||
// .ToList();
|
||||
// if (projectSearchFilter.Any())
|
||||
// {
|
||||
// serviceProjectsQuery = serviceProjectsQuery.ApplySearchFilters(projectSearchFilter);
|
||||
// isProjectFilter = true;
|
||||
// }
|
||||
// }
|
||||
|
||||
// return await serviceProjectsQuery.Select(sp => _mapper.Map<BasicProjectVM>(sp)).ToListAsync();
|
||||
// });
|
||||
|
||||
// await Task.WhenAll(infraProjectTask, serviceProjectTask);
|
||||
|
||||
// var projects = infraProjectTask.Result;
|
||||
// projects.AddRange(serviceProjectTask.Result);
|
||||
|
||||
// if (isProjectFilter)
|
||||
// {
|
||||
// var projectIds = projects.Select(p => p.Id).ToList();
|
||||
// query = query.Where(pid => projectIds.Contains(pid.ProjectId));
|
||||
// }
|
||||
|
||||
// var totalCount = await query.CountAsync(ct);
|
||||
|
||||
// var purchaseInvoices = await query
|
||||
// .Skip((pageNumber - 1) * pageSize)
|
||||
// .Take(pageSize)
|
||||
// .ToListAsync();
|
||||
|
||||
// var totalPages = (int)Math.Ceiling((double)totalCount / pageSize);
|
||||
|
||||
// var response = purchaseInvoices.Select(pi =>
|
||||
// {
|
||||
// var result = _mapper.Map<PurchaseInvoiceListVM>(pi);
|
||||
// result.Project = projects.FirstOrDefault(p => p.Id == pi.ProjectId);
|
||||
// return result;
|
||||
// }).ToList();
|
||||
|
||||
// var pagedResult = new
|
||||
// {
|
||||
// CurrentPage = pageNumber,
|
||||
// PageSize = pageSize,
|
||||
// TotalPages = totalPages,
|
||||
// TotalCount = totalCount,
|
||||
// HasPrevious = pageNumber > 1,
|
||||
// HasNext = pageNumber < totalPages,
|
||||
// Data = response
|
||||
// };
|
||||
|
||||
// return ApiResponse<object>.SuccessResponse(pagedResult, "Invoice list fetched successfully", 200);
|
||||
//}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves a paged list of purchase invoices for a given tenant with support for
|
||||
/// advanced filters, project-based search across Infra and Service projects, and
|
||||
/// consistent structured logging and error handling.
|
||||
/// </summary>
|
||||
/// <param name="searchString">Optional basic search string (currently not used, kept for backward compatibility).</param>
|
||||
/// <param name="filter">JSON string containing advanced filter configuration (list/date/search/group filters).</param>
|
||||
/// <param name="isActive">Flag to filter active/inactive invoices.</param>
|
||||
/// <param name="pageSize">Number of records per page.</param>
|
||||
/// <param name="pageNumber">Current page number (1-based).</param>
|
||||
/// <param name="loggedInEmployee">Currently logged-in employee context.</param>
|
||||
/// <param name="tenantId">Tenant identifier for multi-tenant isolation.</param>
|
||||
/// <param name="ct">Cancellation token for cooperative cancellation.</param>
|
||||
/// <returns>Standard ApiResponse containing a paged invoice list payload.</returns>
|
||||
public async Task<ApiResponse<object>> GetPurchaseInvoiceListAsync(string? searchString, string? filter, bool isActive, int pageSize, int pageNumber, Employee loggedInEmployee, Guid tenantId,
|
||||
CancellationToken ct)
|
||||
{
|
||||
// Basic argument validation and guard clauses
|
||||
if (tenantId == Guid.Empty)
|
||||
{
|
||||
_logger.LogWarning("GetPurchaseInvoiceListAsync called with empty TenantId. EmployeeId: {EmployeeId}", loggedInEmployee.Id);
|
||||
|
||||
return ApiResponse<object>.ErrorResponse("Tenant information is missing. Please retry with a valid tenant.", 400);
|
||||
}
|
||||
|
||||
if (pageSize <= 0 || pageNumber <= 0)
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"GetPurchaseInvoiceListAsync called with invalid paging parameters. TenantId: {TenantId}, EmployeeId: {EmployeeId}, PageSize: {PageSize}, PageNumber: {PageNumber}",
|
||||
tenantId, loggedInEmployee.Id, pageSize, pageNumber);
|
||||
|
||||
return ApiResponse<object>.ErrorResponse("Invalid paging parameters. Page size and page number must be greater than zero.", 400);
|
||||
}
|
||||
|
||||
// A correlationId can be pushed earlier in middleware and enriched into all logs.
|
||||
// Here it is assumed to be available through some context (e.g. _requestContext).
|
||||
|
||||
_logger.LogInfo(
|
||||
"GetPurchaseInvoiceListAsync started. TenantId: {TenantId}, EmployeeId: {EmployeeId}, IsActive: {IsActive}, PageSize: {PageSize}, PageNumber: {PageNumber}",
|
||||
tenantId, loggedInEmployee.Id, isActive, pageSize, pageNumber);
|
||||
|
||||
try
|
||||
{
|
||||
await using var context = await _dbContextFactory.CreateDbContextAsync(ct);
|
||||
|
||||
// Base query for purchase invoices scoped to tenant and active flag
|
||||
IQueryable<PurchaseInvoiceDetails> query = context.PurchaseInvoiceDetails
|
||||
.Include(pid => pid.Organization)
|
||||
.Include(pid => pid.Supplier)
|
||||
.Include(pid => pid.Status)
|
||||
.Where(pid => pid.IsActive == isActive && pid.TenantId == tenantId);
|
||||
|
||||
var advanceFilter = TryDeserializeFilter(filter);
|
||||
|
||||
// Apply ordering, default sort, etc. through your custom extension
|
||||
query = query.ApplyCustomFilters(advanceFilter, "CreatedAt");
|
||||
|
||||
if (advanceFilter != null)
|
||||
{
|
||||
// Apply list / dropdown / enum filters
|
||||
if (advanceFilter.Filters != null)
|
||||
{
|
||||
query = query.ApplyListFilters(advanceFilter.Filters);
|
||||
}
|
||||
|
||||
// Apply created/modified date range filters
|
||||
if (advanceFilter.DateFilter != null)
|
||||
{
|
||||
query = query.ApplyDateFilter(advanceFilter.DateFilter);
|
||||
}
|
||||
|
||||
// Apply non-project search filters on invoice fields
|
||||
if (advanceFilter.SearchFilters != null)
|
||||
{
|
||||
// NOTE: fixed logic - use && so that Project/ProjectName are excluded
|
||||
var invoiceSearchFilter = advanceFilter.SearchFilters
|
||||
.Where(f => f.Column != "ProjectName" && f.Column != "Project")
|
||||
.ToList();
|
||||
|
||||
if (invoiceSearchFilter.Any())
|
||||
{
|
||||
query = query.ApplySearchFilters(invoiceSearchFilter);
|
||||
}
|
||||
}
|
||||
|
||||
// Apply grouping if configured
|
||||
if (!string.IsNullOrWhiteSpace(advanceFilter.GroupByColumn))
|
||||
{
|
||||
query = query.ApplyGroupByFilters(advanceFilter.GroupByColumn);
|
||||
}
|
||||
}
|
||||
|
||||
bool isProjectFilter = false;
|
||||
|
||||
// Run project lookups in parallel to reduce latency for project search scenarios.
|
||||
// Each task gets its own DbContext instance from the factory.
|
||||
var infraProjectTask = Task.Run(async () =>
|
||||
{
|
||||
await using var projContext = await _dbContextFactory.CreateDbContextAsync(ct);
|
||||
|
||||
IQueryable<Project> infraProjectsQuery = projContext.Projects
|
||||
.Where(p => p.TenantId == tenantId);
|
||||
|
||||
if (advanceFilter?.SearchFilters != null && advanceFilter.SearchFilters.Any())
|
||||
{
|
||||
var projectSearchFilter = advanceFilter.SearchFilters
|
||||
.Where(f => f.Column == "ProjectName" || f.Column == "Project")
|
||||
.Select(f => new SearchItem { Column = "Name", Value = f.Value })
|
||||
.ToList();
|
||||
|
||||
if (projectSearchFilter.Any())
|
||||
{
|
||||
infraProjectsQuery = infraProjectsQuery.ApplySearchFilters(projectSearchFilter);
|
||||
isProjectFilter = true; // NOTE: shared flag, see comment below.
|
||||
}
|
||||
}
|
||||
|
||||
return await infraProjectsQuery
|
||||
.Select(p => _mapper.Map<BasicProjectVM>(p))
|
||||
.ToListAsync(ct);
|
||||
}, ct);
|
||||
|
||||
var serviceProjectTask = Task.Run(async () =>
|
||||
{
|
||||
await using var projContext = await _dbContextFactory.CreateDbContextAsync(ct);
|
||||
|
||||
IQueryable<ServiceProject> serviceProjectsQuery = projContext.ServiceProjects
|
||||
.Where(sp => sp.TenantId == tenantId);
|
||||
|
||||
if (advanceFilter?.SearchFilters != null && advanceFilter.SearchFilters.Any())
|
||||
{
|
||||
var projectSearchFilter = advanceFilter.SearchFilters
|
||||
.Where(f => f.Column == "ProjectName" || f.Column == "Project")
|
||||
.Select(f => new SearchItem { Column = "Name", Value = f.Value })
|
||||
.ToList();
|
||||
|
||||
if (projectSearchFilter.Any())
|
||||
{
|
||||
serviceProjectsQuery = serviceProjectsQuery.ApplySearchFilters(projectSearchFilter);
|
||||
isProjectFilter = true; // This is safe for bool but can be refactored for purity.
|
||||
}
|
||||
}
|
||||
|
||||
return await serviceProjectsQuery
|
||||
.Select(sp => _mapper.Map<BasicProjectVM>(sp))
|
||||
.ToListAsync(ct);
|
||||
}, ct);
|
||||
|
||||
await Task.WhenAll(infraProjectTask, serviceProjectTask);
|
||||
|
||||
var projects = infraProjectTask.Result ?? new List<BasicProjectVM>();
|
||||
if (serviceProjectTask.Result != null && serviceProjectTask.Result.Any())
|
||||
{
|
||||
projects.AddRange(serviceProjectTask.Result);
|
||||
}
|
||||
|
||||
// If project filters were involved, constrain invoices to those projects
|
||||
if (isProjectFilter && projects.Any())
|
||||
{
|
||||
var projectIds = projects.Select(p => p.Id).ToList();
|
||||
query = query.Where(pid => projectIds.Contains(pid.ProjectId));
|
||||
}
|
||||
|
||||
// Compute total count before paging
|
||||
var totalCount = await query.CountAsync(ct);
|
||||
|
||||
// Apply paging
|
||||
var purchaseInvoices = await query
|
||||
.Skip((pageNumber - 1) * pageSize)
|
||||
.Take(pageSize)
|
||||
.ToListAsync(ct);
|
||||
|
||||
var totalPages = totalCount == 0
|
||||
? 0
|
||||
: (int)Math.Ceiling((double)totalCount / pageSize);
|
||||
|
||||
// Map invoice entities to view model and attach project details
|
||||
var response = purchaseInvoices.Select(pi =>
|
||||
{
|
||||
var vm = _mapper.Map<PurchaseInvoiceListVM>(pi);
|
||||
vm.Project = projects.FirstOrDefault(p => p.Id == pi.ProjectId);
|
||||
return vm;
|
||||
}).ToList();
|
||||
|
||||
var pagedResult = new
|
||||
{
|
||||
CurrentPage = pageNumber,
|
||||
PageSize = pageSize,
|
||||
TotalPages = totalPages,
|
||||
TotalCount = totalCount,
|
||||
HasPrevious = pageNumber > 1,
|
||||
HasNext = pageNumber < totalPages,
|
||||
Data = response
|
||||
};
|
||||
|
||||
_logger.LogInfo(
|
||||
"GetPurchaseInvoiceListAsync completed successfully. TenantId: {TenantId}, EmployeeId: {EmployeeId}, TotalCount: {TotalCount}, ReturnedCount: {ReturnedCount}, PageNumber: {PageNumber}, PageSize: {PageSize}",
|
||||
tenantId, loggedInEmployee.Id, totalCount, response.Count, pageNumber, pageSize);
|
||||
|
||||
var successMessage = totalCount == 0
|
||||
? "No purchase invoices found for the specified criteria."
|
||||
: "Purchase invoice list fetched successfully.";
|
||||
|
||||
return ApiResponse<object>.SuccessResponse(pagedResult, successMessage, 200);
|
||||
}
|
||||
catch (OperationCanceledException ocex)
|
||||
{
|
||||
// Respect cancellation and return a 499-style semantic code if your ApiResponse supports it
|
||||
_logger.LogError(ocex, "GetPurchaseInvoiceListAsync request was canceled. TenantId: {TenantId}, EmployeeId: {EmployeeId}", tenantId, loggedInEmployee.Id);
|
||||
|
||||
return ApiResponse<object>.ErrorResponse("The request was canceled by the client or the server. Please retry if this was unintentional.", 499);
|
||||
}
|
||||
catch (DbUpdateException dbEx)
|
||||
{
|
||||
_logger.LogError(dbEx,
|
||||
"Database error while fetching purchase invoices. TenantId: {TenantId}, EmployeeId: {EmployeeId}", tenantId, loggedInEmployee.Id);
|
||||
|
||||
return ApiResponse<object>.ErrorResponse("A database error occurred while fetching purchase invoices. Please try again later or contact support.", 500);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Unhandled exception in GetPurchaseInvoiceListAsync. CorrelationId: {CorrelationId}, TenantId: {TenantId}, EmployeeId: {EmployeeId}", tenantId, loggedInEmployee.Id);
|
||||
|
||||
return ApiResponse<object>.ErrorResponse("An unexpected error occurred while fetching purchase invoices. Please try again later or contact support.", 500);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new Purchase Invoice with validation, S3 file uploads, and transactional database storage.
|
||||
/// </summary>
|
||||
@ -255,7 +616,7 @@ namespace Marco.Pms.Services.Service
|
||||
response.Project = projectVm;
|
||||
response.Supplier = _mapper.Map<BasicOrganizationVm>(supplier);
|
||||
|
||||
return ApiResponse<PurchaseInvoiceListVM>.SuccessResponse(response, "Purchase invoice created successfully", 200);
|
||||
return ApiResponse<PurchaseInvoiceListVM>.SuccessResponse(response, "Purchase invoice created successfully", 201);
|
||||
}
|
||||
catch (DbUpdateException ex)
|
||||
{
|
||||
@ -285,5 +646,46 @@ namespace Marco.Pms.Services.Service
|
||||
return ApiResponse<PurchaseInvoiceListVM>.ErrorResponse("Creation Failed", "An unexpected error occurred while processing your request.", 500);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private AdvanceFilter? TryDeserializeFilter(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;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@ -7,6 +7,8 @@ namespace Marco.Pms.Services.Service.ServiceInterfaces
|
||||
{
|
||||
public interface IPurchaseInvoiceService
|
||||
{
|
||||
Task<ApiResponse<object>> GetPurchaseInvoiceListAsync(string? searchString, string? filter, bool isActive, int pageSize, int pageNumber,
|
||||
Employee loggedInEmployee, Guid tenantId, CancellationToken ct);
|
||||
Task<ApiResponse<PurchaseInvoiceListVM>> CreatePurchaseInvoiceAsync(PurchaseInvoiceDto model, Employee loggedInEmployee, Guid tenantId, CancellationToken ct);
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user