Completed the get expenses list API with optimized code
This commit is contained in:
parent
c1845dd8b7
commit
3b4b09783b
12
Marco.Pms.Model/Utilities/ExpensesFilter.cs
Normal file
12
Marco.Pms.Model/Utilities/ExpensesFilter.cs
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
namespace Marco.Pms.Model.Utilities
|
||||||
|
{
|
||||||
|
public class ExpensesFilter
|
||||||
|
{
|
||||||
|
public List<Guid>? ProjectIds { get; set; }
|
||||||
|
public List<Guid>? StatusIds { get; set; }
|
||||||
|
public List<Guid>? CreatedByIds { get; set; }
|
||||||
|
public List<Guid>? PaidById { get; set; }
|
||||||
|
public DateTime? StartDate { get; set; }
|
||||||
|
public DateTime? EndDate { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -12,6 +12,7 @@ using MarcoBMS.Services.Service;
|
|||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using System.Text.Json;
|
||||||
using Document = Marco.Pms.Model.DocumentManager.Document;
|
using Document = Marco.Pms.Model.DocumentManager.Document;
|
||||||
|
|
||||||
// For more information on enabling Web API for empty projects, visit https://go.microsoft.com/fwlink/?LinkID=397860
|
// For more information on enabling Web API for empty projects, visit https://go.microsoft.com/fwlink/?LinkID=397860
|
||||||
@ -48,7 +49,7 @@ namespace Marco.Pms.Services.Controllers
|
|||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("list")]
|
[HttpGet("list")]
|
||||||
public async Task<IActionResult> GetExpensesList()
|
public async Task<IActionResult> GetExpensesList1(string? filter, int pageSize = 20, int pageNumber = 1)
|
||||||
{
|
{
|
||||||
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
|
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
|
||||||
var loggedInEmployeeId = loggedInEmployee.Id;
|
var loggedInEmployeeId = loggedInEmployee.Id;
|
||||||
@ -62,42 +63,78 @@ namespace Marco.Pms.Services.Controllers
|
|||||||
.Include(e => e.PaymentMode)
|
.Include(e => e.PaymentMode)
|
||||||
.Include(e => e.Status)
|
.Include(e => e.Status)
|
||||||
.Include(e => e.CreatedBy)
|
.Include(e => e.CreatedBy)
|
||||||
.Where(e => e.TenantId == tenantId);
|
.Where(e => e.TenantId == tenantId)
|
||||||
|
.OrderByDescending(e => e.CreatedAt)
|
||||||
|
.Skip((pageNumber - 1) * pageSize)
|
||||||
|
.Take(pageSize);
|
||||||
|
|
||||||
var HasViewAllPermission = await _permission.HasPermission(PermissionsMaster.ExpenseViewAll, loggedInEmployeeId);
|
var HasViewAllPermission = await _permission.HasPermission(PermissionsMaster.ExpenseViewAll, loggedInEmployeeId);
|
||||||
var HasViewSelfPermission = await _permission.HasPermission(PermissionsMaster.ExpenseViewSelf, loggedInEmployeeId);
|
var HasViewSelfPermission = await _permission.HasPermission(PermissionsMaster.ExpenseViewSelf, loggedInEmployeeId);
|
||||||
|
|
||||||
if (HasViewAllPermission)
|
if (HasViewSelfPermission)
|
||||||
{
|
{
|
||||||
expensesList = await expensesListQuery.ToListAsync();
|
expensesListQuery = expensesListQuery.Where(e => e.CreatedById == loggedInEmployeeId);
|
||||||
}
|
}
|
||||||
else if (HasViewSelfPermission)
|
else if (!HasViewAllPermission)
|
||||||
{
|
{
|
||||||
expensesList = await expensesListQuery.Where(e => e.CreatedById == loggedInEmployeeId).ToListAsync();
|
_logger.LogWarning("Access DENIED for employee {EmployeeId} attempting to get expanses list.", loggedInEmployeeId);
|
||||||
}
|
|
||||||
|
|
||||||
if (expensesList == null)
|
|
||||||
{
|
|
||||||
_logger.LogInfo("No Expense found for employee {EmployeeId}", loggedInEmployeeId);
|
|
||||||
return Ok(ApiResponse<object>.SuccessResponse(new List<ExpenseList>(), "No Expense found for current user", 200));
|
return Ok(ApiResponse<object>.SuccessResponse(new List<ExpenseList>(), "No Expense found for current user", 200));
|
||||||
}
|
}
|
||||||
|
|
||||||
//ImageFilter? imageFilter = null;
|
ExpensesFilter? expenesFilter = null;
|
||||||
//if (!string.IsNullOrWhiteSpace(filter))
|
if (!string.IsNullOrWhiteSpace(filter))
|
||||||
//{
|
{
|
||||||
// try
|
try
|
||||||
// {
|
{
|
||||||
// var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true };
|
var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true };
|
||||||
// //string unescapedJsonString = JsonSerializer.Deserialize<string>(filter, options) ?? "";
|
expenesFilter = JsonSerializer.Deserialize<ExpensesFilter>(filter, options);
|
||||||
// //imageFilter = JsonSerializer.Deserialize<ImageFilter>(unescapedJsonString, options);
|
}
|
||||||
// imageFilter = JsonSerializer.Deserialize<ImageFilter>(filter, options);
|
catch (Exception ex)
|
||||||
// }
|
{
|
||||||
// catch (Exception ex)
|
_logger.LogError(ex, "[GetExpensesList] Failed to parse filter came from website or mobile");
|
||||||
// {
|
|
||||||
// _logger.LogWarning("[GetImageList] Failed to parse filter: {Message}", ex.Message);
|
|
||||||
// }
|
|
||||||
//}
|
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true };
|
||||||
|
string unescapedJsonString = JsonSerializer.Deserialize<string>(filter, options) ?? "";
|
||||||
|
expenesFilter = JsonSerializer.Deserialize<ExpensesFilter>(unescapedJsonString, options);
|
||||||
|
}
|
||||||
|
catch (Exception ex1)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex1, "[GetExpensesList] Failed to parse filter came from postman");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var projectIds = expenesFilter?.ProjectIds;
|
||||||
|
var filterStatusIds = expenesFilter?.StatusIds;
|
||||||
|
var createdByIds = expenesFilter?.CreatedByIds;
|
||||||
|
var paidById = expenesFilter?.PaidById;
|
||||||
|
var startDate = expenesFilter?.StartDate;
|
||||||
|
var endDate = expenesFilter?.EndDate;
|
||||||
|
|
||||||
|
if (startDate != null && endDate != null)
|
||||||
|
{
|
||||||
|
expensesListQuery = expensesListQuery.Where(e => e.CreatedAt.Date >= startDate && e.CreatedAt.Date <= endDate);
|
||||||
|
}
|
||||||
|
else if (projectIds != null && projectIds.Any())
|
||||||
|
{
|
||||||
|
expensesListQuery = expensesListQuery.Where(e => projectIds.Contains(e.ProjectId));
|
||||||
|
}
|
||||||
|
else if (filterStatusIds != null && filterStatusIds.Any())
|
||||||
|
{
|
||||||
|
expensesListQuery = expensesListQuery.Where(e => filterStatusIds.Contains(e.StatusId));
|
||||||
|
}
|
||||||
|
else if (createdByIds != null && createdByIds.Any() && HasViewAllPermission)
|
||||||
|
{
|
||||||
|
expensesListQuery = expensesListQuery.Where(e => createdByIds.Contains(e.CreatedById));
|
||||||
|
}
|
||||||
|
else if (paidById != null && paidById.Any())
|
||||||
|
{
|
||||||
|
expensesListQuery = expensesListQuery.Where(e => paidById.Contains(e.PaidById));
|
||||||
|
}
|
||||||
|
|
||||||
|
expensesList = await expensesListQuery.ToListAsync();
|
||||||
|
|
||||||
var response = _mapper.Map<List<ExpenseList>>(expensesList);
|
var response = _mapper.Map<List<ExpenseList>>(expensesList);
|
||||||
|
|
||||||
@ -117,6 +154,225 @@ namespace Marco.Pms.Services.Controllers
|
|||||||
return StatusCode(200, ApiResponse<object>.SuccessResponse(response, $"{response.Count} records of expenses for you fetched successfully", 200));
|
return StatusCode(200, ApiResponse<object>.SuccessResponse(response, $"{response.Count} records of expenses for you fetched successfully", 200));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Retrieves a paginated list of expenses based on user permissions and optional filters.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="filter">A URL-encoded JSON string containing filter criteria. See <see cref="ExpensesFilter"/>.</param>
|
||||||
|
/// <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>
|
||||||
|
[HttpGet] // Assuming this is a GET endpoint
|
||||||
|
public async Task<IActionResult> GetExpensesList(string? filter, int pageSize = 20, int pageNumber = 1)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_logger.LogInfo(
|
||||||
|
"Attempting to fetch expenses list for PageNumber: {PageNumber}, PageSize: {PageSize} with Filter: {Filter}",
|
||||||
|
pageNumber, pageSize, filter ?? "");
|
||||||
|
|
||||||
|
// 1. --- Get User and Permissions ---
|
||||||
|
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
|
||||||
|
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 Unauthorized(ApiResponse<object>.ErrorResponse("User not found or not authenticated.", 401));
|
||||||
|
}
|
||||||
|
Guid loggedInEmployeeId = loggedInEmployee.Id;
|
||||||
|
|
||||||
|
var hasViewAllPermission = await _permission.HasPermission(PermissionsMaster.ExpenseViewAll, loggedInEmployeeId);
|
||||||
|
var hasViewSelfPermission = await _permission.HasPermission(PermissionsMaster.ExpenseViewSelf, loggedInEmployeeId);
|
||||||
|
|
||||||
|
// 2. --- Build Base Query and Apply Permissions ---
|
||||||
|
// Start with a base IQueryable. Filters will be chained onto this.
|
||||||
|
var expensesQuery = _context.Expenses
|
||||||
|
.Include(e => e.ExpensesType)
|
||||||
|
.Include(e => e.Project)
|
||||||
|
.Include(e => e.PaidBy)
|
||||||
|
.ThenInclude(e => e!.JobRole)
|
||||||
|
.Include(e => e.PaymentMode)
|
||||||
|
.Include(e => e.Status)
|
||||||
|
.Include(e => e.CreatedBy)
|
||||||
|
.Where(e => e.TenantId == tenantId); // Always filter by TenantId first.
|
||||||
|
|
||||||
|
// Apply permission-based filtering BEFORE any other filters or pagination.
|
||||||
|
if (hasViewAllPermission)
|
||||||
|
{
|
||||||
|
// User has 'View All' permission, no initial restriction on who created the expense.
|
||||||
|
_logger.LogInfo("User {EmployeeId} has 'View All' permission.", loggedInEmployeeId);
|
||||||
|
}
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// User has neither required permission. Deny access.
|
||||||
|
_logger.LogWarning("Access DENIED for employee {EmployeeId} attempting to get expenses list.", loggedInEmployeeId);
|
||||||
|
return Ok(ApiResponse<object>.SuccessResponse(new List<ExpenseList>(), "You do not have permission to view any expenses.", 200));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. --- Deserialize Filter and Apply ---
|
||||||
|
ExpensesFilter? expenseFilter = TryDeserializeFilter(filter);
|
||||||
|
|
||||||
|
if (expenseFilter != null)
|
||||||
|
{
|
||||||
|
// CRITICAL FIX: Apply filters cumulatively using multiple `if` statements, not `if-else if`.
|
||||||
|
if (expenseFilter.StartDate.HasValue && expenseFilter.EndDate.HasValue)
|
||||||
|
{
|
||||||
|
expensesQuery = expensesQuery.Where(e => e.CreatedAt.Date >= expenseFilter.StartDate.Value.Date && e.CreatedAt.Date <= expenseFilter.EndDate.Value.Date);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (expenseFilter.ProjectIds?.Any() == true)
|
||||||
|
{
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only allow filtering by 'CreatedBy' if the user has 'View All' permission.
|
||||||
|
if (expenseFilter.CreatedByIds?.Any() == true && hasViewAllPermission)
|
||||||
|
{
|
||||||
|
expensesQuery = expensesQuery.Where(e => expenseFilter.CreatedByIds.Contains(e.CreatedById));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. --- Apply Ordering and Pagination ---
|
||||||
|
// This should be the last step before executing the query.
|
||||||
|
var paginatedQuery = expensesQuery
|
||||||
|
.OrderByDescending(e => e.CreatedAt)
|
||||||
|
.Skip((pageNumber - 1) * pageSize)
|
||||||
|
.Take(pageSize);
|
||||||
|
|
||||||
|
// 5. --- Execute Query and Map Results ---
|
||||||
|
var expensesList = await paginatedQuery.ToListAsync();
|
||||||
|
|
||||||
|
if (!expensesList.Any())
|
||||||
|
{
|
||||||
|
_logger.LogInfo("No expenses found matching the criteria for employee {EmployeeId}.", loggedInEmployeeId);
|
||||||
|
return Ok(ApiResponse<object>.SuccessResponse(new List<ExpenseList>(), "No expenses found for the given criteria.", 200));
|
||||||
|
}
|
||||||
|
|
||||||
|
var response = _mapper.Map<List<ExpenseList>>(expensesList);
|
||||||
|
|
||||||
|
// 6. --- Efficiently Fetch and Append 'Next Status' Information ---
|
||||||
|
var statusIds = expensesList.Select(e => e.StatusId).Distinct().ToList();
|
||||||
|
|
||||||
|
var statusMappings = await _context.ExpensesStatusMapping
|
||||||
|
.Include(sm => sm.NextStatus)
|
||||||
|
.Where(sm => statusIds.Contains(sm.StatusId))
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
// Use a Lookup for efficient O(1) mapping. This is much better than repeated `.Where()` in a loop.
|
||||||
|
var statusMapLookup = statusMappings.ToLookup(sm => sm.StatusId);
|
||||||
|
|
||||||
|
foreach (var expense in response)
|
||||||
|
{
|
||||||
|
if (expense.Status?.Id != null && statusMapLookup.Contains(expense.Status.Id))
|
||||||
|
{
|
||||||
|
expense.NextStatus = statusMapLookup[expense.Status.Id]
|
||||||
|
.Select(sm => _mapper.Map<ExpensesStatusMasterVM>(sm.NextStatus))
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
expense.NextStatus = new List<ExpensesStatusMasterVM>(); // Ensure it's never null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 7. --- Return Final Success Response ---
|
||||||
|
var message = $"{response.Count} expense records fetched successfully.";
|
||||||
|
_logger.LogInfo(message);
|
||||||
|
return StatusCode(200, ApiResponse<object>.SuccessResponse(response, message, 200));
|
||||||
|
}
|
||||||
|
catch (DbUpdateException dbEx)
|
||||||
|
{
|
||||||
|
_logger.LogError(dbEx, "Databsae Exception occured while fetching list expenses");
|
||||||
|
return BadRequest(ApiResponse<object>.ErrorResponse("Databsae Exception", new
|
||||||
|
{
|
||||||
|
Message = dbEx.Message,
|
||||||
|
StackTrace = dbEx.StackTrace,
|
||||||
|
Source = dbEx.Source,
|
||||||
|
innerexcption = new
|
||||||
|
{
|
||||||
|
Message = dbEx.InnerException?.Message,
|
||||||
|
StackTrace = dbEx.InnerException?.StackTrace,
|
||||||
|
Source = dbEx.InnerException?.Source,
|
||||||
|
}
|
||||||
|
}, 400));
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error occured while fetching list expenses");
|
||||||
|
return BadRequest(ApiResponse<object>.ErrorResponse("Error Occured", new
|
||||||
|
{
|
||||||
|
Message = ex.Message,
|
||||||
|
StackTrace = ex.StackTrace,
|
||||||
|
Source = ex.Source,
|
||||||
|
innerexcption = new
|
||||||
|
{
|
||||||
|
Message = ex.InnerException?.Message,
|
||||||
|
StackTrace = ex.InnerException?.StackTrace,
|
||||||
|
Source = ex.InnerException?.Source,
|
||||||
|
}
|
||||||
|
}, 400));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Deserializes the filter string, handling multiple potential formats (e.g., direct JSON vs. escaped JSON string).
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="filter">The JSON filter string from the request.</param>
|
||||||
|
/// <returns>An <see cref="ExpensesFilter"/> object or null if deserialization fails.</returns>
|
||||||
|
private ExpensesFilter? TryDeserializeFilter(string? filter)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(filter))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true };
|
||||||
|
ExpensesFilter? expenseFilter = null;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// First, try to deserialize directly. This is the expected case (e.g., from a web client).
|
||||||
|
expenseFilter = JsonSerializer.Deserialize<ExpensesFilter>(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))
|
||||||
|
{
|
||||||
|
expenseFilter = JsonSerializer.Deserialize<ExpensesFilter>(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 expenseFilter;
|
||||||
|
}
|
||||||
|
|
||||||
[HttpGet("details/{id}")]
|
[HttpGet("details/{id}")]
|
||||||
public string Get(int id)
|
public string Get(int id)
|
||||||
{
|
{
|
||||||
@ -199,13 +455,13 @@ namespace Marco.Pms.Services.Controllers
|
|||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
_logger.LogError(ex, "Error occured while saving image to S3");
|
_logger.LogError(ex, "Error occured while saving image to S3");
|
||||||
//return BadRequest(ApiResponse<object>.ErrorResponse("Cannot upload attachment to S3", new
|
return BadRequest(ApiResponse<object>.ErrorResponse("Cannot upload attachment to S3", new
|
||||||
//{
|
{
|
||||||
// message = ex.Message,
|
message = ex.Message,
|
||||||
// innerexcption = ex.InnerException?.Message,
|
innerexcption = ex.InnerException?.Message,
|
||||||
// stackTrace = ex.StackTrace,
|
stackTrace = ex.StackTrace,
|
||||||
// source = ex.Source
|
source = ex.Source
|
||||||
//}, 400));
|
}, 400));
|
||||||
}
|
}
|
||||||
|
|
||||||
var document = new Document
|
var document = new Document
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user