Merge pull request 'fron build failure fix' (#89) from Dev into production

Reviewed-on: #89
This commit was merged in pull request #89.
This commit is contained in:
2026-09-25 06:58:55 +00:00
3 changed files with 63 additions and 3 deletions
@@ -23,7 +23,12 @@ public sealed record DashboardStatsDto(
int PendingCounts, int PendingCounts,
int OpenRfqs, int OpenRfqs,
int PendingLeaveRequests, int PendingLeaveRequests,
int OverdueSalesInvoices); int OverdueSalesInvoices,
IReadOnlyList<MonthlyPurchasesVsSalesDto> MonthlyPurchasesVsSales);
/// <summary>One bar in the Stock Valuation chart — total FIFO layer value for a warehouse.</summary> /// <summary>One bar in the Stock Valuation chart — total FIFO layer value for a warehouse.</summary>
public sealed record WarehouseValuationDto(int WarehouseId, decimal Total); public sealed record WarehouseValuationDto(int WarehouseId, decimal Total);
/// <summary>One month in the Monthly Purchases chart (Purchase Invoices page) — GrandTotal
/// of Posted purchase invoices vs Posted sales invoices. <c>Month</c> is "yyyy-MM".</summary>
public sealed record MonthlyPurchasesVsSalesDto(string Month, decimal Purchases, decimal Sales);
+48 -2
View File
@@ -26,6 +26,7 @@ public sealed class DashboardService : IDashboardService
private readonly IRepository<Rfq> _rfqs; private readonly IRepository<Rfq> _rfqs;
private readonly IRepository<LeaveRequest> _leaveRequests; private readonly IRepository<LeaveRequest> _leaveRequests;
private readonly IRepository<SalesInvoice> _salesInvoices; private readonly IRepository<SalesInvoice> _salesInvoices;
private readonly IRepository<PurchaseInvoice> _purchaseInvoices;
private readonly IReorderService _reorder; private readonly IReorderService _reorder;
// Neither SalesInvoice nor PurchaseInvoice carries a DueDate field (Phase 1 schema // Neither SalesInvoice nor PurchaseInvoice carries a DueDate field (Phase 1 schema
@@ -33,11 +34,14 @@ public sealed class DashboardService : IDashboardService
// balance older than this many days from its invoice date. // balance older than this many days from its invoice date.
private const int OverdueAgeDays = 30; private const int OverdueAgeDays = 30;
// Monthly Purchases chart window: the current month plus the previous 11.
private const int ChartMonths = 12;
public DashboardService( public DashboardService(
IRepository<StockLayer> layers, IRepository<Grn> grns, IRepository<PurchaseOrder> pos, IRepository<StockLayer> layers, IRepository<Grn> grns, IRepository<PurchaseOrder> pos,
IRepository<Requisition> requisitions, IRepository<StockCount> counts, IRepository<Rfq> rfqs, IRepository<Requisition> requisitions, IRepository<StockCount> counts, IRepository<Rfq> rfqs,
IRepository<LeaveRequest> leaveRequests, IRepository<SalesInvoice> salesInvoices, IRepository<LeaveRequest> leaveRequests, IRepository<SalesInvoice> salesInvoices,
IReorderService reorder) IRepository<PurchaseInvoice> purchaseInvoices, IReorderService reorder)
{ {
_layers = layers; _layers = layers;
_grns = grns; _grns = grns;
@@ -47,6 +51,7 @@ public sealed class DashboardService : IDashboardService
_rfqs = rfqs; _rfqs = rfqs;
_leaveRequests = leaveRequests; _leaveRequests = leaveRequests;
_salesInvoices = salesInvoices; _salesInvoices = salesInvoices;
_purchaseInvoices = purchaseInvoices;
_reorder = reorder; _reorder = reorder;
} }
@@ -103,9 +108,50 @@ public sealed class DashboardService : IDashboardService
var lowStockAlerts = (await _reorder.GetAlertsAsync(null, branchId, new PageQuery { Page = 1, PageSize = 1 }, ct)) var lowStockAlerts = (await _reorder.GetAlertsAsync(null, branchId, new PageQuery { Page = 1, PageSize = 1 }, ct))
.Pagination.TotalItems; .Pagination.TotalItems;
var monthlyPurchasesVsSales = await GetMonthlyPurchasesVsSalesAsync(branchId, salesInvoices, ct);
return new DashboardStatsDto( return new DashboardStatsDto(
lowStockAlerts, onHandTotal, onHandWarehouses, stockValuationTotal, stockValuationByWarehouse, lowStockAlerts, onHandTotal, onHandWarehouses, stockValuationTotal, stockValuationByWarehouse,
pendingApprovalPOs, pendingGrns, openRequisitions, pendingCounts, openRfqs, pendingApprovalPOs, pendingGrns, openRequisitions, pendingCounts, openRfqs,
pendingLeaveRequests, overdueSalesInvoices); pendingLeaveRequests, overdueSalesInvoices, monthlyPurchasesVsSales);
}
/// <summary>
/// Posted purchase vs sales invoice GrandTotals per calendar month (UTC) over the last
/// <see cref="ChartMonths"/> months, oldest first. Months with neither are omitted, so an
/// empty list means "no posted invoices yet". Purchases are dated by VendorInvoiceDate,
/// sales by InvoiceDate.
/// </summary>
private async Task<IReadOnlyList<MonthlyPurchasesVsSalesDto>> GetMonthlyPurchasesVsSalesAsync(
int? branchId, IQueryable<SalesInvoice> salesInvoices, CancellationToken ct)
{
var now = DateTime.UtcNow;
var since = new DateTime(now.Year, now.Month, 1, 0, 0, 0, DateTimeKind.Utc).AddMonths(-(ChartMonths - 1));
var purchaseInvoices = _purchaseInvoices.Query().AsNoTracking();
if (branchId is not null) purchaseInvoices = purchaseInvoices.Where(i => i.BranchId == branchId);
var purchases = await purchaseInvoices
.Where(i => i.Status == PurchaseInvoiceStatus.Posted && i.VendorInvoiceDate >= since)
.GroupBy(i => new { i.VendorInvoiceDate.Year, i.VendorInvoiceDate.Month })
.Select(g => new { g.Key.Year, g.Key.Month, Total = g.Sum(i => i.GrandTotal) })
.ToListAsync(ct);
var sales = await salesInvoices
.Where(i => i.Status == SalesInvoiceStatus.Posted && i.InvoiceDate >= since)
.GroupBy(i => new { i.InvoiceDate.Year, i.InvoiceDate.Month })
.Select(g => new { g.Key.Year, g.Key.Month, Total = g.Sum(i => i.GrandTotal) })
.ToListAsync(ct);
var purchasesByMonth = purchases.ToDictionary(p => (p.Year, p.Month), p => p.Total);
var salesByMonth = sales.ToDictionary(s => (s.Year, s.Month), s => s.Total);
return Enumerable.Range(0, ChartMonths)
.Select(offset => since.AddMonths(offset))
.Select(m => new MonthlyPurchasesVsSalesDto(
$"{m.Year:D4}-{m.Month:D2}",
purchasesByMonth.GetValueOrDefault((m.Year, m.Month)),
salesByMonth.GetValueOrDefault((m.Year, m.Month))))
.Where(m => m.Purchases != 0m || m.Sales != 0m)
.ToList();
} }
} }
+9
View File
@@ -19,4 +19,13 @@ export interface DashboardStats {
openRfqs: number openRfqs: number
pendingLeaveRequests: number pendingLeaveRequests: number
overdueSalesInvoices: number overdueSalesInvoices: number
monthlyPurchasesVsSales: MonthlyPurchasesVsSales[]
}
// One month of Posted purchase vs sales invoice GrandTotals; `month` is "yyyy-MM".
// Only months with activity are returned (last 12, oldest first).
export interface MonthlyPurchasesVsSales {
month: string
purchases: number
sales: number
} }