initial commit
This commit is contained in:
@@ -0,0 +1,434 @@
|
||||
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<SalesPipelineDto> PipelineBreakdown { get; set; } = new();
|
||||
public List<LeadActivityDto> RecentActivities { get; set; } = new();
|
||||
public List<CampaignRoiDto> TopCampaigns { get; set; } = new();
|
||||
public List<SalesPerformanceDto> SalesLeaderboard { get; set; } = new();
|
||||
public List<PendingInvoiceDto> OverdueInvoices { get; set; } = new();
|
||||
public List<RecentLeadDto> RecentLeads { get; set; } = new();
|
||||
public List<BantScoreDto> BantMetrics { get; set; } = new();
|
||||
public List<UpcomingFollowUpDto> UpcomingFollowUps { get; set; } = new();
|
||||
public List<ChartDataPoint> MonthlySalesVsPurchase { get; set; } = new();
|
||||
public List<ChartDataPoint> WeeklySalesVsPurchase { get; set; } = new();
|
||||
|
||||
// New properties for dashboard3 layout (matching CRM-Blazor-Contoh)
|
||||
public List<DealStageDto> DealStages { get; set; } = new();
|
||||
public List<LeadSourceDto> LeadSources { get; set; } = new();
|
||||
public List<ConversionMetricDto> ConversionMetrics { get; set; } = new();
|
||||
public List<SalesActivityDto> SalesActivities { get; set; } = new();
|
||||
public List<RecentDealDto> RecentDeals { get; set; } = new();
|
||||
public List<ActivityFeedDto> ActivityFeeds { get; set; } = new();
|
||||
public List<PipelineSummaryDto> PipelineSummary { get; set; } = new();
|
||||
public List<ChartDataPoint> 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<DashboardCrmResponse>;
|
||||
|
||||
public class DashboardHandler : IRequestHandler<GetCrmDashboardQuery, DashboardCrmResponse>
|
||||
{
|
||||
private readonly AppDbContext _context;
|
||||
public DashboardHandler(AppDbContext context) => _context = context;
|
||||
|
||||
public async Task<DashboardCrmResponse> 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<BantScoreDto>
|
||||
{
|
||||
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<ChartDataPoint>();
|
||||
|
||||
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<ChartDataPoint>();
|
||||
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<DealStageDto>
|
||||
{
|
||||
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<PipelineSummaryDto>
|
||||
{
|
||||
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<ConversionMetricDto>
|
||||
{
|
||||
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<SalesActivityDto>
|
||||
{
|
||||
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
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user