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.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using System.Globalization;
namespace Marco.Pms.Services.Controllers 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)); return Ok(ApiResponse<object>.SuccessResponse(sortedResult, $"{sortedResult.Count} records fetched for attendance overview", 200));
} }
[HttpGet("expense/project")] [HttpGet("expense/monthly")]
public async Task<IActionResult> GetExpenseReportByProjectsAsync([FromQuery] DateTime startDate, [FromQuery] DateTime endDate) public async Task<IActionResult> GetExpenseReportByProjectsAsync([FromQuery] Guid? projectId, [FromQuery] Guid? categoryId, [FromQuery] int months)
{ {
// Structured start log months = 0 - months;
_logger.LogInfo( var end = DateTime.UtcNow.Date;
"GetExpenseReportByProjects started. TenantId={TenantId}, StartDate={StartDate}, EndDate={EndDate}", var start = end.AddMonths(months); // inclusive EOD
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]
try try
{ {
@ -641,55 +631,58 @@ namespace Marco.Pms.Services.Controllers
e.TenantId == tenantId e.TenantId == tenantId
&& e.IsActive && e.IsActive
&& e.StatusId != Draft && e.StatusId != Draft
&& e.Project != null
&& e.TransactionDate >= start && 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 // Single server-side group/aggregate by project
var report = await baseQuery var report = await baseQuery
.GroupBy(e => e.Project) .AsNoTracking()
.Select(g => new .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) Year = g.Key.Year,
.Sum(x => x.Amount), Month = g.Key.Month,
TotalPendingAmount = g.Where(x => x.StatusId != Processed Total = g.Sum(x => x.Amount),
&& x.StatusId != RejectedByReviewer Count = g.Count()
&& x.StatusId != RejectedByApprover) })
.Sum(x => x.Amount), .OrderBy(x => x.Year).ThenBy(x => x.Month)
TotalRejectedAmount = g.Where(x => x.StatusId == RejectedByReviewer .ToListAsync();
|| x.StatusId == RejectedByApprover)
.Sum(x => x.Amount),
TotalProcessedAmount = g.Where(x => x.StatusId == Processed)
.Sum(x => x.Amount)
})
.OrderBy(r => r.ProjectName)
.ToListAsync(); // [Single Round-trip] [memory:7]
var response = new var culture = CultureInfo.GetCultureInfo("en-IN"); // pick desired locale
{
Report = report, var response = report
TotalAmount = report.Sum(r => r.TotalApprovedAmount) .Select(x => new
}; {
MonthName = culture.DateTimeFormat.GetMonthName(x.Month), // e.g., "January"
Year = x.Year,
Total = x.Total,
Count = x.Count
}).ToList();
_logger.LogInfo( _logger.LogInfo(
"GetExpenseReportByProjects completed. TenantId={TenantId}, Rows={Rows}, TotalAmount={TotalAmount}", "GetExpenseReportByProjects completed. TenantId={TenantId}, Rows={Rows}",
tenantId, report.Count, response.TotalAmount); // [Completion Log] [memory:4] 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) catch (OperationCanceledException)
{ {
_logger.LogWarning("GetExpenseReportByProjects canceled by client. TenantId={TenantId}", tenantId); // [Cancel Log] [memory:4] _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] [memory:1] return StatusCode(499, ApiResponse<object>.ErrorResponse("Client has canceled the opration", "Client has canceled the opration", 499)); // [Cancel Response]
} }
catch (Exception ex) catch (Exception ex)
{ {
_logger.LogError(ex, _logger.LogError(ex,
"GetExpenseReportByProjects failed. TenantId={TenantId}, StartDate={StartDate}, EndDate={EndDate}", "GetExpenseReportByProjects failed. TenantId={TenantId}, StartDate={StartDate}, EndDate={EndDate}",
tenantId, start, end); // [Error Log] [memory:4] tenantId, start, end); // [Error Log]
return StatusCode(500, 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] baseQuery = baseQuery.Where(e => e.ProjectId == projectId.Value); // [Filter] [memory:7]
// Project to a minimal shape before grouping to avoid loading navigation graphs // 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 var query = baseQuery
.Where(e => e.ExpensesType != null) .Where(e => e.ExpensesType != null)
.Select(e => new .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, Amount = e.Amount,
StatusId = e.StatusId StatusId = e.StatusId
}) })