Files
blazor-cms/Features/Root/Home/Cqrs/DashboardHandler.cs
T
2026-07-21 13:52:43 +07:00

524 lines
27 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; }
}
// CMS-specific DTOs for dashboard3 reference layout
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 PatientDistributionDto { public string? Label { get; set; } public string? Value { get; set; } public double PctValue { get; set; } public string? BarColor { get; set; } }
public class BookingStageDto { public string? Stage { get; set; } public int Count { get; set; } public string? Color { get; set; } }
public class RecentBookingDto { public string? Number { get; set; } public string? Patient { get; set; } public string? Resource { get; set; } public DateTime? Date { get; set; } public string? Status { get; set; } }
public class AppointmentActivityDto { public string? Type { get; set; } public string? PatientName { 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 RevenueMetricDto { public string? Label { get; set; } public decimal Value { get; set; } public string? FormattedValue { get; set; } public string? Color { get; set; } }
public class DashboardScmResponse
{
// Original properties (keep backward compatibility)
public int PendingSO { get; set; }
public int PendingPO { get; set; }
public int PendingInvoice { get; set; }
public int PendingBill { get; set; }
public int TotalCustomer { get; set; }
public int TotalVendor { get; set; }
public decimal SalesMTD { get; set; }
public decimal PurchaseMTD { get; set; }
public decimal TotalReceivable { get; set; }
public decimal TotalPayable { get; set; }
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> PayMethodStats { get; set; } = new();
public List<ScmMetricDto> CashFlowMTD { get; set; } = new();
public string FormattedSales => FormatCurrency(SalesMTD);
public string FormattedPurchase => FormatCurrency(PurchaseMTD);
public string FormattedReceivable => FormatCurrency(TotalReceivable);
public string FormattedPayable => FormatCurrency(TotalPayable);
// CMS-specific dashboard properties
public int TotalPatients { get; set; }
public int TotalBookings { get; set; }
public int TotalMedicalRecords { get; set; }
public int ActiveAppointments { get; set; }
public decimal MRR { get; set; }
public double BookingCompletionRate { get; set; }
public int TotalEmployees { get; set; }
public int TotalProducts { get; set; }
public decimal AvgBookingValue { get; set; }
public double OverallConversionRate { get; set; }
public double PatientRetentionRate { get; set; }
public double SlaPercentage { get; set; }
public double EnterprisePercentage { get; set; }
public double TargetCvr { get; set; }
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 List<PatientDistributionDto> PatientDistribution { get; set; } = new();
public List<BookingStageDto> BookingStages { get; set; } = new();
public List<RecentBookingDto> RecentBookings { get; set; } = new();
public List<AppointmentActivityDto> AppointmentActivities { get; set; } = new();
public List<RevenueMetricDto> RevenueMetrics { get; set; } = new();
public string FormattedTotalPatients => TotalPatients.ToString("N0");
public string FormattedTotalBookings => TotalBookings.ToString("N0");
public string FormattedMRR => FormatCurrency(MRR);
public string FormattedAvgBooking => FormatCurrency(AvgBookingValue);
public string FormattedActiveAppointments => ActiveAppointments.ToString("N0");
public string FormattedTotalMedicalRecords => TotalMedicalRecords.ToString("N0");
public static string FormatCurrency(decimal v)
{
if (v >= 1000000) return (v / 1000000m).ToString("F1") + "M";
if (v >= 1000) return (v / 1000m).ToString("F1") + "K";
return 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);
// ===================================================================
// APPROACH: Take top 1000 raw records → materialize → process in memory
// ===================================================================
// ---- Patient raw data (top 1000) ----
var patientsRaw = await _context.Patient
.OrderByDescending(x => x.CreatedAt)
.Take(1000)
.Select(x => new { x.Name, GroupName = x.PatientGroup != null ? x.PatientGroup.Name : null, CategoryName = x.PatientCategory != null ? x.PatientCategory.Name : null })
.ToListAsync(ct);
var totalPatients = patientsRaw.Count;
// Patient distribution by group (processed in memory)
var patientGroupStats = patientsRaw
.GroupBy(x => string.IsNullOrEmpty(x.GroupName) ? "General" : x.GroupName)
.Select(g => new PatientDistributionDto
{
Label = g.Key,
Value = g.Count().ToString(),
PctValue = totalPatients > 0 ? Math.Round((double)g.Count() / totalPatients * 100, 1) : 0,
BarColor = "#2563eb"
}).OrderByDescending(x => x.PctValue).Take(5).ToList();
// Lead Sources / Patient Sources (grouped by category in memory)
var patientGroupCounts = patientsRaw
.GroupBy(x => string.IsNullOrEmpty(x.CategoryName) ? "General" : x.CategoryName)
.Select(g => new { Label = g.Key, Count = g.Count() })
.OrderByDescending(x => x.Count)
.Take(5)
.ToList();
var totalSourceCount = patientGroupCounts.Sum(x => x.Count);
var sourceColors = new[] { "#2563eb", "#f97316", "#ec4899", "#8b5cf6", "#06b6d4" };
var leadSources = patientGroupCounts
.Select((g, i) => new LeadSourceDto
{
Source = g.Label,
Count = g.Count,
Percentage = totalSourceCount > 0 ? Math.Round((double)g.Count / totalSourceCount * 100, 1) : 0,
Color = i < sourceColors.Length ? sourceColors[i] : "#94a3b8"
}).ToList();
// ---- Vendor raw data (top 1000) ----
var vendorsRaw = await _context.Vendor
.OrderByDescending(x => x.CreatedAt)
.Take(1000)
.Select(x => new { x.Name, GroupName = x.VendorGroup != null ? x.VendorGroup.Name : null })
.ToListAsync(ct);
var vendGroups = vendorsRaw
.GroupBy(x => string.IsNullOrEmpty(x.GroupName) ? "General" : x.GroupName)
.Select(g => new ScmMetricDto { Label = g.Key, Value = g.Count().ToString() })
.ToList();
// ---- Payment methods raw data (top 1000) ----
var paymentsRaw = await _context.PaymentReceive
.OrderByDescending(x => x.CreatedAt)
.Take(1000)
.Select(x => new { MethodName = x.PaymentMethod != null ? x.PaymentMethod.Name : null })
.ToListAsync(ct);
var payMethods = paymentsRaw
.GroupBy(x => string.IsNullOrEmpty(x.MethodName) ? "Cash" : x.MethodName)
.Select(g => new ScmMetricDto { Label = g.Key, Value = g.Count().ToString() })
.ToList();
// ---- Product raw data (top 1000 sales order items) ----
var soiRaw = await _context.SalesOrderItem
.OrderByDescending(x => x.CreatedAt)
.Take(1000)
.Select(x => new { ProductName = x.Product != null ? x.Product.Name : null, Qty = x.Quantity ?? 0 })
.ToListAsync(ct);
var topProds = soiRaw
.GroupBy(x => string.IsNullOrEmpty(x.ProductName) ? "N/A" : x.ProductName)
.OrderByDescending(g => g.Sum(x => (double)x.Qty))
.Take(10)
.Select(g => new ScmMetricDto { Label = g.Key, Value = g.Sum(x => (double)x.Qty).ToString("N0") })
.ToList();
// ---- Global flow log (sales orders, top 1000 → take 20) ----
var soRaw = await _context.SalesOrder
.OrderByDescending(x => x.OrderDate)
.Take(1000)
.Select(x => new
{
x.AutoNumber,
CustomerName = x.Customer != null ? x.Customer.Name : "N/A",
TotalQty = x.SalesOrderItemList.Sum(i => i.Quantity ?? 0),
Status = x.OrderStatus.ToString(),
x.OrderDate
})
.ToListAsync(ct);
var globalLogs = soRaw
.Take(20)
.Select(x => new ScmActivityDto
{
Module = "Sales Order",
Number = x.AutoNumber,
Partner = x.CustomerName,
Qty = x.TotalQty,
Status = x.Status,
Date = x.OrderDate
}).ToList();
// ---- Doc counts (simple count queries, fine as-is) ----
var docGrid = new List<ScmMetricDto> {
new() { Label = "Sales Order", Value = (await _context.SalesOrder.CountAsync(ct)).ToString(), Color = "#10b981" },
new() { Label = "Purchase Order", Value = (await _context.PurchaseOrder.CountAsync(ct)).ToString(), Color = "#ef4444" },
new() { Label = "Invoice", Value = (await _context.Invoice.CountAsync(ct)).ToString(), Color = "#14b8a6" },
new() { Label = "Bill", Value = (await _context.Bill.CountAsync(ct)).ToString(), Color = "#f97316" },
new() { Label = "Pay Receive", Value = (await _context.PaymentReceive.CountAsync(ct)).ToString(), Color = "#84cc16" },
new() { Label = "Pay Disburse", Value = (await _context.PaymentDisburse.CountAsync(ct)).ToString(), Color = "#d946ef" }
};
// ---- CMS totals ----
var totalBookings = await _context.Booking.CountAsync(ct);
var totalMedicalRecords = await _context.MedicalRecord.CountAsync(ct);
var activeBookings = await _context.Booking.CountAsync(x => x.Status == BookingStatus.Confirmed, ct);
var totalEmployees = await _context.Employee.CountAsync(ct);
var totalProducts = await _context.Product.CountAsync(ct);
// ---- Booking raw data (top 1000) ----
var bookingsRaw = await _context.Booking
.OrderByDescending(x => x.CreatedAt)
.Take(1000)
.Select(x => new
{
x.AutoNumber,
x.Subject,
ResourceName = x.BookingResource != null ? x.BookingResource.Name : "N/A",
x.StartTime,
Status = x.Status
})
.ToListAsync(ct);
var completedBookings = bookingsRaw.Count(x => x.Status == BookingStatus.Done);
var bookingCompletionRate = totalBookings > 0 ? Math.Round((double)completedBookings / totalBookings * 100, 1) : 0;
// Booking stages (processed from raw data in memory)
var bookingStages = bookingsRaw
.GroupBy(x => x.Status)
.Select(g => new BookingStageDto
{
Stage = g.Key.ToString(),
Count = g.Count(),
Color = g.Key switch
{
BookingStatus.Draft => "#8b5cf6",
BookingStatus.Confirmed => "#f59e0b",
BookingStatus.OnProgress => "#e11d48",
BookingStatus.Done => "#6366f1",
_ => "#94a3b8"
}
}).ToList();
// Recent bookings (top 4 from materialized)
var recentBookings = bookingsRaw
.Take(4)
.Select(x => new RecentBookingDto
{
Number = x.AutoNumber,
Patient = x.Subject ?? "N/A",
Resource = x.ResourceName,
Date = x.StartTime,
Status = x.Status.ToString()
}).ToList();
// Recent deals (from bookings)
var recentDeals = recentBookings.Select(b => new RecentDealDto
{
DealNumber = b.Number,
Description = b.Patient,
Value = 0,
Stage = b.Resource,
Status = b.Status == "Done" ? "Posted" : b.Status == "Confirmed" ? "Pending" : "Draft",
Date = b.Date
}).ToList();
// ---- Medical Record raw data (top 1000) ----
var mrRaw = await _context.MedicalRecord
.OrderByDescending(x => x.CreatedAt)
.Take(1000)
.Select(x => new
{
x.Description,
x.Status,
CategoryName = x.MedicalRecordCategory != null ? x.MedicalRecordCategory.Name : null,
GroupName = x.MedicalRecordGroup != null ? x.MedicalRecordGroup.Name : null,
PatientName = x.Patient != null ? x.Patient.Name : null
})
.ToListAsync(ct);
// Sales activities (medical records grouped by category in memory)
var activityColorsArr = new[] { "#10b981", "#e11d48", "#e11d48", "#6366f1" };
var salesActivities = mrRaw
.GroupBy(x => string.IsNullOrEmpty(x.CategoryName) ? "General" : x.CategoryName)
.OrderByDescending(g => g.Count())
.Take(4)
.Select((g, i) => new SalesActivityDto
{
Category = g.Key,
Count = g.Count(),
Color = i < activityColorsArr.Length ? activityColorsArr[i] : "#94a3b8"
})
.ToList();
// Appointment activities (top 4 from materialized)
var medicalRecords = mrRaw
.Take(4)
.Select(x => new AppointmentActivityDto
{
Type = string.IsNullOrEmpty(x.CategoryName) ? "Checkup" : x.CategoryName,
PatientName = x.PatientName ?? "Patient",
Detail = x.Description ?? "",
ChipLabel = x.Status.ToString(),
ChipColor = x.Status == MedicalRecordStatus.Completed ? "chip-green" : "chip-indigo",
AvatarBg = x.Status == MedicalRecordStatus.Completed ? "#10b981" : "#6366f1",
AvatarText = (string.IsNullOrEmpty(x.GroupName) ? "MR" : x.GroupName).Substring(0, Math.Min(string.IsNullOrEmpty(x.GroupName) ? 2 : x.GroupName.Length, 2)).ToUpper()
}).ToList();
var activityFeeds = medicalRecords.Select(mr => new ActivityFeedDto
{
Title = mr.Type ?? "Medical Record",
Detail = !string.IsNullOrEmpty(mr.PatientName) ? $"Patient: {mr.PatientName}" : "",
TimeAgo = mr.ChipLabel ?? "",
ChipLabel = mr.ChipLabel,
ChipColor = mr.ChipColor,
AvatarBg = mr.AvatarBg,
AvatarText = mr.AvatarText
}).ToList();
// ---- Revenue Trend: top 1000 SalesOrder + PurchaseOrder, materialize, process in memory ----
var soDatesRaw = await _context.SalesOrder
.OrderByDescending(x => x.OrderDate)
.Take(1000)
.Select(x => new { x.OrderDate, Amount = x.AfterTaxAmount ?? 0 })
.ToListAsync(ct);
var poDatesRaw = await _context.PurchaseOrder
.OrderByDescending(x => x.OrderDate)
.Take(1000)
.Select(x => new { x.OrderDate, Amount = x.AfterTaxAmount ?? 0 })
.ToListAsync(ct);
var allSalesByMonth = soDatesRaw
.Where(x => x.OrderDate.HasValue)
.GroupBy(x => new { x.OrderDate!.Value.Year, x.OrderDate.Value.Month })
.OrderByDescending(g => g.Key.Year).ThenByDescending(g => g.Key.Month)
.Take(6)
.Reverse()
.Select(g => new { g.Key.Year, g.Key.Month, Sales = g.Sum(x => (double)x.Amount) })
.ToList();
var allPurchaseByMonth = poDatesRaw
.Where(x => x.OrderDate.HasValue)
.GroupBy(x => new { x.OrderDate!.Value.Year, x.OrderDate.Value.Month })
.ToList();
var monthlyLabels = new[] { "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" };
var revenueTrend = allSalesByMonth
.Select(ym =>
{
var purchaseAmount = allPurchaseByMonth
.Where(g => g.Key.Year == ym.Year && g.Key.Month == ym.Month)
.Sum(g => g.Sum(x => (double)x.Amount));
return new ChartDataPoint
{
Label = monthlyLabels[ym.Month - 1],
SalesAmount = Math.Round(ym.Sales / 1000.0, 1),
PurchaseAmount = Math.Round(purchaseAmount / 1000.0, 1)
};
}).ToList();
// ---- MRR ----
var mrr = await _context.Invoice
.Where(x => x.InvoiceStatus == InvoiceStatus.Confirmed && x.CreatedAt >= start)
.SumAsync(x => x.SalesOrder != null ? x.SalesOrder.AfterTaxAmount ?? 0 : 0, ct);
// ---- Conversion metrics ----
var invoicesPaid = await _context.Invoice.CountAsync(x => x.InvoiceStatus == InvoiceStatus.FullPaid, ct);
var invoicesConfirmed = await _context.Invoice.CountAsync(x => x.InvoiceStatus == InvoiceStatus.Confirmed, ct);
var conversionMetrics = new List<ConversionMetricDto>
{
new() { Label = "Patients->Booked", Current = activeBookings, Target = totalPatients, PctValue = totalPatients > 0 ? Math.Round((double)activeBookings / totalPatients * 100, 1) : 0, BarColor = "#2563eb" },
new() { Label = "Booked->Done", Current = completedBookings, Target = totalBookings > 0 ? totalBookings : 1, PctValue = totalBookings > 0 ? Math.Round((double)completedBookings / totalBookings * 100, 1) : 0, BarColor = "#f97316" },
new() { Label = "Invoice->Paid", Current = invoicesPaid, Target = invoicesConfirmed, PctValue = invoicesConfirmed > 0 ? Math.Round((double)invoicesPaid / invoicesConfirmed * 100, 1) : 0, BarColor = "#ec4899" },
new() { Label = "Overall CVR", Current = completedBookings, Target = totalPatients, PctValue = totalPatients > 0 ? Math.Round((double)completedBookings / totalPatients * 100, 1) : 0, BarColor = "#10b981" }
};
// ---- Pipeline summary ----
var pipelineSummaryList = new List<PipelineSummaryDto>
{
new() { Label = "Total Bookings", Value = totalBookings, FormattedValue = totalBookings.ToString("N0"), IconColorClass = "gc1" },
new() { Label = "Completed", Value = completedBookings, FormattedValue = completedBookings.ToString("N0"), IconColorClass = "gc10" },
new() { Label = "Avg Booking", Value = 0, FormattedValue = "0", IconColorClass = "gc2" }
};
// ---- MTD values ----
var salesMTD = soDatesRaw
.Where(x => x.OrderDate >= start)
.Sum(x => x.Amount);
var totalReceivable = await _context.Invoice
.Where(x => x.InvoiceStatus == InvoiceStatus.Confirmed)
.SumAsync(x => x.SalesOrder != null ? x.SalesOrder.AfterTaxAmount ?? 0 : 0, ct);
// ---- Revenue metrics ----
var arBalance = await _context.Invoice
.Where(x => x.InvoiceStatus != InvoiceStatus.FullPaid && x.InvoiceStatus == InvoiceStatus.Confirmed)
.SumAsync(x => x.SalesOrder != null ? x.SalesOrder.AfterTaxAmount ?? 0 : 0, ct);
var collectionRate = invoicesConfirmed > 0 ? Math.Round((double)invoicesPaid / invoicesConfirmed * 100, 1) : 0;
var revenueMetrics = new List<RevenueMetricDto>
{
new() { Label = "MTD Revenue", Value = salesMTD, FormattedValue = DashboardScmResponse.FormatCurrency(salesMTD), Color = "#2563eb" },
new() { Label = "Total Receivable", Value = totalReceivable, FormattedValue = DashboardScmResponse.FormatCurrency(totalReceivable), Color = "#f97316" },
new() { Label = "AR Balance", Value = arBalance, FormattedValue = DashboardScmResponse.FormatCurrency(arBalance), Color = "#e11d48" },
new() { Label = "Collection Rate", Value = (decimal)collectionRate, FormattedValue = collectionRate.ToString("F1") + "%", Color = "#10b981" }
};
// ---- Computed rates ----
var overallConversionRate = totalPatients > 0 ? Math.Round((double)completedBookings / totalPatients * 100, 1) : 0;
var slaPercentage = totalBookings > 0 ? Math.Round((double)completedBookings / totalBookings * 100, 1) : 94.2;
var enterprisePercentage = totalPatients > 0 ? Math.Round((double)(await _context.Patient.CountAsync(x => x.PatientCategory != null && x.PatientCategory.Name == "VIP", ct)) / totalPatients * 100, 1) : 35;
// ---- Cashflow MTD ----
var cashflowInflow = await _context.PaymentReceive
.Where(x => x.PaymentDate >= start)
.SumAsync(x => x.PaymentAmount ?? 0, ct);
var cashflowOutflow = await _context.PaymentDisburse
.Where(x => x.PaymentDate >= start)
.SumAsync(x => x.PaymentAmount ?? 0, ct);
var res = new DashboardScmResponse
{
// Original properties
PendingSO = await _context.SalesOrder.CountAsync(x => x.OrderStatus == SalesOrderStatus.Draft, ct),
PendingPO = await _context.PurchaseOrder.CountAsync(x => x.OrderStatus == PurchaseOrderStatus.Draft, ct),
PendingInvoice = await _context.Invoice.CountAsync(x => x.InvoiceStatus == InvoiceStatus.Draft, ct),
PendingBill = await _context.Bill.CountAsync(x => x.BillStatus == BillStatus.Draft, ct),
TotalCustomer = totalPatients,
TotalVendor = await _context.Vendor.CountAsync(ct),
SalesMTD = salesMTD,
PurchaseMTD = await _context.PurchaseOrder.Where(x => x.OrderDate >= start).SumAsync(x => x.AfterTaxAmount ?? 0, ct),
TotalReceivable = totalReceivable,
TotalPayable = await _context.Bill.Where(x => x.BillStatus == BillStatus.Confirmed).SumAsync(x => x.PurchaseOrder != null ? x.PurchaseOrder.AfterTaxAmount ?? 0 : 0, ct),
DocGrid = docGrid,
GlobalFlowLog = globalLogs,
TopProductsByQty = topProds,
CustGroupStats = patientGroupStats.Select(p => new ScmMetricDto { Label = p.Label, Value = p.Value }).ToList(),
VendGroupStats = vendGroups,
PayMethodStats = payMethods,
CashFlowMTD = new List<ScmMetricDto> {
new() { Label = "Inflow (Receive)", Value = cashflowInflow.ToString("N0"), Color = "#10b981" },
new() { Label = "Outflow (Disburse)", Value = cashflowOutflow.ToString("N0"), Color = "#ef4444" }
},
// CMS-specific properties
TotalPatients = totalPatients,
TotalBookings = totalBookings,
TotalMedicalRecords = totalMedicalRecords,
ActiveAppointments = activeBookings,
MRR = mrr,
BookingCompletionRate = bookingCompletionRate,
TotalEmployees = totalEmployees,
TotalProducts = totalProducts,
AvgBookingValue = 0,
OverallConversionRate = overallConversionRate,
PatientRetentionRate = 92.4,
SlaPercentage = slaPercentage,
EnterprisePercentage = enterprisePercentage,
TargetCvr = 5.0,
DealStages = bookingStages.Select(b => new DealStageDto { Stage = b.Stage, Count = b.Count, Color = b.Color }).ToList(),
LeadSources = leadSources,
ConversionMetrics = conversionMetrics,
SalesActivities = salesActivities,
RecentDeals = recentDeals,
ActivityFeeds = activityFeeds,
PipelineSummary = pipelineSummaryList,
RevenueTrend = revenueTrend,
PatientDistribution = patientGroupStats,
BookingStages = bookingStages,
RecentBookings = recentBookings,
AppointmentActivities = medicalRecords,
RevenueMetrics = revenueMetrics
};
return res;
}
}