diff --git a/Marco.Pms.Services/Controllers/DashboardController.cs b/Marco.Pms.Services/Controllers/DashboardController.cs index d508d87..4447b68 100644 --- a/Marco.Pms.Services/Controllers/DashboardController.cs +++ b/Marco.Pms.Services/Controllers/DashboardController.cs @@ -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.SuccessResponse(sortedResult, $"{sortedResult.Count} records fetched for attendance overview", 200)); } - [HttpGet("expense/project")] - public async Task GetExpenseReportByProjectsAsync([FromQuery] DateTime startDate, [FromQuery] DateTime endDate) + [HttpGet("expense/monthly")] + public async Task 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.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) - .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) - }) - .OrderBy(r => r.ProjectName) - .ToListAsync(); // [Single Round-trip] [memory:7] + .AsNoTracking() + .GroupBy(e => new { e.TransactionDate.Year, e.TransactionDate.Month }) + .Select(g => new + { + Year = g.Key.Year, + Month = g.Key.Month, + Total = g.Sum(x => x.Amount), + Count = g.Count() + }) + .OrderBy(x => x.Year).ThenBy(x => x.Month) + .ToListAsync(); - var response = new - { - Report = report, - TotalAmount = report.Sum(r => r.TotalApprovedAmount) - }; + var culture = CultureInfo.GetCultureInfo("en-IN"); // pick desired locale + + var response = report + .Select(x => new + { + 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.SuccessResponse(response, "Expense report by project fetched successfully", 200)); // [Success Response] [memory:1] + return Ok(ApiResponse.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.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.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.ErrorResponse("An error occurred while fetching the expense report.", 500)); // [Error Response] [memory:1] + ApiResponse.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 })