Getting the monthly expenses report based on project and category

This commit is contained in:
ashutosh.nehete 2025-10-03 12:53:19 +05:30
parent 91f4305995
commit 548e714ea9

View File

@ -14,6 +14,7 @@ using MarcoBMS.Services.Service;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using System.Globalization;
namespace Marco.Pms.Services.Controllers
{
@ -614,23 +615,12 @@ namespace Marco.Pms.Services.Controllers
return Ok(ApiResponse<object>.SuccessResponse(sortedResult, $"{sortedResult.Count} records fetched for attendance overview", 200));
}
[HttpGet("expense/project")]
public async Task<IActionResult> GetExpenseReportByProjectsAsync([FromQuery] DateTime startDate, [FromQuery] DateTime endDate)
[HttpGet("expense/monthly")]
public async Task<IActionResult> GetExpenseReportByProjectsAsync([FromQuery] Guid? projectId, [FromQuery] Guid? categoryId, [FromQuery] int months)
{
// Structured start log
_logger.LogInfo(
"GetExpenseReportByProjects started. TenantId={TenantId}, StartDate={StartDate}, EndDate={EndDate}",
tenantId, startDate, endDate); // [Start Log] [memory:4][memory:1]
// Guard: validate range and normalize to inclusive end-of-day
if (endDate < startDate)
{
_logger.LogWarning("Invalid date range. StartDate={StartDate}, EndDate={EndDate}", startDate, endDate); // [Validation Log] [memory:4]
return BadRequest(ApiResponse<object>.ErrorResponse("endDate must be on or after startDate.", 400)); // [Validation Response] [memory:1]
}
var start = startDate.Date;
var end = endDate.Date.AddDays(1).AddTicks(-1); // inclusive EOD [memory:7]
months = 0 - months;
var end = DateTime.UtcNow.Date;
var start = end.AddMonths(months); // inclusive EOD
try
{
@ -641,55 +631,58 @@ namespace Marco.Pms.Services.Controllers
e.TenantId == tenantId
&& e.IsActive
&& e.StatusId != Draft
&& e.Project != null
&& e.TransactionDate >= start
&& e.TransactionDate <= end); // [Server Filters] [memory:7]
&& e.TransactionDate <= end); // [Server Filters]
if (projectId.HasValue)
baseQuery.Where(e => e.ProjectId == projectId);
if (categoryId.HasValue)
baseQuery.Where(e => e.ExpensesTypeId == categoryId);
// Single server-side group/aggregate by project
var report = await baseQuery
.GroupBy(e => e.Project)
.AsNoTracking()
.GroupBy(e => new { e.TransactionDate.Year, e.TransactionDate.Month })
.Select(g => new
{
ProjectName = g.Key!.Name,
TotalApprovedAmount = g.Where(x => x.StatusId == Processed || x.StatusId == ProcessPending)
.Sum(x => x.Amount),
TotalPendingAmount = g.Where(x => x.StatusId != Processed
&& x.StatusId != RejectedByReviewer
&& x.StatusId != RejectedByApprover)
.Sum(x => x.Amount),
TotalRejectedAmount = g.Where(x => x.StatusId == RejectedByReviewer
|| x.StatusId == RejectedByApprover)
.Sum(x => x.Amount),
TotalProcessedAmount = g.Where(x => x.StatusId == Processed)
.Sum(x => x.Amount)
Year = g.Key.Year,
Month = g.Key.Month,
Total = g.Sum(x => x.Amount),
Count = g.Count()
})
.OrderBy(r => r.ProjectName)
.ToListAsync(); // [Single Round-trip] [memory:7]
.OrderBy(x => x.Year).ThenBy(x => x.Month)
.ToListAsync();
var response = new
var culture = CultureInfo.GetCultureInfo("en-IN"); // pick desired locale
var response = report
.Select(x => new
{
Report = report,
TotalAmount = report.Sum(r => r.TotalApprovedAmount)
};
MonthName = culture.DateTimeFormat.GetMonthName(x.Month), // e.g., "January"
Year = x.Year,
Total = x.Total,
Count = x.Count
}).ToList();
_logger.LogInfo(
"GetExpenseReportByProjects completed. TenantId={TenantId}, Rows={Rows}, TotalAmount={TotalAmount}",
tenantId, report.Count, response.TotalAmount); // [Completion Log] [memory:4]
"GetExpenseReportByProjects completed. TenantId={TenantId}, Rows={Rows}",
tenantId, report.Count); // [Completion Log]
return Ok(ApiResponse<object>.SuccessResponse(response, "Expense report by project fetched successfully", 200)); // [Success Response] [memory:1]
return Ok(ApiResponse<object>.SuccessResponse(response, "Expense report by project fetched successfully", 200)); // [Success Response]
}
catch (OperationCanceledException)
{
_logger.LogWarning("GetExpenseReportByProjects canceled by client. TenantId={TenantId}", tenantId); // [Cancel Log] [memory:4]
return StatusCode(499, ApiResponse<object>.ErrorResponse("Client has canceled the opration", "Client has canceled the opration", 499)); // [Cancel Response] [memory:1]
_logger.LogWarning("GetExpenseReportByProjects canceled by client. TenantId={TenantId}", tenantId); // [Cancel Log]
return StatusCode(499, ApiResponse<object>.ErrorResponse("Client has canceled the opration", "Client has canceled the opration", 499)); // [Cancel Response]
}
catch (Exception ex)
{
_logger.LogError(ex,
"GetExpenseReportByProjects failed. TenantId={TenantId}, StartDate={StartDate}, EndDate={EndDate}",
tenantId, start, end); // [Error Log] [memory:4]
tenantId, start, end); // [Error Log]
return StatusCode(500,
ApiResponse<object>.ErrorResponse("An error occurred while fetching the expense report.", 500)); // [Error Response] [memory:1]
ApiResponse<object>.ErrorResponse("An error occurred while fetching the expense report.", 500)); // [Error Response]
}
}
@ -717,12 +710,12 @@ namespace Marco.Pms.Services.Controllers
baseQuery = baseQuery.Where(e => e.ProjectId == projectId.Value); // [Filter] [memory:7]
// Project to a minimal shape before grouping to avoid loading navigation graphs
// Group by expense type name; adjust to the correct key if ExpensesType is an enum or navigation
// Group by expense type name; adjust to the correct key if ExpensesCategory is an enum or navigation
var query = baseQuery
.Where(e => e.ExpensesType != null)
.Select(e => new
{
ExpenseTypeName = e.ExpensesType!.Name, // If enum, use e.ExpensesType.ToString()
ExpenseTypeName = e.ExpensesType!.Name, // If enum, use e.ExpensesCategory.ToString()
Amount = e.Amount,
StatusId = e.StatusId
})