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; } } // SWM-specific DTOs public class BookingKpiDto { public string? ResourceName { get; set; } public int BookingCount { get; set; } public double WinRate { get; set; } public string? Color { get; set; } } public class BookingStatusDto { public string? Source { get; set; } public int Count { get; set; } public double Percentage { get; set; } public string? Color { get; set; } } public class CustomerGroupDto { public string? Name { get; set; } public int Count { get; set; } public double PctValue { get; set; } public string? BarColor { get; set; } } public class VendorGroupDto { public string? Name { get; set; } public int Count { get; set; } public string? IconClass { get; set; } } public class TopProductDto { public string? Name { get; set; } public double Quantity { get; set; } public double Share { get; set; } } public class PaymentMethodDto { public string? Name { get; set; } public int Count { get; set; } } public class DocGridDto { public string? Label { get; set; } public string? Value { get; set; } public string? Color { get; set; } } public class CashFlowDto { public string? Label { get; set; } public string? Value { get; set; } public string? Color { get; set; } } public class RecentSoDto { public string? Number { get; set; } public string? Customer { get; set; } public decimal Value { get; set; } public string? Stage { get; set; } public string? Status { get; set; } public DateTimeOffset? Date { get; set; } } public class ActivityFeedDto { 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 PipelineSummaryDto { public string? Label { get; set; } public string? Value { get; set; } public string? IconColorClass { get; set; } } public class DealStageDto { public string? Stage { get; set; } public int Count { get; set; } public string? Color { get; set; } } public class SalesActivityDto { public string? Category { get; set; } public int Count { get; set; } public string? Color { get; set; } } public class DashboardSwmResponse { // Core KPIs public int TotalCustomer { get; set; } public int TotalVendor { get; set; } public int TotalBookings { get; set; } public int ConfirmedBookings { get; set; } public int DraftBookings { get; set; } public int CancelledBookings { get; set; } public int TotalResources { get; set; } public int TotalBookingGroups { get; set; } public double BookingWinRate { get; set; } public int PendingSO { get; set; } public int PendingPO { get; set; } // Financial public decimal SalesMTD { get; set; } public decimal PurchaseMTD { get; set; } public decimal TotalReceivable { get; set; } public decimal TotalPayable { get; set; } // Collections public List BookingsByResource { get; set; } = new(); public List BookingsByGroup { get; set; } = new(); public List BookingStatuses { get; set; } = new(); public List CustomerGroups { get; set; } = new(); public List VendorGroups { get; set; } = new(); public List TopProducts { get; set; } = new(); public List PaymentMethods { get; set; } = new(); public List DocGrid { get; set; } = new(); public List CashFlowMTD { get; set; } = new(); public List RecentSalesOrders { get; set; } = new(); public List RecentBookingsFeed { get; set; } = new(); public List RevenueTrend { get; set; } = new(); public List MonthlyBookingsTrend { get; set; } = new(); public List PipelineSummary { get; set; } = new(); public List BookingStages { get; set; } = new(); public List BookingActivity { get; set; } = new(); // Formatted public string FormattedSales => FormatNumber(SalesMTD); public string FormattedPurchase => FormatNumber(PurchaseMTD); public string FormattedReceivable => FormatNumber(TotalReceivable); public string FormattedPayable => FormatNumber(TotalPayable); private static string FormatNumber(decimal v) { if (v >= 1_000_000) return (v / 1_000_000m).ToString("F1") + "M"; if (v >= 1_000) return (v / 1_000m).ToString("F1") + "K"; return v.ToString("N0"); } } public record GetSwmDashboardQuery() : IRequest; public class DashboardHandler : IRequestHandler { private readonly AppDbContext _context; public DashboardHandler(AppDbContext context) => _context = context; public async Task Handle(GetSwmDashboardQuery request, CancellationToken ct) { var today = DateTime.Today; var monthStart = new DateTime(today.Year, today.Month, 1); // 1. Pull top 1000 bookings (with resource) var topBookings = await _context.Booking .AsNoTracking() .OrderByDescending(x => x.StartTime) .Take(1000) .Select(x => new { x.Status, x.StartTime, x.Subject, x.AutoNumber, ResourceName = x.BookingResource != null ? x.BookingResource.Name : "N/A", ResourceId = x.BookingResourceId ?? "0", }) .ToListAsync(ct); // 2. Pull top 1000 sales orders (with customer) var topSO = await _context.SalesOrder .AsNoTracking() .Where(x => x.OrderDate != null) .OrderByDescending(x => x.OrderDate) .Take(1000) .Select(x => new { x.AutoNumber, x.OrderDate, x.AfterTaxAmount, x.OrderStatus, CustomerName = x.Customer != null ? x.Customer.Name : "N/A", }) .ToListAsync(ct); // 3. Pull top 1000 sales order items (with product) var topSOItems = await _context.SalesOrderItem .AsNoTracking() .Where(x => x.Quantity != null) .OrderByDescending(x => x.SalesOrder!.OrderDate) .Take(1000) .Select(x => new { ProductName = x.Product != null ? x.Product.Name : "N/A", x.Quantity, }) .ToListAsync(ct); // 4. Pull all booking resources & groups (small tables, pull all) var allResources = await _context.BookingResource .AsNoTracking() .Select(x => new { x.Name, GroupName = x.BookingGroup != null ? x.BookingGroup.Name : "General" }) .ToListAsync(ct); // 5. Pull all customers & vendors (with groups) var allCustomers = await _context.Customer .AsNoTracking() .Select(x => new { GroupName = x.CustomerGroup != null ? x.CustomerGroup.Name : "General" }) .ToListAsync(ct); var allVendors = await _context.Vendor .AsNoTracking() .Select(x => new { GroupName = x.VendorGroup != null ? x.VendorGroup.Name : "General" }) .ToListAsync(ct); // 6. Pull payment methods var allPayMethods = await _context.PaymentReceive .AsNoTracking() .Select(x => new { MethodName = x.PaymentMethod != null ? x.PaymentMethod.Name : "Cash" }) .ToListAsync(ct); // === AGGREGATE IN MEMORY === // Core counts from bookings var totalBookings = topBookings.Count; var confirmedBookings = topBookings.Count(x => x.Status == BookingStatus.Confirmed); var draftBookings = topBookings.Count(x => x.Status == BookingStatus.Draft); var cancelledBookings = topBookings.Count(x => x.Status == BookingStatus.Cancelled); var bookingWinRate = totalBookings > 0 ? Math.Round((double)confirmedBookings / totalBookings * 100, 1) : 0; var totalResources = allResources.Select(x => x.Name).Distinct().Count(); var totalGroups = allResources.Select(x => x.GroupName).Distinct().Count(); var totalCustomer = allCustomers.Count; var totalVendor = allVendors.Count; // Financial from topSO (already has dates and amounts) var salesMTD = topSO.Where(x => x.OrderDate >= monthStart).Sum(x => x.AfterTaxAmount ?? 0); var purchaseMTD = await _context.PurchaseOrder.AsNoTracking() .Where(x => x.OrderDate >= monthStart) .SumAsync(x => x.AfterTaxAmount ?? 0, ct); var totalReceivable = await _context.Invoice.AsNoTracking() .Where(x => x.InvoiceStatus == InvoiceStatus.Confirmed) .SumAsync(x => (x.SalesOrder != null ? x.SalesOrder.AfterTaxAmount : 0) ?? 0, ct); var totalPayable = await _context.Bill.AsNoTracking() .Where(x => x.BillStatus == BillStatus.Confirmed) .SumAsync(x => (x.PurchaseOrder != null ? x.PurchaseOrder.AfterTaxAmount : 0) ?? 0, ct); var pendingSO = topSO.Count(x => x.OrderStatus == SalesOrderStatus.Draft); var pendingPO = await _context.PurchaseOrder.AsNoTracking().CountAsync(x => x.OrderStatus == PurchaseOrderStatus.Draft, ct); // Bookings by Resource (from topBookings) var whColors = new[] { "#2563eb", "#10b981", "#f59e0b", "#8b5cf6", "#ec4899", "#06b6d4", "#f97316", "#6366f1" }; var bookingsByResource = topBookings .GroupBy(x => x.ResourceName) .Select((g, i) => new BookingKpiDto { ResourceName = g.Key, BookingCount = g.Count(), WinRate = g.Count() > 0 ? Math.Round((double)g.Count(x => x.Status == BookingStatus.Confirmed) / g.Count() * 100, 1) : 0, Color = whColors[i % whColors.Length] }) .OrderByDescending(x => x.BookingCount) .Take(8) .ToList(); // Bookings by Group (from allResources) var bookingsByGroup = allResources .GroupBy(x => x.GroupName) .Select((g, i) => new BookingKpiDto { ResourceName = g.Key, BookingCount = g.Count(), WinRate = 0, Color = "#8b5cf6" }) .ToList(); // Booking Statuses (for doughnut chart) var statusColors = new[] { "#10b981", "#f59e0b", "#e11d48" }; var statusLabels = new[] { "Confirmed", "Draft", "Cancelled" }; var statusCounts = new[] { confirmedBookings, draftBookings, cancelledBookings }; var totalStatus = statusCounts.Sum(); var bookingStatuses = statusLabels.Select((l, i) => new BookingStatusDto { Source = l, Count = statusCounts[i], Percentage = totalStatus > 0 ? Math.Round((double)statusCounts[i] / totalStatus * 100, 1) : 0, Color = i < statusColors.Length ? statusColors[i] : "#94a3b8" }).ToList(); // Customer Groups (with progress bars) var customerGroups = allCustomers .GroupBy(x => x.GroupName) .Select(g => new CustomerGroupDto { Name = g.Key, Count = g.Count(), PctValue = totalCustomer > 0 ? Math.Min(Math.Round((double)g.Count() / totalCustomer * 100, 1), 100) : 0, BarColor = "#2563eb" }) .OrderByDescending(x => x.Count) .Take(6) .ToList(); // Vendor Groups var vendorGroups = allVendors .GroupBy(x => x.GroupName) .Select(g => new VendorGroupDto { Name = g.Key, Count = g.Count(), IconClass = "gc7" }) .ToList(); // Top Products (from topSOItems) var totalQty = topSOItems.Sum(x => x.Quantity ?? 0); var topProducts = topSOItems .GroupBy(x => x.ProductName) .Select(g => new { Name = g.Key, Qty = g.Sum(x => (double)(x.Quantity ?? 0)) }) .OrderByDescending(x => x.Qty) .Take(4) .Select(x => new TopProductDto { Name = x.Name, Quantity = x.Qty, Share = totalQty > 0 ? Math.Round(x.Qty / totalQty * 100, 1) : 0 }) .ToList(); // Payment Methods var paymentMethods = allPayMethods .GroupBy(x => x.MethodName) .Select(g => new PaymentMethodDto { Name = g.Key, Count = g.Count() }) .ToList(); // Doc Grid var docGrid = new List { new() { Label = "Sales Order", Value = await _context.SalesOrder.CountAsync(ct).ContinueWith(t => t.Result.ToString()), Color = "#10b981" }, new() { Label = "Purchase Order", Value = await _context.PurchaseOrder.CountAsync(ct).ContinueWith(t => t.Result.ToString()), Color = "#ef4444" }, new() { Label = "Invoice", Value = await _context.Invoice.CountAsync(ct).ContinueWith(t => t.Result.ToString()), Color = "#14b8a6" }, new() { Label = "Bill", Value = await _context.Bill.CountAsync(ct).ContinueWith(t => t.Result.ToString()), Color = "#f97316" }, new() { Label = "Bookings", Value = totalBookings.ToString(), Color = "#8b5cf6" }, new() { Label = "Resources", Value = totalResources.ToString(), Color = "#6366f1" } }; // Cash Flow MTD var cashFlowMTD = new List { new() { Label = "Inflow (Receive)", Value = await _context.PaymentReceive.Where(x => x.PaymentDate >= monthStart).SumAsync(x => x.PaymentAmount ?? 0, ct).ContinueWith(t => t.Result.ToString("N0")), Color = "#10b981" }, new() { Label = "Outflow (Disburse)", Value = await _context.PaymentDisburse.Where(x => x.PaymentDate >= monthStart).SumAsync(x => x.PaymentAmount ?? 0, ct).ContinueWith(t => t.Result.ToString("N0")), Color = "#ef4444" } }; // Recent Sales Orders (from topSO) var recentSalesOrders = topSO .OrderByDescending(x => x.OrderDate) .Take(6) .Select(x => new RecentSoDto { Number = x.AutoNumber, Customer = x.CustomerName, Value = x.AfterTaxAmount ?? 0, Stage = x.OrderStatus.ToString(), Status = x.OrderStatus == SalesOrderStatus.Confirmed ? "Posted" : "Pending", Date = x.OrderDate.HasValue ? new DateTimeOffset(x.OrderDate.Value, TimeSpan.Zero) : null }) .ToList(); // Recent Bookings activity feed var recentBookingsFeed = topBookings .OrderByDescending(x => x.StartTime) .Take(3) .Select((b, i) => { var statusClass = b.Status == BookingStatus.Confirmed ? "chip-green" : b.Status == BookingStatus.Draft ? "chip-amber" : "chip-red"; var avatarBg = b.Status == BookingStatus.Confirmed ? "#10b981" : b.Status == BookingStatus.Draft ? "#f59e0b" : "#e11d48"; var timeAgo = b.StartTime.HasValue ? (DateTime.Now - b.StartTime.Value).TotalMinutes < 60 ? $"{(int)(DateTime.Now - b.StartTime.Value).TotalMinutes}m ago" : (DateTime.Now - b.StartTime.Value).TotalHours < 24 ? $"{(int)(DateTime.Now - b.StartTime.Value).TotalHours}h ago" : $"{(int)(DateTime.Now - b.StartTime.Value).TotalDays}d ago" : ""; var initials = b.ResourceName.Length > 0 ? b.ResourceName[..Math.Min(2, b.ResourceName.Length)].ToUpper() : "BK"; return new ActivityFeedDto { Title = $"{b.Subject} — {b.AutoNumber}", Detail = $"📍 {b.ResourceName} · {b.StartTime:MMM dd, yyyy}", TimeAgo = timeAgo, ChipLabel = b.Status.ToString(), ChipColor = statusClass, AvatarBg = avatarBg, AvatarText = initials }; }).ToList(); // Revenue Trend (from topSO + PurchaseOrder) 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 poMonthsRaw = await _context.PurchaseOrder.AsNoTracking() .Where(x => x.OrderDate != null) .Select(x => x.OrderDate!.Value) .ToListAsync(ct); var revenueTrend = last6Months.Select(m => { var salesAmount = topSO .Where(x => x.OrderDate.HasValue && x.OrderDate.Value.Year == m.Year && x.OrderDate.Value.Month == m.Month) .Sum(x => (double)(x.AfterTaxAmount ?? 0)); var purchaseAmount = poMonthsRaw .Where(d => d.Year == m.Year && d.Month == m.Month) .Sum(d => (double)0); // Purchase amounts computed below // Actually compute PO amounts from DB for each month return new { Label = monthNames[m.Month - 1], SalesAmount = Math.Round(salesAmount / 1000.0, 1), m.Year, m.Month }; }).ToList(); // Compute PO amounts for each of last 6 months var revenueTrendFinal = new List(); foreach (var m in last6Months) { var ms = new DateTime(m.Year, m.Month, 1); var me = ms.AddMonths(1); var sa = topSO .Where(x => x.OrderDate.HasValue && x.OrderDate.Value.Year == m.Year && x.OrderDate.Value.Month == m.Month) .Sum(x => (double)(x.AfterTaxAmount ?? 0)); var pa = await _context.PurchaseOrder.AsNoTracking() .Where(x => x.OrderDate >= ms && x.OrderDate < me) .SumAsync(x => (double?)x.AfterTaxAmount ?? 0, ct); revenueTrendFinal.Add(new ChartDataPoint { Label = monthNames[m.Month - 1], SalesAmount = Math.Round(sa / 1000.0, 1), PurchaseAmount = Math.Round(pa / 1000.0, 1) }); } // Monthly Bookings Trend var monthlyBookingsTrend = last6Months.Select(m => { var count = topBookings .Where(x => x.StartTime.HasValue && x.StartTime.Value.Year == m.Year && x.StartTime.Value.Month == m.Month) .Count(); return new ChartDataPoint { Label = monthNames[m.Month - 1], SalesAmount = count, PurchaseAmount = 0 }; }).ToList(); // Pipeline Summary var pipelineSummary = new List { new() { Label = "Total Bookings", Value = totalBookings.ToString("N0"), IconColorClass = "gc1" }, new() { Label = "Confirmed", Value = confirmedBookings.ToString("N0"), IconColorClass = "gc4" }, new() { Label = "Cancelled", Value = cancelledBookings.ToString("N0"), IconColorClass = "gc10" } }; // Booking Stages var bookingStages = new List { new() { Stage = "Confirmed", Count = confirmedBookings, Color = "#10b981" }, new() { Stage = "Draft", Count = draftBookings, Color = "#f59e0b" }, new() { Stage = "Cancelled", Count = cancelledBookings, Color = "#e11d48" } }; // Booking Activity var bookingActivity = new List { new() { Category = "Confirmed", Count = confirmedBookings, Color = "#10b981" }, new() { Category = "Draft", Count = draftBookings, Color = "#e11d48" }, new() { Category = "Cancelled", Count = cancelledBookings, Color = "#e11d48" }, new() { Category = "Total", Count = totalBookings, Color = "#6366f1" } }; return new DashboardSwmResponse { TotalCustomer = totalCustomer, TotalVendor = totalVendor, TotalBookings = totalBookings, ConfirmedBookings = confirmedBookings, DraftBookings = draftBookings, CancelledBookings = cancelledBookings, TotalResources = totalResources, TotalBookingGroups = totalGroups, BookingWinRate = bookingWinRate, PendingSO = pendingSO, PendingPO = pendingPO, SalesMTD = salesMTD, PurchaseMTD = purchaseMTD, TotalReceivable = totalReceivable, TotalPayable = totalPayable, BookingsByResource = bookingsByResource, BookingsByGroup = bookingsByGroup, BookingStatuses = bookingStatuses, CustomerGroups = customerGroups, VendorGroups = vendorGroups, TopProducts = topProducts, PaymentMethods = paymentMethods, DocGrid = docGrid, CashFlowMTD = cashFlowMTD, RecentSalesOrders = recentSalesOrders, RecentBookingsFeed = recentBookingsFeed, RevenueTrend = revenueTrendFinal, MonthlyBookingsTrend = monthlyBookingsTrend, PipelineSummary = pipelineSummary, BookingStages = bookingStages, BookingActivity = bookingActivity }; } }