using Indotalent.ConfigBackEnd.Extensions; 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 SalesPipelineDto { public string? Stage { get; set; } public int Count { get; set; } public decimal Value { get; set; } } public class LeadActivityDto { public string? Summary { get; set; } public string? LeadName { get; set; } public string? Type { get; set; } public DateTime? Date { get; set; } } public class CampaignRoiDto { public string? Label { get; set; } public decimal Cost { get; set; } public decimal Revenue { get; set; } public double Roi { get; set; } } public class SalesPerformanceDto { public string? Name { get; set; } public decimal Achievement { get; set; } public int Deals { get; set; } public double WinRate { get; set; } } public class PendingInvoiceDto { public string? Number { get; set; } public string? Customer { get; set; } public decimal Amount { get; set; } public string? Status { get; set; } } public class RecentLeadDto { public string? LeadTitle { get; set; } public string? Company { get; set; } public decimal Amount { get; set; } public string? Stage { get; set; } } public class BantScoreDto { public string? Label { get; set; } public string? Value { get; set; } public string? Color { get; set; } } public class UpcomingFollowUpDto { public string? LeadName { get; set; } public string? Summary { get; set; } public DateTime? ScheduledDate { get; set; } public string? Type { get; set; } } // DTOs for dashboard3 reference layout (matching CRM-Blazor-Contoh) public class DealStageDto { public string? Stage { get; set; } public int Count { get; set; } public string? Color { get; set; } } public class LeadSourceDto { public string? Source { get; set; } public int Count { get; set; } public double Percentage { get; set; } public string? Color { get; set; } } public class ConversionMetricDto { 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 SalesActivityDto { public string? Category { get; set; } public int Count { get; set; } public string? Color { get; set; } } public class RecentDealDto { public string? DealNumber { get; set; } public string? Description { 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 decimal Value { get; set; } public string? IconColorClass { get; set; } public string? FormattedValue { get; set; } } public class DashboardCrmResponse { public int TotalLeads { get; set; } public decimal TotalPipelineValue { get; set; } public decimal MonthlySales { get; set; } public double AvgWinRate { get; set; } public int ActiveCampaigns { get; set; } public int WonDealsCount { get; set; } public decimal PendingPayments { get; set; } public decimal TotalBudget { get; set; } public decimal ActualExpense { get; set; } public double RetentionRate { get; set; } public decimal AvgDealSize { get; set; } public double OverallCvr { get; set; } public int ActiveDealsCount { get; set; } public int TotalContacts { get; set; } public int TotalCampaigns { get; set; } public double SlaPercentage { get; set; } public double EnterprisePercentage { get; set; } public double TargetCvr { get; set; } public List PipelineBreakdown { get; set; } = new(); public List RecentActivities { get; set; } = new(); public List TopCampaigns { get; set; } = new(); public List SalesLeaderboard { get; set; } = new(); public List OverdueInvoices { get; set; } = new(); public List RecentLeads { get; set; } = new(); public List BantMetrics { get; set; } = new(); public List UpcomingFollowUps { get; set; } = new(); public List MonthlySalesVsPurchase { get; set; } = new(); public List WeeklySalesVsPurchase { get; set; } = new(); // New properties for dashboard3 layout (matching CRM-Blazor-Contoh) public List DealStages { get; set; } = new(); public List LeadSources { get; set; } = new(); public List ConversionMetrics { get; set; } = new(); public List SalesActivities { get; set; } = new(); public List RecentDeals { get; set; } = new(); public List ActivityFeeds { get; set; } = new(); public List PipelineSummary { get; set; } = new(); public List RevenueTrend { get; set; } = new(); public string FormattedPipeline => FormatNumber(TotalPipelineValue); public string FormattedMonthlySales => FormatNumber(MonthlySales); public string FormattedPendingPayments => FormatNumber(PendingPayments); public string FormattedBudget => FormatNumber(TotalBudget); public string FormattedExpense => FormatNumber(ActualExpense); public string FormattedAvgDealSize => FormatNumber(AvgDealSize); public string FormattedTotalLeads => TotalLeads.ToString("N0"); private string FormatNumber(decimal value) { if (value >= 1000000) return (value / 1000000m).ToString("F1") + "M"; if (value >= 1000) return (value / 1000m).ToString("F1") + "K"; return value.ToString("N0"); } } public record GetCrmDashboardQuery() : IRequest; public class DashboardHandler : IRequestHandler { private readonly AppDbContext _context; public DashboardHandler(AppDbContext context) => _context = context; public async Task Handle(GetCrmDashboardQuery request, CancellationToken ct) { var today = DateTime.Today; var monthStart = new DateTime(today.Year, today.Month, 1); var pipeline = await _context.Lead.NotDeletedOnly() .GroupBy(x => x.PipelineStage) .Select(g => new SalesPipelineDto { Stage = g.Key.ToString(), Count = g.Count(), Value = g.Sum(x => x.AmountTargeted ?? 0) }).ToListAsync(ct); var leaders = await _context.SalesRepresentative.NotDeletedOnly() .Take(5) .Select(x => new SalesPerformanceDto { Name = x.Name, Achievement = _context.Lead .Where(l => l.SalesTeamId == x.SalesTeamId && l.ClosingStatus == ClosingStatus.ClosedWon) .Sum(l => l.AmountClosed ?? 0), Deals = _context.Lead .Count(l => l.SalesTeamId == x.SalesTeamId && l.ClosingStatus == ClosingStatus.ClosedWon), WinRate = _context.Lead.Where(l => l.SalesTeamId == x.SalesTeamId).Any() ? (double)_context.Lead.Count(l => l.SalesTeamId == x.SalesTeamId && l.ClosingStatus == ClosingStatus.ClosedWon) / _context.Lead.Count(l => l.SalesTeamId == x.SalesTeamId) * 100 : 0 }).OrderByDescending(x => x.Achievement).ToListAsync(ct); var campaigns = await _context.Campaign.NotDeletedOnly() .Include(x => x.ExpenseList) .Include(x => x.LeadList) .Take(3) .Select(x => new CampaignRoiDto { Label = x.Title, Cost = x.ExpenseList.Sum(e => e.Amount ?? 0), Revenue = x.LeadList.Where(l => l.ClosingStatus == ClosingStatus.ClosedWon).Sum(l => l.AmountClosed ?? 0), Roi = x.ExpenseList.Sum(e => e.Amount ?? 0) > 0 ? (double)(x.LeadList.Where(l => l.ClosingStatus == ClosingStatus.ClosedWon).Sum(l => l.AmountClosed ?? 0) / x.ExpenseList.Sum(e => e.Amount ?? 0)) : 0 }).ToListAsync(ct); var overdue = await _context.Invoice.NotDeletedOnly() .Include(x => x.SalesOrder).ThenInclude(so => so!.Customer) .Where(x => x.InvoiceStatus != InvoiceStatus.FullPaid) .OrderByDescending(x => x.CreatedAt).Take(5) .Select(x => new PendingInvoiceDto { Number = x.AutoNumber, Customer = x.SalesOrder!.Customer!.Name, Amount = x.SalesOrder.AfterTaxAmount ?? 0, Status = x.InvoiceStatus.ToString() }).ToListAsync(ct); var recentLeads = await _context.Lead.NotDeletedOnly() .OrderByDescending(x => x.CreatedAt).Take(5) .Select(x => new RecentLeadDto { LeadTitle = x.Title, Company = x.CompanyName, Amount = x.AmountTargeted ?? 0, Stage = x.PipelineStage.ToString() }).ToListAsync(ct); var recentActivities = await _context.LeadActivity.NotDeletedOnly() .Include(x => x.Lead) .OrderByDescending(x => x.FromDate).Take(10) .Select(x => new LeadActivityDto { Summary = x.Summary, LeadName = x.Lead!.Title, Type = x.Type.ToString(), Date = x.FromDate }).ToListAsync(ct); var followUps = await _context.LeadActivity.NotDeletedOnly() .Include(x => x.Lead) .Where(x => x.FromDate >= today && (x.Type == LeadActivityType.Phone || x.Type == LeadActivityType.Meeting)) .OrderBy(x => x.FromDate) .Take(5) .Select(x => new UpcomingFollowUpDto { LeadName = x.Lead!.Title, Summary = x.Summary, ScheduledDate = x.FromDate, Type = x.Type.ToString() }).ToListAsync(ct); var totalLeadsCount = await _context.Lead.NotDeletedOnly().CountAsync(ct); var wonLeadsCount = await _context.Lead.NotDeletedOnly().CountAsync(x => x.ClosingStatus == ClosingStatus.ClosedWon, ct); var bantMetrics = new List { new() { Label = "Hot Deals", Value = await _context.Lead.CountAsync(x => x.BudgetScore > 80 && x.ClosingStatus == ClosingStatus.OnProgress, ct).ContinueWith(t => t.Result.ToString()), Color = "#f59e0b" }, new() { Label = "Budget Ready", Value = await _context.Lead.CountAsync(x => x.BudgetScore >= 70, ct).ContinueWith(t => t.Result.ToString()), Color = "#f59e0b" }, new() { Label = "Decision Maker", Value = await _context.Lead.CountAsync(x => x.AuthorityScore >= 70, ct).ContinueWith(t => t.Result.ToString()), Color = "#10b981" }, new() { Label = "Timeline Ready", Value = await _context.Lead.CountAsync(x => x.TimelineScore >= 70, ct).ContinueWith(t => t.Result.ToString()), Color = "#3b82f6" }, new() { Label = "SQL", Value = await _context.Lead.CountAsync(x => x.PipelineStage >= PipelineStage.Qualification, ct).ContinueWith(t => t.Result.ToString()), Color = "#64748b" }, new() { Label = "MQL", Value = totalLeadsCount.ToString(), Color = "#1e293b" } }; var monthlyLabels = new[] { "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" }; var monthlyData = new List(); 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(); 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) }); } var weeklyData = new List(); 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(); 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) }); } // New queries for dashboard3 layout (matching CRM-Blazor-Contoh) var totalPipeline = pipeline.Sum(x => x.Value); var totalClosedWon = await _context.Lead.NotDeletedOnly() .Where(x => x.ClosingStatus == ClosingStatus.ClosedWon) .SumAsync(x => x.AmountClosed ?? 0, ct); var dealStages = new List { new() { Stage = "Prospecting", Count = await _context.Lead.NotDeletedOnly().CountAsync(x => x.PipelineStage == PipelineStage.Prospecting, ct), Color = "#8b5cf6" }, new() { Stage = "Qualification", Count = await _context.Lead.NotDeletedOnly().CountAsync(x => x.PipelineStage == PipelineStage.Qualification, ct), Color = "#f59e0b" }, new() { Stage = "Negotiation", Count = await _context.Lead.NotDeletedOnly().CountAsync(x => x.PipelineStage == PipelineStage.Negotiation, ct), Color = "#e11d48" }, new() { Stage = "Closed Won", Count = wonLeadsCount, Color = "#6366f1" } }; var pipelineSummaryList = new List { new() { Label = "Total Pipeline", Value = totalPipeline, FormattedValue = totalPipeline >= 1000000 ? $"${totalPipeline / 1000000:F2}M" : totalPipeline >= 1000 ? $"${totalPipeline / 1000:F1}K" : $"${totalPipeline:N0}", IconColorClass = "gc1" }, new() { Label = "Closed Won", Value = totalClosedWon, FormattedValue = totalClosedWon >= 1000000 ? $"${totalClosedWon / 1000000:F2}M" : totalClosedWon >= 1000 ? $"${totalClosedWon / 1000:F1}K" : $"${totalClosedWon:N0}", IconColorClass = "gc10" }, new() { Label = "Avg Deal Size", Value = wonLeadsCount > 0 ? totalClosedWon / wonLeadsCount : 0, IconColorClass = "gc2" } }; var leadSourceRaw = await _context.Lead.NotDeletedOnly() .Select(x => x.CompanyName ?? "Unknown") .ToListAsync(ct); var leadSourceGroups = leadSourceRaw .GroupBy(x => x.Length > 1 ? x.Substring(0, 1) : "?") .Select(g => new { Key = g.Key, Count = g.Count() }) .OrderByDescending(x => x.Count) .Take(5) .ToList(); var totalSource = leadSourceGroups.Sum(x => x.Count); var sourceColors = new[] { "#2563eb", "#f97316", "#ec4899", "#8b5cf6", "#06b6d4" }; var sourceLabels = new[] { "Website", "Referral", "Social", "Email", "Others" }; var leadSources = leadSourceGroups.Select((g, i) => new LeadSourceDto { Source = i < sourceLabels.Length ? sourceLabels[i] : g.Key, Count = g.Count, Percentage = totalSource > 0 ? Math.Round((double)g.Count / totalSource * 100, 1) : 0, Color = i < sourceColors.Length ? sourceColors[i] : "#94a3b8" }).ToList(); var prospectingCount = await _context.Lead.NotDeletedOnly().CountAsync(x => x.PipelineStage == PipelineStage.Prospecting, ct); var qualifiedCount = await _context.Lead.NotDeletedOnly().CountAsync(x => x.PipelineStage == PipelineStage.Qualification, ct); var sqlCount = await _context.Lead.NotDeletedOnly().CountAsync(x => x.PipelineStage >= PipelineStage.Qualification, ct); var conversionMetrics = new List { new() { Label = "Leads→MQL", Current = sqlCount, Target = totalLeadsCount, PctValue = totalLeadsCount > 0 ? Math.Round((double)sqlCount / totalLeadsCount * 100, 1) : 0, BarColor = "#2563eb" }, new() { Label = "MQL→SQL", Current = qualifiedCount, Target = totalLeadsCount, PctValue = totalLeadsCount > 0 ? Math.Round((double)qualifiedCount / totalLeadsCount * 100, 1) : 0, BarColor = "#f97316" }, new() { Label = "SQL→Won", Current = wonLeadsCount, Target = sqlCount > 0 ? sqlCount : 1, PctValue = sqlCount > 0 ? Math.Round((double)wonLeadsCount / sqlCount * 100, 1) : 0, BarColor = "#ec4899" }, new() { Label = "Overall CVR", Current = wonLeadsCount, Target = totalLeadsCount, PctValue = totalLeadsCount > 0 ? Math.Round((double)wonLeadsCount / totalLeadsCount * 100, 1) : 0, BarColor = "#10b981" } }; var recentDeals = await _context.Lead.NotDeletedOnly() .OrderByDescending(x => x.CreatedAt).Take(4) .Select(x => new RecentDealDto { DealNumber = x.AutoNumber, Description = x.Title, Value = x.AmountTargeted ?? 0, Stage = x.PipelineStage.ToString(), Status = x.ClosingStatus == ClosingStatus.ClosedWon ? "Posted" : x.ClosingStatus == ClosingStatus.ClosedLost ? "Lost" : "Pending", Date = x.CreatedAt }).ToListAsync(ct); var activityFeed = recentActivities.Take(4).Select((a, i) => new ActivityFeedDto { Title = a.Summary ?? a.LeadName, Detail = $"Type: {a.Type} · Lead: {a.LeadName}", TimeAgo = a.Date.HasValue ? (DateTime.Now - a.Date.Value).TotalMinutes < 60 ? $"{(int)(DateTime.Now - a.Date.Value).TotalMinutes} min ago" : $"{(int)(DateTime.Now - a.Date.Value).TotalHours} hour ago" : "", ChipLabel = a.Type, ChipColor = i % 2 == 0 ? "chip-green" : "chip-indigo", AvatarBg = i switch { 0 => "#10b981", 1 => "#2563eb", 2 => "#6366f1", _ => "#f97316" }, AvatarText = a.Type?.Substring(0, Math.Min(a.Type.Length, 2)).ToUpper() }).ToList(); var salesActivities = new List { new() { Category = "Calls", Count = await _context.LeadActivity.NotDeletedOnly().CountAsync(x => x.Type == LeadActivityType.Phone, ct), Color = "#10b981" }, new() { Category = "Emails", Count = await _context.LeadActivity.NotDeletedOnly().CountAsync(x => x.Type == LeadActivityType.Email, ct), Color = "#e11d48" }, new() { Category = "Meetings", Count = await _context.LeadActivity.NotDeletedOnly().CountAsync(x => x.Type == LeadActivityType.Meeting, ct), Color = "#e11d48" }, new() { Category = "Total", Count = await _context.LeadActivity.NotDeletedOnly().CountAsync(ct), Color = "#6366f1" } }; var slaPercentage = totalLeadsCount > 0 ? Math.Round((double)wonLeadsCount / totalLeadsCount * 100, 1) : 0; var enterprisePercentage = totalPipeline > 0 ? Math.Round((double)(totalClosedWon / totalPipeline) * 100, 1) : 0; var targetCvr = 5.0; var revenueTrend = monthlyData; var avgDealSize = wonLeadsCount > 0 ? totalClosedWon / wonLeadsCount : 0; var overallCvr = totalLeadsCount > 0 ? Math.Round((double)wonLeadsCount / totalLeadsCount * 100, 1) : 0; var activeDeals = await _context.Lead.NotDeletedOnly().CountAsync(x => x.ClosingStatus == ClosingStatus.OnProgress, ct); var totalContacts = await _context.LeadContact.NotDeletedOnly().CountAsync(ct); var totalCampaigns = await _context.Campaign.NotDeletedOnly().CountAsync(ct); // Retention rate (using original SaaS approach with navigation properties) var customersWithOrders = await _context.SalesOrder.NotDeletedOnly() .Where(x => x.CustomerId != null) .Select(x => x.CustomerId) .Distinct() .CountAsync(ct); var repeatCustomerIds = await _context.SalesOrder.NotDeletedOnly() .Where(x => x.CustomerId != null) .GroupBy(x => x.CustomerId) .Where(g => g.Count() > 1) .Select(g => g.Key) .CountAsync(ct); var retentionRateCalc = customersWithOrders > 0 ? Math.Round((double)repeatCustomerIds / customersWithOrders * 100, 1) : 0; return new DashboardCrmResponse { TotalLeads = totalLeadsCount, TotalPipelineValue = totalPipeline, MonthlySales = await _context.SalesOrder.NotDeletedOnly().Where(x => x.OrderDate >= monthStart).SumAsync(x => x.AfterTaxAmount ?? 0, ct), AvgWinRate = totalLeadsCount > 0 ? Math.Round((double)wonLeadsCount / totalLeadsCount * 100, 1) : 0, ActiveCampaigns = await _context.Campaign.NotDeletedOnly().CountAsync(x => x.Status == CampaignStatus.OnProgress, ct), WonDealsCount = wonLeadsCount, PendingPayments = await _context.Invoice.NotDeletedOnly().Where(x => x.InvoiceStatus != InvoiceStatus.FullPaid).SumAsync(x => x.SalesOrder!.AfterTaxAmount ?? 0, ct), TotalBudget = await _context.Budget.NotDeletedOnly().SumAsync(x => x.Amount ?? 0, ct), ActualExpense = await _context.Expense.NotDeletedOnly().SumAsync(x => x.Amount ?? 0, ct), RetentionRate = retentionRateCalc, SlaPercentage = slaPercentage, EnterprisePercentage = enterprisePercentage, TargetCvr = targetCvr, AvgDealSize = avgDealSize, OverallCvr = overallCvr, ActiveDealsCount = activeDeals, TotalContacts = totalContacts, TotalCampaigns = totalCampaigns, PipelineBreakdown = pipeline, SalesLeaderboard = leaders, TopCampaigns = campaigns, OverdueInvoices = overdue, RecentLeads = recentLeads, RecentActivities = recentActivities, BantMetrics = bantMetrics, UpcomingFollowUps = followUps, MonthlySalesVsPurchase = monthlyData, WeeklySalesVsPurchase = weeklyData, DealStages = dealStages, LeadSources = leadSources, ConversionMetrics = conversionMetrics, SalesActivities = salesActivities, RecentDeals = recentDeals, ActivityFeeds = activityFeed, PipelineSummary = pipelineSummaryList, RevenueTrend = revenueTrend }; } }