|
|
|
@@ -26,6 +26,7 @@ public sealed class DashboardService : IDashboardService
|
|
|
|
|
private readonly IRepository<Rfq> _rfqs;
|
|
|
|
|
private readonly IRepository<LeaveRequest> _leaveRequests;
|
|
|
|
|
private readonly IRepository<SalesInvoice> _salesInvoices;
|
|
|
|
|
private readonly IRepository<PurchaseInvoice> _purchaseInvoices;
|
|
|
|
|
private readonly IReorderService _reorder;
|
|
|
|
|
|
|
|
|
|
// 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.
|
|
|
|
|
private const int OverdueAgeDays = 30;
|
|
|
|
|
|
|
|
|
|
// Monthly Purchases chart window: the current month plus the previous 11.
|
|
|
|
|
private const int ChartMonths = 12;
|
|
|
|
|
|
|
|
|
|
public DashboardService(
|
|
|
|
|
IRepository<StockLayer> layers, IRepository<Grn> grns, IRepository<PurchaseOrder> pos,
|
|
|
|
|
IRepository<Requisition> requisitions, IRepository<StockCount> counts, IRepository<Rfq> rfqs,
|
|
|
|
|
IRepository<LeaveRequest> leaveRequests, IRepository<SalesInvoice> salesInvoices,
|
|
|
|
|
IReorderService reorder)
|
|
|
|
|
IRepository<PurchaseInvoice> purchaseInvoices, IReorderService reorder)
|
|
|
|
|
{
|
|
|
|
|
_layers = layers;
|
|
|
|
|
_grns = grns;
|
|
|
|
@@ -47,6 +51,7 @@ public sealed class DashboardService : IDashboardService
|
|
|
|
|
_rfqs = rfqs;
|
|
|
|
|
_leaveRequests = leaveRequests;
|
|
|
|
|
_salesInvoices = salesInvoices;
|
|
|
|
|
_purchaseInvoices = purchaseInvoices;
|
|
|
|
|
_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))
|
|
|
|
|
.Pagination.TotalItems;
|
|
|
|
|
|
|
|
|
|
var monthlyPurchasesVsSales = await GetMonthlyPurchasesVsSalesAsync(branchId, salesInvoices, ct);
|
|
|
|
|
|
|
|
|
|
return new DashboardStatsDto(
|
|
|
|
|
lowStockAlerts, onHandTotal, onHandWarehouses, stockValuationTotal, stockValuationByWarehouse,
|
|
|
|
|
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();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|