initial commit
This commit is contained in:
@@ -0,0 +1,481 @@
|
||||
using Indotalent.Infrastructure.Database;
|
||||
using Indotalent.Data.Entities;
|
||||
using Indotalent.Data.Enums;
|
||||
using MediatR;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Indotalent.Features.Root.Home.Cqrs;
|
||||
|
||||
// Chart data
|
||||
public class ChartDataPoint
|
||||
{
|
||||
public string? Label { get; set; }
|
||||
public double SalesAmount { get; set; }
|
||||
public double PurchaseAmount { get; set; }
|
||||
}
|
||||
|
||||
// WMS-specific DTOs
|
||||
public class WarehouseKpiDto
|
||||
{
|
||||
public string? WarehouseName { get; set; }
|
||||
public double StockQty { get; set; }
|
||||
public int SkuCount { get; set; }
|
||||
public decimal InventoryValue { get; set; }
|
||||
public string? Color { get; set; }
|
||||
}
|
||||
|
||||
public class WmsModuleBreakdownDto
|
||||
{
|
||||
public string? Label { get; set; }
|
||||
public int Count { get; set; }
|
||||
public double Quantity { get; set; }
|
||||
public string? Color { get; set; }
|
||||
}
|
||||
|
||||
public class WmsPipelineSummaryDto
|
||||
{
|
||||
public string? Label { get; set; }
|
||||
public string? Value { get; set; }
|
||||
public string? IconColorClass { get; set; }
|
||||
}
|
||||
|
||||
public class WmsConversionMetricDto
|
||||
{
|
||||
public string? Label { get; set; }
|
||||
public double Current { get; set; }
|
||||
public double Target { get; set; }
|
||||
public double PctValue { get; set; }
|
||||
public string? BarColor { get; set; }
|
||||
}
|
||||
|
||||
public class WmsRecentTransactionDto
|
||||
{
|
||||
public string? Number { get; set; }
|
||||
public string? Description { get; set; }
|
||||
public string? Module { get; set; }
|
||||
public double Quantity { get; set; }
|
||||
public string? Type { get; set; }
|
||||
public string? Status { get; set; }
|
||||
public DateTimeOffset? Date { get; set; }
|
||||
}
|
||||
|
||||
public class WmsActivityFeedDto
|
||||
{
|
||||
public string? Title { get; set; }
|
||||
public string? Detail { get; set; }
|
||||
public string? TimeAgo { get; set; }
|
||||
public string? ChipLabel { get; set; }
|
||||
public string? ChipColor { get; set; }
|
||||
public string? AvatarBg { get; set; }
|
||||
public string? AvatarText { get; set; }
|
||||
}
|
||||
|
||||
public class WmsStockMovementPointDto
|
||||
{
|
||||
public string? Label { get; set; }
|
||||
public double InQty { get; set; }
|
||||
public double OutQty { get; set; }
|
||||
}
|
||||
|
||||
public class WmsYearlyMovementDto
|
||||
{
|
||||
public string? Category { get; set; }
|
||||
public double Qty { get; set; }
|
||||
public string? Color { get; set; }
|
||||
}
|
||||
|
||||
public class WmsTopProductDto
|
||||
{
|
||||
public string? ProductName { get; set; }
|
||||
public double TotalMovement { get; set; }
|
||||
public string? Color { get; set; }
|
||||
}
|
||||
|
||||
public class DashboardWmsResponse
|
||||
{
|
||||
// Core KPIs
|
||||
public double TotalStockQty { get; set; }
|
||||
public decimal InventoryValue { get; set; }
|
||||
public int TotalSku { get; set; }
|
||||
public int WarehouseCount { get; set; }
|
||||
public double StockAccuracy { get; set; }
|
||||
public double OrderFulfillmentRate { get; set; }
|
||||
|
||||
// Monthly movement
|
||||
public double MonthlyInboundQty { get; set; }
|
||||
public double MonthlyOutboundQty { get; set; }
|
||||
public int MonthlyTransactionCount { get; set; }
|
||||
|
||||
// Yearly
|
||||
public double YearlyInboundQty { get; set; }
|
||||
public double YearlyOutboundQty { get; set; }
|
||||
|
||||
// Counts
|
||||
public int PendingInbound { get; set; }
|
||||
public int PendingOutbound { get; set; }
|
||||
public int ScrapCount { get; set; }
|
||||
public int AdjustmentCount { get; set; }
|
||||
public int GrCount { get; set; }
|
||||
public int DoCount { get; set; }
|
||||
public int TransferCount { get; set; }
|
||||
public int StockCountCount { get; set; }
|
||||
|
||||
// Collections
|
||||
public List<WarehouseKpiDto> WarehouseKpis { get; set; } = new();
|
||||
public List<WmsModuleBreakdownDto> ModuleBreakdown { get; set; } = new();
|
||||
public List<WmsPipelineSummaryDto> PipelineSummary { get; set; } = new();
|
||||
public List<WmsConversionMetricDto> ConversionMetrics { get; set; } = new();
|
||||
public List<WmsRecentTransactionDto> RecentTransactions { get; set; } = new();
|
||||
public List<WmsActivityFeedDto> ActivityFeeds { get; set; } = new();
|
||||
public List<ChartDataPoint> RevenueTrend { get; set; } = new();
|
||||
public List<WmsStockMovementPointDto> StockMovementTrend { get; set; } = new();
|
||||
public List<WmsYearlyMovementDto> YearlyMovement { get; set; } = new();
|
||||
public List<WmsTopProductDto> TopProducts { get; set; } = new();
|
||||
|
||||
// Formatted
|
||||
public string FormattedStock => FormatNumber((decimal)TotalStockQty);
|
||||
public string FormattedInvValue => FormatNumber(InventoryValue);
|
||||
public string FormattedMonthlyIn => FormatNumber((decimal)MonthlyInboundQty);
|
||||
public string FormattedMonthlyOut => FormatNumber((decimal)MonthlyOutboundQty);
|
||||
|
||||
private static string FormatNumber(decimal v)
|
||||
{
|
||||
if (v >= 1_000_000) return (v / 1_000_000m).ToString("F2") + "M";
|
||||
if (v >= 1_000) return (v / 1_000m).ToString("F1") + "K";
|
||||
return v.ToString("N0");
|
||||
}
|
||||
}
|
||||
|
||||
public record GetWmsDashboardQuery() : IRequest<DashboardWmsResponse>;
|
||||
|
||||
public class DashboardHandler : IRequestHandler<GetWmsDashboardQuery, DashboardWmsResponse>
|
||||
{
|
||||
private readonly AppDbContext _context;
|
||||
|
||||
public DashboardHandler(AppDbContext context) => _context = context;
|
||||
|
||||
public async Task<DashboardWmsResponse> Handle(GetWmsDashboardQuery request, CancellationToken ct)
|
||||
{
|
||||
var today = DateTime.Today;
|
||||
var monthStart = new DateTime(today.Year, today.Month, 1);
|
||||
var yearStart = new DateTime(today.Year, 1, 1);
|
||||
|
||||
// 1. Top 1000 recent confirmed inventory transactions (with product & warehouse)
|
||||
var topTxs = await _context.InventoryTransaction
|
||||
.AsNoTracking()
|
||||
.Where(x => x.ProductId != null && x.WarehouseId != null && x.Status == InventoryTransactionStatus.Confirmed)
|
||||
.OrderByDescending(x => x.CreatedAt)
|
||||
.Take(1000)
|
||||
.Select(x => new
|
||||
{
|
||||
x.ModuleCode,
|
||||
x.ModuleNumber,
|
||||
x.ModuleName,
|
||||
x.MovementDate,
|
||||
x.Movement,
|
||||
x.TransType,
|
||||
x.Stock,
|
||||
x.CreatedAt,
|
||||
ProductId = x.ProductId!,
|
||||
ProductName = x.Product!.Name,
|
||||
ProductNumber = x.Product!.AutoNumber,
|
||||
UnitPrice = x.Product!.UnitPrice ?? 0,
|
||||
WarehouseId = x.WarehouseId!,
|
||||
WarehouseName = x.Warehouse!.Name
|
||||
})
|
||||
.ToListAsync(ct);
|
||||
|
||||
// Fallback: if table is empty, use count-based metrics from entity tables
|
||||
var grCount = await _context.GoodsReceive.AsNoTracking().CountAsync(ct);
|
||||
var doCount = await _context.DeliveryOrder.AsNoTracking().CountAsync(ct);
|
||||
var scrapCount = await _context.Scrapping.AsNoTracking().CountAsync(ct);
|
||||
var adjCount = await _context.PositiveAdjustment.AsNoTracking().CountAsync(ct)
|
||||
+ await _context.NegativeAdjustment.AsNoTracking().CountAsync(ct);
|
||||
var transferCount = await _context.TransferIn.AsNoTracking().CountAsync(ct);
|
||||
var stockCountCount = await _context.StockCount.AsNoTracking().CountAsync(ct);
|
||||
var pendingIn = await _context.GoodsReceive.AsNoTracking().CountAsync(x => x.Status == GoodsReceiveStatus.Draft, ct);
|
||||
var pendingOut = await _context.DeliveryOrder.AsNoTracking().CountAsync(x => x.Status == DeliveryOrderStatus.Draft, ct);
|
||||
|
||||
// 2. Aggregate from top transactions
|
||||
var totalStockQty = topTxs.Sum(x => x.Stock ?? 0);
|
||||
var totalSku = topTxs.Select(x => x.ProductId).Distinct().Count();
|
||||
var warehouseCount = topTxs.Select(x => x.WarehouseId).Distinct().Count();
|
||||
var inventoryValue = topTxs.Sum(x => (decimal)(x.Stock ?? 0) * x.UnitPrice);
|
||||
|
||||
// ModuleCode uses SHORT codes: GR, DO, ADJ+, ADJ-, COUNT, SCRP, TO-IN, TO-OUT, SRN, PRN
|
||||
var inboundModules = new HashSet<string> { "GR", "ADJ+", "TO-IN", "PRN" };
|
||||
var outboundModules = new HashSet<string> { "DO", "ADJ-", "TO-OUT", "SCRP", "SRN" };
|
||||
|
||||
bool IsInbound(string? mc) => mc != null && inboundModules.Contains(mc);
|
||||
bool IsOutbound(string? mc) => mc != null && outboundModules.Contains(mc);
|
||||
|
||||
var monthlyInbound = topTxs
|
||||
.Where(x => x.MovementDate >= monthStart && IsInbound(x.ModuleCode))
|
||||
.Sum(x => Math.Abs(x.Movement ?? 0));
|
||||
var monthlyOutbound = topTxs
|
||||
.Where(x => x.MovementDate >= monthStart && IsOutbound(x.ModuleCode))
|
||||
.Sum(x => Math.Abs(x.Movement ?? 0));
|
||||
var monthlyTxCount = topTxs.Count(x => x.MovementDate >= monthStart);
|
||||
|
||||
var yearlyInbound = topTxs
|
||||
.Where(x => x.MovementDate >= yearStart && IsInbound(x.ModuleCode))
|
||||
.Sum(x => Math.Abs(x.Movement ?? 0));
|
||||
var yearlyOutbound = topTxs
|
||||
.Where(x => x.MovementDate >= yearStart && IsOutbound(x.ModuleCode))
|
||||
.Sum(x => Math.Abs(x.Movement ?? 0));
|
||||
|
||||
// 3. Stock Accuracy from StockCount transactions (ModuleCode = "COUNT")
|
||||
var countTxs = topTxs.Where(x => x.ModuleCode == "COUNT").ToList();
|
||||
var stockAccuracy = 100.0;
|
||||
if (countTxs.Any())
|
||||
{
|
||||
var totalSys = countTxs.Sum(x => x.Stock ?? 0);
|
||||
var totalDelta = countTxs.Sum(x => Math.Abs((x.Movement ?? 0) - (x.Stock ?? 0)));
|
||||
stockAccuracy = totalSys > 0 ? Math.Max(0, Math.Round(100 - (totalDelta / totalSys * 100), 1)) : 100;
|
||||
}
|
||||
|
||||
// 4. Order Fulfillment
|
||||
var totalDo = await _context.DeliveryOrder.AsNoTracking().CountAsync(x => x.Status != DeliveryOrderStatus.Draft, ct);
|
||||
var doConfirmed = await _context.DeliveryOrder.AsNoTracking().CountAsync(x => x.Status == DeliveryOrderStatus.Confirmed, ct);
|
||||
var fulfPct = totalDo > 0 ? Math.Round((double)doConfirmed / totalDo * 100, 1) : 0;
|
||||
|
||||
// 5. Warehouse KPIs (group by warehouse from top txs)
|
||||
var whColors = new[] { "#2563eb", "#10b981", "#f59e0b", "#8b5cf6", "#ec4899", "#06b6d4" };
|
||||
var warehouseKpis = topTxs
|
||||
.GroupBy(x => new { x.WarehouseId, x.WarehouseName })
|
||||
.Select((g, i) => new WarehouseKpiDto
|
||||
{
|
||||
WarehouseName = g.Key.WarehouseName ?? "Unknown",
|
||||
StockQty = g.Sum(x => x.Stock ?? 0),
|
||||
SkuCount = g.Select(x => x.ProductId).Distinct().Count(),
|
||||
InventoryValue = g.Sum(x => (decimal)(x.Stock ?? 0) * x.UnitPrice),
|
||||
Color = whColors[i % whColors.Length]
|
||||
})
|
||||
.OrderByDescending(x => x.StockQty)
|
||||
.ToList();
|
||||
|
||||
// 6. Module breakdown (by count) — ModuleCode uses SHORT codes
|
||||
var moduleColors = new Dictionary<string, string>
|
||||
{
|
||||
["GR"] = "#06b6d4", ["DO"] = "#10b981",
|
||||
["ADJ+"] = "#6366f1", ["ADJ-"] = "#ef4444",
|
||||
["COUNT"] = "#f59e0b", ["SCRP"] = "#dc2626",
|
||||
["TO-IN"] = "#8b5cf6", ["TO-OUT"] = "#f43f5e",
|
||||
["PRN"] = "#0891b2", ["SRN"] = "#ea580c"
|
||||
};
|
||||
|
||||
var moduleBreakdown = topTxs
|
||||
.GroupBy(x => x.ModuleCode ?? "Other")
|
||||
.Select(g => new WmsModuleBreakdownDto
|
||||
{
|
||||
Label = g.Key,
|
||||
Count = g.Count(),
|
||||
Quantity = g.Sum(x => Math.Abs(x.Movement ?? 0)),
|
||||
Color = moduleColors.TryGetValue(g.Key, out var c) ? c : "#94a3b8"
|
||||
})
|
||||
.OrderByDescending(x => x.Count)
|
||||
.ToList();
|
||||
|
||||
if (!moduleBreakdown.Any())
|
||||
{
|
||||
moduleBreakdown = new List<WmsModuleBreakdownDto>
|
||||
{
|
||||
new() { Label = "GR", Count = grCount, Color = "#06b6d4" },
|
||||
new() { Label = "DO", Count = doCount, Color = "#10b981" },
|
||||
new() { Label = "ADJ", Count = adjCount, Color = "#6366f1" },
|
||||
new() { Label = "SCRP", Count = scrapCount, Color = "#dc2626" },
|
||||
new() { Label = "TO", Count = transferCount, Color = "#8b5cf6" }
|
||||
};
|
||||
}
|
||||
|
||||
// 7. Pipeline Summary
|
||||
var pipelineSummary = new List<WmsPipelineSummaryDto>
|
||||
{
|
||||
new() { Label = "Total Stock Qty", Value = FormattedSingle((decimal)totalStockQty), IconColorClass = "gc1" },
|
||||
new() { Label = "Inventory Value", Value = FormattedSingle(inventoryValue), IconColorClass = "gc2" },
|
||||
new() { Label = "Active SKUs", Value = totalSku.ToString("N0"), IconColorClass = "gc3" },
|
||||
new() { Label = "Warehouses", Value = warehouseCount.ToString(), IconColorClass = "gc4" }
|
||||
};
|
||||
|
||||
// 8. Conversion Metrics
|
||||
var conversionMetrics = new List<WmsConversionMetricDto>
|
||||
{
|
||||
new() { Label = "GR→Stock", Current = grCount, Target = Math.Max(grCount, 1), PctValue = grCount > 0 ? 100 : 0, BarColor = "#06b6d4" },
|
||||
new() { Label = "DO Fulfilled", Current = doConfirmed, Target = Math.Max(totalDo, 1), PctValue = fulfPct, BarColor = "#10b981" },
|
||||
new() { Label = "Stock Accuracy", Current = stockAccuracy, Target = 100, PctValue = stockAccuracy, BarColor = "#6366f1" },
|
||||
new() { Label = "Pending Clear", Current = totalDo - pendingOut, Target = Math.Max(totalDo, 1), PctValue = totalDo > 0 ? (double)(totalDo - pendingOut) / totalDo * 100 : 0, BarColor = "#f59e0b" }
|
||||
};
|
||||
|
||||
// 9. Recent Transactions (top 5 from our top-1000 set)
|
||||
var recentTransactions = topTxs
|
||||
.OrderByDescending(x => x.CreatedAt)
|
||||
.Take(5)
|
||||
.Select(x => new WmsRecentTransactionDto
|
||||
{
|
||||
Number = x.ModuleNumber,
|
||||
Description = x.ProductName ?? "?",
|
||||
Module = x.ModuleCode,
|
||||
Quantity = Math.Abs(x.Movement ?? 0),
|
||||
Type = x.TransType?.ToString() ?? "-",
|
||||
Status = "Confirmed",
|
||||
Date = x.CreatedAt
|
||||
}).ToList();
|
||||
|
||||
// 10. Activity Feed (from top transactions, most recent 4) — filter to key module types
|
||||
var activityModules = new[] { "GR", "DO", "SCRP", "TO-IN", "COUNT" };
|
||||
var avatarColors = new[] { "#06b6d4", "#10b981", "#dc2626", "#8b5cf6", "#6366f1" };
|
||||
var activityFeeds = topTxs
|
||||
.Where(x => x.ModuleCode != null && activityModules.Contains(x.ModuleCode))
|
||||
.OrderByDescending(x => x.CreatedAt)
|
||||
.Take(4)
|
||||
.Select((x, i) =>
|
||||
{
|
||||
var pn = x.ProductName ?? "?";
|
||||
var mc = x.ModuleCode ?? "?";
|
||||
var timeAgo = x.CreatedAt.HasValue
|
||||
? (DateTimeOffset.UtcNow - x.CreatedAt.Value).TotalMinutes < 60
|
||||
? $"{(int)(DateTimeOffset.UtcNow - x.CreatedAt.Value).TotalMinutes}m ago"
|
||||
: (DateTimeOffset.UtcNow - x.CreatedAt.Value).TotalHours < 24
|
||||
? $"{(int)(DateTimeOffset.UtcNow - x.CreatedAt.Value).TotalHours}h ago"
|
||||
: $"{(int)(DateTimeOffset.UtcNow - x.CreatedAt.Value).TotalDays}d ago"
|
||||
: "";
|
||||
return new WmsActivityFeedDto
|
||||
{
|
||||
Title = $"{mc} — {pn}",
|
||||
Detail = $"Qty: {Math.Abs(x.Movement ?? 0):N0} · {x.WarehouseName}",
|
||||
TimeAgo = timeAgo,
|
||||
ChipLabel = mc,
|
||||
ChipColor = i % 2 == 0 ? "chip-cyan" : "chip-amber",
|
||||
AvatarBg = avatarColors[i % avatarColors.Length],
|
||||
AvatarText = mc
|
||||
};
|
||||
}).ToList();
|
||||
|
||||
// 11. Stock Movement Trend (last 6 months from top transactions)
|
||||
var monthNames = new[] { "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" };
|
||||
var last6Months = Enumerable.Range(0, 6)
|
||||
.Select(i => today.AddMonths(-5 + i))
|
||||
.Select(d => new { d.Year, d.Month })
|
||||
.ToList();
|
||||
|
||||
var stockMovementTrend = last6Months.Select(m =>
|
||||
{
|
||||
var inQty = topTxs
|
||||
.Where(x => x.MovementDate.HasValue
|
||||
&& x.MovementDate.Value.Year == m.Year
|
||||
&& x.MovementDate.Value.Month == m.Month
|
||||
&& IsInbound(x.ModuleCode))
|
||||
.Sum(x => Math.Abs(x.Movement ?? 0));
|
||||
var outQty = topTxs
|
||||
.Where(x => x.MovementDate.HasValue
|
||||
&& x.MovementDate.Value.Year == m.Year
|
||||
&& x.MovementDate.Value.Month == m.Month
|
||||
&& IsOutbound(x.ModuleCode))
|
||||
.Sum(x => Math.Abs(x.Movement ?? 0));
|
||||
return new WmsStockMovementPointDto
|
||||
{
|
||||
Label = monthNames[m.Month - 1],
|
||||
InQty = inQty,
|
||||
OutQty = outQty
|
||||
};
|
||||
}).ToList();
|
||||
|
||||
// 12. SO vs PO revenue trend (from SalesOrder/PurchaseOrder tables, as in CRM)
|
||||
var soMonths = await _context.SalesOrder.AsNoTracking()
|
||||
.Where(x => x.OrderDate != null)
|
||||
.Select(x => x.OrderDate!.Value)
|
||||
.ToListAsync(ct);
|
||||
var poMonths = await _context.PurchaseOrder.AsNoTracking()
|
||||
.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 revenueTrend = new List<ChartDataPoint>();
|
||||
foreach (var ym in allDates)
|
||||
{
|
||||
var ms = new DateTime(ym.Year, ym.Month, 1);
|
||||
var me = ms.AddMonths(1);
|
||||
var sa = await _context.SalesOrder.AsNoTracking()
|
||||
.Where(x => x.OrderDate >= ms && x.OrderDate < me)
|
||||
.SumAsync(x => (double?)x.AfterTaxAmount ?? 0, ct);
|
||||
var pa = await _context.PurchaseOrder.AsNoTracking()
|
||||
.Where(x => x.OrderDate >= ms && x.OrderDate < me)
|
||||
.SumAsync(x => (double?)x.AfterTaxAmount ?? 0, ct);
|
||||
revenueTrend.Add(new ChartDataPoint
|
||||
{
|
||||
Label = monthNames[ym.Month - 1],
|
||||
SalesAmount = Math.Round(sa / 1000.0, 1),
|
||||
PurchaseAmount = Math.Round(pa / 1000.0, 1)
|
||||
});
|
||||
}
|
||||
|
||||
// 13. Yearly Movement summary
|
||||
var yearlyMovement = new List<WmsYearlyMovementDto>
|
||||
{
|
||||
new() { Category = "Inbound", Qty = yearlyInbound, Color = "#10b981" },
|
||||
new() { Category = "Outbound", Qty = yearlyOutbound, Color = "#e11d48" },
|
||||
new() { Category = "Net", Qty = yearlyInbound - yearlyOutbound, Color = "#6366f1" }
|
||||
};
|
||||
|
||||
// 14. Top Products by total movement volume
|
||||
var topProducts = topTxs
|
||||
.GroupBy(x => x.ProductName ?? "?")
|
||||
.Select(g => new { Name = g.Key, Total = g.Sum(x => Math.Abs(x.Movement ?? 0)) })
|
||||
.OrderByDescending(x => x.Total)
|
||||
.Take(5)
|
||||
.Select((x, i) => new WmsTopProductDto
|
||||
{
|
||||
ProductName = x.Name,
|
||||
TotalMovement = x.Total,
|
||||
Color = whColors[i % whColors.Length]
|
||||
})
|
||||
.ToList();
|
||||
|
||||
return new DashboardWmsResponse
|
||||
{
|
||||
TotalStockQty = totalStockQty,
|
||||
InventoryValue = inventoryValue,
|
||||
TotalSku = totalSku,
|
||||
WarehouseCount = warehouseCount,
|
||||
StockAccuracy = stockAccuracy,
|
||||
OrderFulfillmentRate = fulfPct,
|
||||
MonthlyInboundQty = monthlyInbound,
|
||||
MonthlyOutboundQty = monthlyOutbound,
|
||||
MonthlyTransactionCount = monthlyTxCount,
|
||||
YearlyInboundQty = yearlyInbound,
|
||||
YearlyOutboundQty = yearlyOutbound,
|
||||
PendingInbound = pendingIn,
|
||||
PendingOutbound = pendingOut,
|
||||
ScrapCount = scrapCount,
|
||||
AdjustmentCount = adjCount,
|
||||
GrCount = grCount,
|
||||
DoCount = doCount,
|
||||
TransferCount = transferCount,
|
||||
StockCountCount = stockCountCount,
|
||||
WarehouseKpis = warehouseKpis,
|
||||
ModuleBreakdown = moduleBreakdown,
|
||||
PipelineSummary = pipelineSummary,
|
||||
ConversionMetrics = conversionMetrics,
|
||||
RecentTransactions = recentTransactions,
|
||||
ActivityFeeds = activityFeeds,
|
||||
RevenueTrend = revenueTrend,
|
||||
StockMovementTrend = stockMovementTrend,
|
||||
YearlyMovement = yearlyMovement,
|
||||
TopProducts = topProducts
|
||||
};
|
||||
}
|
||||
|
||||
private static string FormattedSingle(decimal v)
|
||||
{
|
||||
if (v >= 1_000_000) return (v / 1_000_000m).ToString("F2") + "M";
|
||||
if (v >= 1_000) return (v / 1_000m).ToString("F1") + "K";
|
||||
return v.ToString("N0");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user