Files
2026-07-21 14:28:43 +07:00

649 lines
30 KiB
C#

using Indotalent.Infrastructure.Database;
using Indotalent.Data.Entities;
using Indotalent.Data.Enums;
using MediatR;
using Microsoft.EntityFrameworkCore;
namespace Indotalent.Features.Root.Home.Cqrs;
public class ChartDataPoint
{
public string? Label { get; set; }
public double SalesAmount { get; set; }
public double PurchaseAmount { get; set; }
}
public class ScmMetricDto
{
public string? Label { get; set; }
public string? Value { get; set; }
public string? Color { get; set; }
public double Percentage { get; set; }
}
public class ScmActivityDto
{
public string? Module { get; set; }
public string? Number { get; set; }
public string? Partner { get; set; }
public double Qty { get; set; }
public string? Status { get; set; }
public DateTime? Date { get; set; }
public string? Description { get; set; }
}
public class RecentPurchaseOrderDto
{
public DateTime? OrderDate { get; set; }
public string? Number { get; set; }
public string? VendorName { get; set; }
public decimal Amount { get; set; }
public string? Status { get; set; }
public int ItemCount { get; set; }
}
public class RecentGoodsReceiveDto
{
public DateTime? ReceiveDate { get; set; }
public string? Number { get; set; }
public string? PONumber { get; set; }
public string? Status { get; set; }
}
public class StatusCountDto
{
public string? Status { get; set; }
public int Count { get; set; }
public string? Color { get; set; }
}
public class DashboardScmResponse
{
public int PendingPR { get; set; }
public int PendingSQ { get; set; }
public int PendingGR { get; set; }
public int PendingDO { get; set; }
public int ActiveCustomer { get; set; }
public int ActiveVendor { get; set; }
public double InboundQtyMTD { get; set; }
public double OutboundQtyMTD { get; set; }
public decimal SalesMTD { get; set; }
public decimal PurchaseMTD { get; set; }
public double ChainAccuracyIndex { get; set; }
public decimal TotalReceivable { get; set; }
public decimal TotalPayable { get; set; }
public int TotalPR { get; set; }
public int TotalPO { get; set; }
public int TotalGR { get; set; }
public int TotalDO { get; set; }
public List<ChartDataPoint> MonthlySalesVsPurchase { get; set; } = new();
public List<ChartDataPoint> WeeklySalesVsPurchase { get; set; } = new();
public List<ScmMetricDto> DocGrid { get; set; } = new();
public List<ScmActivityDto> GlobalFlowLog { get; set; } = new();
public List<ScmMetricDto> TopProductsByQty { get; set; } = new();
public List<ScmMetricDto> CustGroupStats { get; set; } = new();
public List<ScmMetricDto> VendGroupStats { get; set; } = new();
public List<ScmMetricDto> CustCategoryStats { get; set; } = new();
public List<ScmMetricDto> VendCategoryStats { get; set; } = new();
public List<ScmMetricDto> PayMethodStats { get; set; } = new();
public List<ScmMetricDto> WhseInventoryLevel { get; set; } = new();
public List<ScmMetricDto> CashFlowMTD { get; set; } = new();
public List<ScmMetricDto> StockValuePerGroup { get; set; } = new();
public List<ScmMetricDto> VendorPOTotals { get; set; } = new();
public List<ScmMetricDto> ProductCategories { get; set; } = new();
public List<StatusCountDto> POStatusBreakdown { get; set; } = new();
public List<StatusCountDto> GRStatusBreakdown { get; set; } = new();
public List<StatusCountDto> PRStatusBreakdown { get; set; } = new();
public List<StatusCountDto> DOStatusBreakdown { get; set; } = new();
public List<RecentPurchaseOrderDto> RecentPOs { get; set; } = new();
public List<RecentGoodsReceiveDto> RecentGRs { get; set; } = new();
public string FormattedSales => FormatCurrency(SalesMTD);
public string FormattedPurchase => FormatCurrency(PurchaseMTD);
public string FormattedReceivable => FormatCurrency(TotalReceivable);
public string FormattedPayable => FormatCurrency(TotalPayable);
private static string FormatCurrency(decimal v) => v >= 1000000 ? "$" + (v / 1000000m).ToString("F2") + "M" : "$" + v.ToString("N0");
}
public record GetScmDashboardQuery() : IRequest<DashboardScmResponse>;
public class DashboardHandler : IRequestHandler<GetScmDashboardQuery, DashboardScmResponse>
{
private readonly AppDbContext _context;
public DashboardHandler(AppDbContext context) => _context = context;
public async Task<DashboardScmResponse> Handle(GetScmDashboardQuery request, CancellationToken ct)
{
var start = new DateTime(DateTime.Today.Year, DateTime.Today.Month, 1);
// --- Inventory Transactions (materialize with FK navigation via projection, no .Include) ---
var txRaw = await _context.InventoryTransaction.AsNoTracking()
.Where(x => x.MovementDate >= start)
.Select(x => new
{
x.MovementDate,
x.TransType,
x.Status,
x.Movement,
x.QtySCSys,
x.QtySCDelta,
x.WarehouseId,
x.ProductId,
x.ModuleCode,
x.ModuleName,
x.ModuleNumber
})
.ToListAsync(ct);
var warehouseIds = txRaw.Where(x => x.WarehouseId != null).Select(x => x.WarehouseId).Distinct().ToList();
var productIds = txRaw.Where(x => x.ProductId != null).Select(x => x.ProductId).Distinct().ToList();
var warehouses = await _context.Warehouse.AsNoTracking()
.Where(w => warehouseIds.Contains(w.Id))
.Select(w => new { w.Id, w.Name })
.ToListAsync(ct);
var products = await _context.Product.AsNoTracking()
.Where(p => productIds.Contains(p.Id))
.Select(p => new { p.Id, p.Name, p.ProductGroupId, p.UnitPrice })
.ToListAsync(ct);
var productGroupIds = products.Where(p => p.ProductGroupId != null).Select(p => p.ProductGroupId).Distinct().ToList();
var productGroups = await _context.ProductGroup.AsNoTracking()
.Where(g => productGroupIds.Contains(g.Id))
.Select(g => new { g.Id, g.Name })
.ToListAsync(ct);
var warehouseMap = warehouses.ToDictionary(w => w.Id, w => w.Name ?? "Main");
var productMap = products.ToDictionary(p => p.Id, p => p.Name ?? "N/A");
var productPriceMap = products.ToDictionary(p => p.Id, p => p.UnitPrice ?? 0);
var groupMap = productGroups.ToDictionary(g => g.Id, g => g.Name ?? "General");
// Chain Accuracy Index
var countData = txRaw.Where(x => x.ModuleCode == "COUNT").ToList();
double accuracy = 100;
if (countData.Count != 0)
{
var sys = countData.Sum(x => x.QtySCSys ?? 0);
var delta = countData.Sum(x => Math.Abs(x.QtySCDelta ?? 0));
accuracy = sys > 0 ? Math.Max(0, 100 - (delta / sys * 100)) : 100;
}
// Inbound / Outbound MTD
var inboundQty = txRaw.Where(x => x.TransType == InventoryTransType.In && x.Status == InventoryTransactionStatus.Confirmed)
.Sum(x => x.Movement ?? 0);
var outboundQty = txRaw.Where(x => x.TransType == InventoryTransType.Out && x.Status == InventoryTransactionStatus.Confirmed)
.Sum(x => x.Movement ?? 0);
// Whse Inventory Level
var whseLevel = txRaw
.GroupBy(x => x.WarehouseId)
.Select(g => new ScmMetricDto
{
Label = warehouseMap.GetValueOrDefault(g.Key ?? "", "Main"),
Value = g.Sum(x => (x.Movement ?? 0) * (x.TransType == InventoryTransType.In ? 1 : -1)).ToString("N0")
}).ToList();
// Stock Value Per Group
var stockValGroup = txRaw
.GroupBy(x => productIds.Contains(x.ProductId ?? "") ? groupMap.GetValueOrDefault(
products.FirstOrDefault(p => p.Id == x.ProductId)?.ProductGroupId ?? "", "General") : "General")
.Select(g => new ScmMetricDto
{
Label = g.Key,
Value = g.Sum(x =>
{
var price = productPriceMap.GetValueOrDefault(x.ProductId ?? "", 0);
return (decimal)((x.Movement ?? 0) * (x.TransType == InventoryTransType.In ? 1 : -1)) * price;
}).ToString("N0")
}).ToList();
// --- Customer Groups (no .Include - use CustomerGroupId FK) ---
var custRaw = await _context.Customer.AsNoTracking()
.Select(x => new { x.CustomerGroupId })
.ToListAsync(ct);
var custGroupIds = custRaw.Where(x => x.CustomerGroupId != null).Select(x => x.CustomerGroupId).Distinct().ToList();
var custGroupsDb = await _context.CustomerGroup.AsNoTracking()
.Where(g => custGroupIds.Contains(g.Id))
.Select(g => new { g.Id, g.Name })
.ToListAsync(ct);
var custGroupMap = custGroupsDb.ToDictionary(g => g.Id, g => g.Name ?? "General");
var custGroups = custRaw
.GroupBy(x => custGroupMap.GetValueOrDefault(x.CustomerGroupId ?? "", "General"))
.Select(g => new ScmMetricDto { Label = g.Key, Value = g.Count().ToString() })
.ToList();
// --- Vendor Groups (no .Include - use VendorGroupId FK) ---
var vendRaw = await _context.Vendor.AsNoTracking()
.Select(x => new { x.VendorGroupId })
.ToListAsync(ct);
var vendGroupIds = vendRaw.Where(x => x.VendorGroupId != null).Select(x => x.VendorGroupId).Distinct().ToList();
var vendGroupsDb = await _context.VendorGroup.AsNoTracking()
.Where(g => vendGroupIds.Contains(g.Id))
.Select(g => new { g.Id, g.Name })
.ToListAsync(ct);
var vendGroupMap = vendGroupsDb.ToDictionary(g => g.Id, g => g.Name ?? "General");
var vendGroups = vendRaw
.GroupBy(x => vendGroupMap.GetValueOrDefault(x.VendorGroupId ?? "", "General"))
.Select(g => new ScmMetricDto { Label = g.Key, Value = g.Count().ToString() })
.ToList();
// --- Customer Categories ---
var custCatRaw = await _context.Customer.AsNoTracking()
.Select(x => new { x.CustomerCategoryId })
.ToListAsync(ct);
var custCatIds = custCatRaw.Where(x => x.CustomerCategoryId != null).Select(x => x.CustomerCategoryId).Distinct().ToList();
var custCatDb = await _context.CustomerCategory.AsNoTracking()
.Where(c => custCatIds.Contains(c.Id))
.Select(c => new { c.Id, c.Name })
.ToListAsync(ct);
var custCatMap = custCatDb.ToDictionary(c => c.Id, c => c.Name ?? "General");
var custCats = custCatRaw
.GroupBy(x => custCatMap.GetValueOrDefault(x.CustomerCategoryId ?? "", "General"))
.Select(g => new ScmMetricDto { Label = g.Key, Value = g.Count().ToString() })
.ToList();
// --- Vendor Categories ---
var vendCatRaw = await _context.Vendor.AsNoTracking()
.Select(x => new { x.VendorCategoryId })
.ToListAsync(ct);
var vendCatIds = vendCatRaw.Where(x => x.VendorCategoryId != null).Select(x => x.VendorCategoryId).Distinct().ToList();
var vendCatDb = await _context.VendorCategory.AsNoTracking()
.Where(c => vendCatIds.Contains(c.Id))
.Select(c => new { c.Id, c.Name })
.ToListAsync(ct);
var vendCatMap = vendCatDb.ToDictionary(c => c.Id, c => c.Name ?? "General");
var vendCats = vendCatRaw
.GroupBy(x => vendCatMap.GetValueOrDefault(x.VendorCategoryId ?? "", "General"))
.Select(g => new ScmMetricDto { Label = g.Key, Value = g.Count().ToString() })
.ToList();
// --- Payment Methods (no .Include) ---
var payRaw = await _context.PaymentReceive.AsNoTracking()
.Select(x => new { x.PaymentMethodId })
.ToListAsync(ct);
var payMethodIds = payRaw.Where(x => x.PaymentMethodId != null).Select(x => x.PaymentMethodId).Distinct().ToList();
var payMethodDb = await _context.PaymentMethod.AsNoTracking()
.Where(m => payMethodIds.Contains(m.Id))
.Select(m => new { m.Id, m.Name })
.ToListAsync(ct);
var payMethodMap = payMethodDb.ToDictionary(m => m.Id, m => m.Name ?? "Cash");
var payMethods = payRaw
.GroupBy(x => payMethodMap.GetValueOrDefault(x.PaymentMethodId ?? "", "Cash"))
.Select(g => new ScmMetricDto { Label = g.Key, Value = g.Count().ToString() })
.ToList();
// --- Top Products by Qty (no .Include - use ProductId FK) ---
var soiRaw = await _context.SalesOrderItem.AsNoTracking()
.Select(x => new { x.ProductId, x.Quantity })
.ToListAsync(ct);
var topProdIds = soiRaw.Where(x => x.ProductId != null).Select(x => x.ProductId).Distinct().ToList();
var topProdDb = await _context.Product.AsNoTracking()
.Where(p => topProdIds.Contains(p.Id))
.Select(p => new { p.Id, p.Name })
.ToListAsync(ct);
var topProdMap = topProdDb.ToDictionary(p => p.Id, p => p.Name ?? "N/A");
var topProds = soiRaw
.Where(x => x.ProductId != null)
.GroupBy(x => x.ProductId!)
.Select(g => new ScmMetricDto { Label = topProdMap.GetValueOrDefault(g.Key, "N/A"), Value = g.Sum(x => x.Quantity ?? 0).ToString("N0") })
.OrderByDescending(x => double.Parse(x.Value ?? "0"))
.Take(10)
.ToList();
// --- Global Flow Log ---
var globalLogs = await _context.InventoryTransaction
.AsNoTracking()
.OrderByDescending(x => x.MovementDate)
.Take(30)
.Select(x => new ScmActivityDto
{
Module = x.ModuleName,
Number = x.ModuleNumber,
Partner = x.WarehouseId,
Qty = x.Movement ?? 0,
Status = x.Status.ToString(),
Date = x.MovementDate,
Description = x.ModuleCode
}).ToListAsync(ct);
var logWhIds = globalLogs.Where(x => x.Partner != null).Select(x => x.Partner).Distinct().ToList();
var logWhDb = await _context.Warehouse.AsNoTracking()
.Where(w => logWhIds.Contains(w.Id))
.Select(w => new { w.Id, w.Name })
.ToListAsync(ct);
var logWhMap = logWhDb.ToDictionary(w => w.Id, w => w.Name ?? "N/A");
foreach (var log in globalLogs)
{
if (log.Partner != null && logWhMap.TryGetValue(log.Partner, out var whName))
log.Partner = whName;
}
// --- Doc Grid ---
var sqCount = await _context.SalesQuotation.CountAsync(ct);
var soCount = await _context.SalesOrder.CountAsync(ct);
var prCount = await _context.PurchaseRequisition.CountAsync(ct);
var poCount = await _context.PurchaseOrder.CountAsync(ct);
var grCount = await _context.GoodsReceive.CountAsync(ct);
var doCount = await _context.DeliveryOrder.CountAsync(ct);
var srCount = await _context.SalesReturn.CountAsync(ct);
var prRetCount = await _context.PurchaseReturn.CountAsync(ct);
var invCount = await _context.Invoice.CountAsync(ct);
var billCount = await _context.Bill.CountAsync(ct);
var cnCount = await _context.CreditNote.CountAsync(ct);
var dnCount = await _context.DebitNote.CountAsync(ct);
var payRecCount = await _context.PaymentReceive.CountAsync(ct);
var payDisbCount = await _context.PaymentDisburse.CountAsync(ct);
var docGrid = new List<ScmMetricDto>
{
new() { Label = "Sales Quotation", Value = sqCount.ToString(), Color = "#3b82f6" },
new() { Label = "Sales Order", Value = soCount.ToString(), Color = "#10b981" },
new() { Label = "Purchase Req", Value = prCount.ToString(), Color = "#f59e0b" },
new() { Label = "Purchase Order", Value = poCount.ToString(), Color = "#ef4444" },
new() { Label = "Goods Receive", Value = grCount.ToString(), Color = "#64748b" },
new() { Label = "Delivery Order", Value = doCount.ToString(), Color = "#06b6d4" },
new() { Label = "Sales Return", Value = srCount.ToString(), Color = "#8b5cf6" },
new() { Label = "Purch. Return", Value = prRetCount.ToString(), Color = "#f43f5e" },
new() { Label = "Invoice", Value = invCount.ToString(), Color = "#14b8a6" },
new() { Label = "Bill", Value = billCount.ToString(), Color = "#f97316" },
new() { Label = "Credit Note", Value = cnCount.ToString(), Color = "#ec4899" },
new() { Label = "Debit Note", Value = dnCount.ToString(), Color = "#0ea5e9" },
new() { Label = "Pay Receive", Value = payRecCount.ToString(), Color = "#84cc16" },
new() { Label = "Pay Disburse", Value = payDisbCount.ToString(), Color = "#d946ef" }
};
// --- Monthly Sales vs Purchase ---
var monthlyLabels = new[] { "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" };
var soMonths = await _context.SalesOrder
.Where(x => x.OrderDate != null)
.Select(x => x.OrderDate!.Value)
.ToListAsync(ct);
var poMonths = await _context.PurchaseOrder
.Where(x => x.OrderDate != null)
.Select(x => x.OrderDate!.Value)
.ToListAsync(ct);
var allDates = soMonths.Concat(poMonths)
.Select(d => new { d.Year, d.Month })
.Distinct()
.OrderByDescending(x => x.Year).ThenByDescending(x => x.Month)
.Take(6)
.Reverse()
.ToList();
var monthlyData = new List<ChartDataPoint>();
foreach (var ym in allDates)
{
var monthStartDt = new DateTime(ym.Year, ym.Month, 1);
var monthEndDt = monthStartDt.AddMonths(1);
var salesAmount = await _context.SalesOrder
.Where(x => x.OrderDate >= monthStartDt && x.OrderDate < monthEndDt)
.SumAsync(x => (double?)x.AfterTaxAmount ?? 0, ct);
var purchaseAmount = await _context.PurchaseOrder
.Where(x => x.OrderDate >= monthStartDt && x.OrderDate < monthEndDt)
.SumAsync(x => (double?)x.AfterTaxAmount ?? 0, ct);
monthlyData.Add(new ChartDataPoint
{
Label = monthlyLabels[ym.Month - 1],
SalesAmount = Math.Round(salesAmount / 1000.0, 1),
PurchaseAmount = Math.Round(purchaseAmount / 1000.0, 1)
});
}
// --- Weekly Sales vs Purchase ---
var soWeeks = await _context.SalesOrder
.Where(x => x.OrderDate != null)
.Select(x => x.OrderDate!.Value)
.ToListAsync(ct);
var poWeeks = await _context.PurchaseOrder
.Where(x => x.OrderDate != null)
.Select(x => x.OrderDate!.Value)
.ToListAsync(ct);
var allWeekStarts = soWeeks.Concat(poWeeks)
.Select(d => d.AddDays(-(int)d.DayOfWeek))
.Distinct()
.OrderByDescending(w => w)
.Take(6)
.Reverse()
.ToList();
var weeklyData = new List<ChartDataPoint>();
foreach (var weekStart in allWeekStarts)
{
var weekEnd = weekStart.AddDays(7);
var salesAmount = await _context.SalesOrder
.Where(x => x.OrderDate >= weekStart && x.OrderDate < weekEnd)
.SumAsync(x => (double?)x.AfterTaxAmount ?? 0, ct);
var purchaseAmount = await _context.PurchaseOrder
.Where(x => x.OrderDate >= weekStart && x.OrderDate < weekEnd)
.SumAsync(x => (double?)x.AfterTaxAmount ?? 0, ct);
weeklyData.Add(new ChartDataPoint
{
Label = $"Wk {weekStart:MM/dd}",
SalesAmount = Math.Round(salesAmount / 1000.0, 1),
PurchaseAmount = Math.Round(purchaseAmount / 1000.0, 1)
});
}
// --- Vendor PO Totals ---
var poVendorRaw = await _context.PurchaseOrder.AsNoTracking()
.Select(x => new { x.VendorId, x.AfterTaxAmount })
.ToListAsync(ct);
var poVendorIds = poVendorRaw.Where(x => x.VendorId != null).Select(x => x.VendorId).Distinct().ToList();
var poVendorDb = await _context.Vendor.AsNoTracking()
.Where(v => poVendorIds.Contains(v.Id))
.Select(v => new { v.Id, v.Name })
.ToListAsync(ct);
var poVendorMap = poVendorDb.ToDictionary(v => v.Id, v => v.Name ?? "N/A");
var vendorPOTotals = poVendorRaw
.Where(x => x.VendorId != null)
.GroupBy(x => x.VendorId!)
.Select(g => new ScmMetricDto
{
Label = poVendorMap.GetValueOrDefault(g.Key, "N/A"),
Value = g.Sum(x => x.AfterTaxAmount ?? 0).ToString("N0"),
Color = "#6366f1"
})
.OrderByDescending(x => decimal.TryParse(x.Value, out var v) ? v : 0)
.Take(15)
.ToList();
// --- PO Status Breakdown ---
var poStatusRaw = await _context.PurchaseOrder.AsNoTracking()
.Select(x => x.OrderStatus)
.ToListAsync(ct);
var poStatusBreakdown = poStatusRaw
.GroupBy(x => x)
.Select(g => new StatusCountDto
{
Status = g.Key.ToString(),
Count = g.Count(),
Color = GetPOStatusColor(g.Key.ToString())
}).ToList();
// --- GR Status Breakdown ---
var grStatusRaw = await _context.GoodsReceive.AsNoTracking()
.Select(x => x.Status)
.ToListAsync(ct);
var grStatusBreakdown = grStatusRaw
.GroupBy(x => x)
.Select(g => new StatusCountDto
{
Status = g.Key.ToString(),
Count = g.Count(),
Color = GetGRStatusColor(g.Key.ToString())
}).ToList();
// --- PR Status Breakdown ---
var prStatusRaw = await _context.PurchaseRequisition.AsNoTracking()
.Select(x => x.RequisitionStatus)
.ToListAsync(ct);
var prStatusBreakdown = prStatusRaw
.GroupBy(x => x)
.Select(g => new StatusCountDto
{
Status = g.Key.ToString(),
Count = g.Count(),
Color = GetPOStatusColor(g.Key.ToString())
}).ToList();
// --- DO Status Breakdown ---
var doStatusRaw = await _context.DeliveryOrder.AsNoTracking()
.Select(x => x.Status)
.ToListAsync(ct);
var doStatusBreakdown = doStatusRaw
.GroupBy(x => x)
.Select(g => new StatusCountDto
{
Status = g.Key.ToString(),
Count = g.Count(),
Color = GetGRStatusColor(g.Key.ToString())
}).ToList();
// --- Recent POs ---
var recentPOsRaw = await _context.PurchaseOrder.AsNoTracking()
.OrderByDescending(x => x.OrderDate)
.Take(10)
.Select(x => new
{
x.OrderDate,
x.AutoNumber,
x.VendorId,
x.AfterTaxAmount,
x.OrderStatus,
ItemCount = x.PurchaseOrderItemList != null ? x.PurchaseOrderItemList.Count : 0
})
.ToListAsync(ct);
var recentPOVendorIds = recentPOsRaw.Where(x => x.VendorId != null).Select(x => x.VendorId).Distinct().ToList();
var recentPOVendorDb = await _context.Vendor.AsNoTracking()
.Where(v => recentPOVendorIds.Contains(v.Id))
.Select(v => new { v.Id, v.Name })
.ToListAsync(ct);
var recentPOVendorMap = recentPOVendorDb.ToDictionary(v => v.Id, v => v.Name ?? "N/A");
var recentPOs = recentPOsRaw.Select(x => new RecentPurchaseOrderDto
{
OrderDate = x.OrderDate,
Number = x.AutoNumber,
VendorName = recentPOVendorMap.GetValueOrDefault(x.VendorId ?? "", "N/A"),
Amount = x.AfterTaxAmount ?? 0,
Status = x.OrderStatus.ToString(),
ItemCount = x.ItemCount
}).ToList();
// --- Recent GRs ---
var recentGRsRaw = await _context.GoodsReceive.AsNoTracking()
.OrderByDescending(x => x.ReceiveDate)
.Take(10)
.Select(x => new
{
x.ReceiveDate,
x.AutoNumber,
x.PurchaseOrderId,
x.Status
})
.ToListAsync(ct);
var recentGRPOIds = recentGRsRaw.Where(x => x.PurchaseOrderId != null).Select(x => x.PurchaseOrderId).Distinct().ToList();
var recentGRPODb = await _context.PurchaseOrder.AsNoTracking()
.Where(po => recentGRPOIds.Contains(po.Id))
.Select(po => new { po.Id, po.AutoNumber })
.ToListAsync(ct);
var recentGRPOMap = recentGRPODb.ToDictionary(po => po.Id, po => po.AutoNumber ?? "N/A");
var recentGRs = recentGRsRaw.Select(x => new RecentGoodsReceiveDto
{
ReceiveDate = x.ReceiveDate,
Number = x.AutoNumber,
PONumber = recentGRPOMap.GetValueOrDefault(x.PurchaseOrderId ?? "", "N/A"),
Status = x.Status.ToString()
}).ToList();
// --- Product Categories for donut ---
var prodCatRaw = await _context.Product.AsNoTracking()
.Select(x => new { x.ProductGroupId })
.ToListAsync(ct);
var prodCatIds = prodCatRaw.Where(x => x.ProductGroupId != null).Select(x => x.ProductGroupId).Distinct().ToList();
var prodCatDb = await _context.ProductGroup.AsNoTracking()
.Where(g => prodCatIds.Contains(g.Id))
.Select(g => new { g.Id, g.Name })
.ToListAsync(ct);
var prodCatMap = prodCatDb.ToDictionary(g => g.Id, g => g.Name ?? "General");
var productCategories = prodCatRaw
.GroupBy(x => prodCatMap.GetValueOrDefault(x.ProductGroupId ?? "", "General"))
.Select(g => new ScmMetricDto { Label = g.Key, Value = g.Count().ToString() })
.ToList();
// --- Invoice Receivable ---
var totalReceivable = await _context.Invoice.AsNoTracking()
.Where(x => x.InvoiceStatus == InvoiceStatus.Confirmed || x.InvoiceStatus == InvoiceStatus.PartialPaid)
.SumAsync(x => x.SalesOrder != null ? (x.SalesOrder.AfterTaxAmount ?? 0) : 0, ct);
// --- Bill Payable ---
var totalPayable = await _context.Bill.AsNoTracking()
.Where(x => x.BillStatus == BillStatus.Confirmed || x.BillStatus == BillStatus.PartialPaid)
.SumAsync(x => x.PurchaseOrder != null ? (x.PurchaseOrder.AfterTaxAmount ?? 0) : 0, ct);
var res = new DashboardScmResponse
{
PendingPR = await _context.PurchaseRequisition.CountAsync(x => x.RequisitionStatus == PurchaseRequisitionStatus.Draft, ct),
PendingSQ = await _context.SalesQuotation.CountAsync(x => x.QuotationStatus == SalesQuotationStatus.Draft, ct),
PendingGR = await _context.GoodsReceive.CountAsync(x => x.Status == GoodsReceiveStatus.Draft, ct),
PendingDO = await _context.DeliveryOrder.CountAsync(x => x.Status == DeliveryOrderStatus.Draft, ct),
ActiveCustomer = await _context.Customer.CountAsync(ct),
ActiveVendor = await _context.Vendor.CountAsync(ct),
InboundQtyMTD = inboundQty,
OutboundQtyMTD = outboundQty,
SalesMTD = await _context.SalesOrder.Where(x => x.OrderDate >= start).SumAsync(x => x.AfterTaxAmount ?? 0, ct),
PurchaseMTD = await _context.PurchaseOrder.Where(x => x.OrderDate >= start).SumAsync(x => x.AfterTaxAmount ?? 0, ct),
TotalReceivable = totalReceivable,
TotalPayable = totalPayable,
ChainAccuracyIndex = Math.Round(accuracy, 1),
TotalPR = prCount,
TotalPO = poCount,
TotalGR = grCount,
TotalDO = doCount,
DocGrid = docGrid,
GlobalFlowLog = globalLogs,
TopProductsByQty = topProds,
CustGroupStats = custGroups,
VendGroupStats = vendGroups,
CustCategoryStats = custCats,
VendCategoryStats = vendCats,
PayMethodStats = payMethods,
WhseInventoryLevel = whseLevel,
StockValuePerGroup = stockValGroup,
CashFlowMTD = new List<ScmMetricDto>
{
new() { Label = "Inflow (Receive)", Value = (await _context.PaymentReceive.Where(x => x.PaymentDate >= start).SumAsync(x => x.PaymentAmount ?? 0, ct)).ToString("N0"), Color = "#10b981" },
new() { Label = "Outflow (Disburse)", Value = (await _context.PaymentDisburse.Where(x => x.PaymentDate >= start).SumAsync(x => x.PaymentAmount ?? 0, ct)).ToString("N0"), Color = "#ef4444" }
},
MonthlySalesVsPurchase = monthlyData,
WeeklySalesVsPurchase = weeklyData,
VendorPOTotals = vendorPOTotals,
ProductCategories = productCategories,
POStatusBreakdown = poStatusBreakdown,
GRStatusBreakdown = grStatusBreakdown,
PRStatusBreakdown = prStatusBreakdown,
DOStatusBreakdown = doStatusBreakdown,
RecentPOs = recentPOs,
RecentGRs = recentGRs
};
return res;
}
private static string GetPOStatusColor(string status) => status switch
{
"Draft" => "#f59e0b",
"Confirmed" => "#2563eb",
"Closed" => "#10b981",
"Cancelled" => "#e11d48",
_ => "#64748b"
};
private static string GetGRStatusColor(string status) => status switch
{
"Draft" => "#f59e0b",
"Confirmed" => "#2563eb",
"Closed" => "#10b981",
"Cancelled" => "#e11d48",
_ => "#64748b"
};
}