Refactor dashboard queries to push filtering and aggregation into the database
DashboardReadService no longer loads full entity lists and filters in memory. All job panels (today/overdue/in-progress) now execute targeted COUNT + capped SELECT queries in SQL. AR aging buckets, powder order lines, bill totals, and active-customer counts are all aggregated at the DB level. The SuperAdmin action previously loaded every company row to compute plan distribution and alert lists; it now delegates to a new GetSuperAdminDashboardDataAsync() that uses SQL GROUP BY and projections instead. DashboardIndexData record updated to carry pre-sliced counts and capped lists so the controller only does lightweight DTO projection. DashboardPowderOrderLineData replaces the deep Job→JobItem→Coat Include chains with a single flat coat query projected in SQL. OnlineUserMiddleware switches its per-user throttle from a static ConcurrentDictionary (grows forever) to IMemoryCache with a 60-second sliding expiry. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -26,26 +26,6 @@ public class DashboardController : Controller
|
||||
private readonly UserManager<ApplicationUser> _userManager;
|
||||
private readonly ISubscriptionService _subscriptionService;
|
||||
|
||||
private static readonly string[] CompletedStatusCodes =
|
||||
[
|
||||
"COMPLETED",
|
||||
"READY_FOR_PICKUP",
|
||||
"DELIVERED",
|
||||
"CANCELLED"
|
||||
];
|
||||
|
||||
private static readonly string[] InProgressStatusCodes =
|
||||
[
|
||||
"IN_PREPARATION",
|
||||
"SANDBLASTING",
|
||||
"MASKING_TAPING",
|
||||
"CLEANING",
|
||||
"IN_OVEN",
|
||||
"COATING",
|
||||
"CURING",
|
||||
"QUALITY_CHECK"
|
||||
];
|
||||
|
||||
public DashboardController(
|
||||
IUnitOfWork unitOfWork,
|
||||
ILogger<DashboardController> logger,
|
||||
@@ -79,48 +59,27 @@ public class DashboardController : Controller
|
||||
try
|
||||
{
|
||||
var today = DateTime.Today;
|
||||
var lookAheadDate = today.AddDays(7);
|
||||
|
||||
var data = await _dashboardRead.GetIndexDataAsync(today);
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// Job panels — in-memory split of the pre-fetched activeJobs list
|
||||
// Job panels
|
||||
// ---------------------------------------------------------------
|
||||
var todaysJobsFiltered = data.ActiveJobs
|
||||
.Where(j => (j.ScheduledDate.HasValue && j.ScheduledDate.Value.Date == today) ||
|
||||
(j.DueDate.HasValue && j.DueDate.Value.Date == today));
|
||||
var todaysJobsCount = todaysJobsFiltered.Count();
|
||||
var todaysJobs = todaysJobsFiltered
|
||||
.OrderBy(j => j.JobPriority.DisplayOrder)
|
||||
.ThenBy(j => j.ScheduledDate ?? j.DueDate)
|
||||
.Take(10)
|
||||
var todaysJobs = data.TodaysJobs
|
||||
.Select(MapJobDto)
|
||||
.ToList();
|
||||
|
||||
var overdueJobsFiltered = data.ActiveJobs
|
||||
.Where(j => j.DueDate.HasValue && j.DueDate.Value.Date < today);
|
||||
var overdueJobsCount = overdueJobsFiltered.Count();
|
||||
var overdueJobs = overdueJobsFiltered
|
||||
.OrderBy(j => j.JobPriority.DisplayOrder)
|
||||
.ThenBy(j => j.DueDate)
|
||||
.Take(10)
|
||||
var overdueJobs = data.OverdueJobs
|
||||
.Select(MapJobDto)
|
||||
.ToList();
|
||||
|
||||
var inProgressJobs = data.ActiveJobs
|
||||
.Where(j => InProgressStatusCodes.Contains(j.JobStatus.StatusCode))
|
||||
.OrderBy(j => j.JobPriority.DisplayOrder)
|
||||
.ThenBy(j => j.ScheduledDate)
|
||||
.Take(10)
|
||||
var inProgressJobs = data.InProgressJobs
|
||||
.Select(MapJobDto)
|
||||
.ToList();
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// Appointments
|
||||
// ---------------------------------------------------------------
|
||||
var todaysAppointmentsCount = data.TodaysAppointments.Count;
|
||||
var todaysAppointments = data.TodaysAppointments
|
||||
.Take(10)
|
||||
.Select(a => new DashboardAppointmentDto
|
||||
{
|
||||
Id = a.Id,
|
||||
@@ -140,12 +99,7 @@ public class DashboardController : Controller
|
||||
// ---------------------------------------------------------------
|
||||
// Low stock items
|
||||
// ---------------------------------------------------------------
|
||||
var lowStockAll = await _unitOfWork.InventoryItems.FindAsync(
|
||||
i => i.IsActive && i.QuantityOnHand <= i.ReorderPoint);
|
||||
var lowStockCount = lowStockAll.Count();
|
||||
var lowStockItems = lowStockAll
|
||||
.OrderBy(i => i.QuantityOnHand)
|
||||
.Take(10)
|
||||
var lowStockItems = data.LowStockItems
|
||||
.Select(i => new DashboardLowStockDto
|
||||
{
|
||||
Id = i.Id,
|
||||
@@ -177,8 +131,6 @@ public class DashboardController : Controller
|
||||
// Quotes
|
||||
// ---------------------------------------------------------------
|
||||
var pendingQuotes = data.PendingQuotes
|
||||
.OrderBy(q => q.ExpirationDate)
|
||||
.Take(10)
|
||||
.Select(q => new DashboardQuoteDto
|
||||
{
|
||||
Id = q.Id,
|
||||
@@ -195,14 +147,7 @@ public class DashboardController : Controller
|
||||
StatusDisplayName = q.QuoteStatus.DisplayName
|
||||
}).ToList();
|
||||
|
||||
var pendingQuoteValue = data.PendingQuotes.Sum(q => q.Total);
|
||||
|
||||
var expiringQuotes = data.PendingQuotes
|
||||
.Where(q => q.ExpirationDate.HasValue
|
||||
&& q.ExpirationDate.Value.Date >= today
|
||||
&& q.ExpirationDate.Value.Date <= lookAheadDate)
|
||||
.OrderBy(q => q.ExpirationDate)
|
||||
.Take(10)
|
||||
var expiringQuotes = data.ExpiringQuotes
|
||||
.Select(q => new DashboardQuoteDto
|
||||
{
|
||||
Id = q.Id,
|
||||
@@ -219,26 +164,10 @@ public class DashboardController : Controller
|
||||
StatusDisplayName = q.QuoteStatus.DisplayName
|
||||
}).ToList();
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// Active customers
|
||||
// ---------------------------------------------------------------
|
||||
var activeCustomersCount = await _unitOfWork.Customers.CountAsync(c => c.IsActive);
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// Invoices & AR aging
|
||||
// ---------------------------------------------------------------
|
||||
var outstandingAr = data.OpenInvoices.Sum(i => i.BalanceDue);
|
||||
|
||||
var overdueInvoicesList = data.OpenInvoices
|
||||
.Where(i => i.DueDate.HasValue && i.DueDate.Value.Date < today)
|
||||
.OrderBy(i => i.DueDate)
|
||||
.ToList();
|
||||
|
||||
var overdueInvoicesCount = overdueInvoicesList.Count;
|
||||
var overdueInvoicesAmount = overdueInvoicesList.Sum(i => i.BalanceDue);
|
||||
|
||||
var overdueInvoices = overdueInvoicesList
|
||||
.Take(6)
|
||||
var overdueInvoices = data.OverdueInvoices
|
||||
.Select(i => new DashboardInvoiceDto
|
||||
{
|
||||
Id = i.Id,
|
||||
@@ -252,24 +181,6 @@ public class DashboardController : Controller
|
||||
})
|
||||
.ToList();
|
||||
|
||||
// AR Aging buckets
|
||||
decimal agingCurrent = 0, aging1To30 = 0, aging31To60 = 0, aging61To90 = 0, agingOver90 = 0;
|
||||
foreach (var inv in data.OpenInvoices)
|
||||
{
|
||||
if (!inv.DueDate.HasValue || inv.DueDate.Value.Date >= today)
|
||||
{
|
||||
agingCurrent += inv.BalanceDue;
|
||||
}
|
||||
else
|
||||
{
|
||||
var daysLate = (int)(today - inv.DueDate.Value.Date).TotalDays;
|
||||
if (daysLate <= 30) aging1To30 += inv.BalanceDue;
|
||||
else if (daysLate <= 60) aging31To60 += inv.BalanceDue;
|
||||
else if (daysLate <= 90) aging61To90 += inv.BalanceDue;
|
||||
else agingOver90 += inv.BalanceDue;
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// Payments
|
||||
// ---------------------------------------------------------------
|
||||
@@ -278,7 +189,7 @@ public class DashboardController : Controller
|
||||
{
|
||||
Id = p.Id,
|
||||
InvoiceId = p.InvoiceId,
|
||||
InvoiceNumber = p.Invoice?.InvoiceNumber ?? "—",
|
||||
InvoiceNumber = p.Invoice?.InvoiceNumber ?? "-",
|
||||
CustomerName = p.Invoice?.Customer?.CompanyName
|
||||
?? $"{p.Invoice?.Customer?.ContactFirstName} {p.Invoice?.Customer?.ContactLastName}".Trim(),
|
||||
Amount = p.Amount,
|
||||
@@ -298,11 +209,7 @@ public class DashboardController : Controller
|
||||
// ---------------------------------------------------------------
|
||||
// Equipment alerts
|
||||
// ---------------------------------------------------------------
|
||||
var equipmentAlerts = (await _unitOfWork.Equipment.FindAsync(
|
||||
e => e.Status == EquipmentStatus.NeedsMaintenance ||
|
||||
e.Status == EquipmentStatus.OutOfService))
|
||||
.OrderByDescending(e => e.Status == EquipmentStatus.OutOfService ? 1 : 0)
|
||||
.Take(5)
|
||||
var equipmentAlerts = data.EquipmentAlerts
|
||||
.Select(e => new DashboardEquipmentAlertDto
|
||||
{
|
||||
Id = e.Id,
|
||||
@@ -359,134 +266,12 @@ public class DashboardController : Controller
|
||||
// ---------------------------------------------------------------
|
||||
// Powder orders needed
|
||||
// ---------------------------------------------------------------
|
||||
var powderFlat = data.JobsNeedingPowder
|
||||
.SelectMany(j => j.JobItems
|
||||
.SelectMany(i => i.Coats
|
||||
.Where(c => !c.IsDeleted && !c.PowderOrdered && c.PowderToOrder > 0
|
||||
&& (c.InventoryItemId == null || c.InventoryItem!.QuantityOnHand < c.PowderToOrder))
|
||||
.Select(c =>
|
||||
{
|
||||
var vendor = c.Vendor ?? c.InventoryItem?.PrimaryVendor;
|
||||
return new
|
||||
{
|
||||
CoatId = c.Id,
|
||||
JobId = j.Id,
|
||||
JobNumber = j.JobNumber,
|
||||
CustomerName = j.Customer?.CompanyName ?? j.Customer?.ContactFirstName ?? "Unknown",
|
||||
CoatName = c.CoatName,
|
||||
ColorName = c.ColorName ?? c.InventoryItem?.ColorName,
|
||||
ColorCode = c.ColorCode ?? c.InventoryItem?.ColorCode,
|
||||
Finish = c.Finish ?? c.InventoryItem?.Finish,
|
||||
SKU = c.InventoryItem?.SKU,
|
||||
LbsToOrder = c.PowderToOrder!.Value,
|
||||
CostPerLb = c.PowderCostPerLb ?? c.InventoryItem?.UnitCost,
|
||||
VendorId = vendor?.Id,
|
||||
VendorName = vendor?.CompanyName,
|
||||
VendorPhone = vendor?.Phone,
|
||||
VendorEmail = vendor?.Email,
|
||||
};
|
||||
})))
|
||||
.ToList();
|
||||
|
||||
var powderOrderGroups = powderFlat
|
||||
.GroupBy(l => l.VendorId)
|
||||
.Select(g =>
|
||||
{
|
||||
var first = g.First();
|
||||
return new PowderOrderVendorGroupDto
|
||||
{
|
||||
VendorId = g.Key,
|
||||
VendorName = first.VendorName ?? "No Vendor Assigned",
|
||||
VendorPhone = first.VendorPhone,
|
||||
VendorEmail = first.VendorEmail,
|
||||
TotalLbsNeeded = g.Sum(l => l.LbsToOrder),
|
||||
TotalEstCost = g.Sum(l => l.CostPerLb.HasValue ? l.LbsToOrder * l.CostPerLb.Value : 0),
|
||||
Lines = g.Select(l => new PowderOrderLineDto
|
||||
{
|
||||
CoatId = l.CoatId,
|
||||
JobId = l.JobId,
|
||||
JobNumber = l.JobNumber,
|
||||
CustomerName = l.CustomerName,
|
||||
CoatName = l.CoatName,
|
||||
ColorName = l.ColorName,
|
||||
ColorCode = l.ColorCode,
|
||||
Finish = l.Finish,
|
||||
SKU = l.SKU,
|
||||
LbsToOrder = l.LbsToOrder,
|
||||
CostPerLb = l.CostPerLb,
|
||||
}).OrderBy(l => l.JobNumber).ThenBy(l => l.CoatName).ToList()
|
||||
};
|
||||
})
|
||||
.OrderBy(g => g.VendorName)
|
||||
.ToList();
|
||||
var powderOrderGroups = MapPowderOrderGroups(data.PowderOrdersNeeded);
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// Powder orders placed
|
||||
// ---------------------------------------------------------------
|
||||
var placedFlat = data.JobsWithOrderedPowder
|
||||
.SelectMany(j => j.JobItems
|
||||
.SelectMany(i => i.Coats
|
||||
.Where(c => !c.IsDeleted && c.PowderOrdered && !c.PowderReceived)
|
||||
.Select(c =>
|
||||
{
|
||||
var vendor = c.Vendor ?? c.InventoryItem?.PrimaryVendor;
|
||||
return new
|
||||
{
|
||||
CoatId = c.Id,
|
||||
JobId = j.Id,
|
||||
JobNumber = j.JobNumber,
|
||||
CustomerName = j.Customer?.CompanyName ?? j.Customer?.ContactFirstName ?? "Unknown",
|
||||
CoatName = c.CoatName,
|
||||
ColorName = c.ColorName ?? c.InventoryItem?.ColorName,
|
||||
ColorCode = c.ColorCode ?? c.InventoryItem?.ColorCode,
|
||||
Finish = c.Finish ?? c.InventoryItem?.Finish,
|
||||
SKU = c.InventoryItem?.SKU,
|
||||
LbsToOrder = c.PowderToOrder ?? 0m,
|
||||
CostPerLb = c.PowderCostPerLb ?? c.InventoryItem?.UnitCost,
|
||||
OrderedAt = c.PowderOrderedAt,
|
||||
HasInventoryItem = c.InventoryItemId.HasValue,
|
||||
VendorId = vendor?.Id,
|
||||
VendorName = vendor?.CompanyName,
|
||||
VendorPhone = vendor?.Phone,
|
||||
VendorEmail = vendor?.Email,
|
||||
};
|
||||
})))
|
||||
.ToList();
|
||||
|
||||
var powderPlacedGroups = placedFlat
|
||||
.GroupBy(l => l.VendorId)
|
||||
.Select(g =>
|
||||
{
|
||||
var first = g.First();
|
||||
return new PowderOrderVendorGroupDto
|
||||
{
|
||||
VendorId = g.Key,
|
||||
VendorName = first.VendorName ?? "No Vendor Assigned",
|
||||
VendorPhone = first.VendorPhone,
|
||||
VendorEmail = first.VendorEmail,
|
||||
TotalLbsNeeded = g.Sum(l => l.LbsToOrder),
|
||||
TotalEstCost = g.Sum(l => l.CostPerLb.HasValue ? l.LbsToOrder * l.CostPerLb.Value : 0),
|
||||
Lines = g.Select(l => new PowderOrderLineDto
|
||||
{
|
||||
CoatId = l.CoatId,
|
||||
JobId = l.JobId,
|
||||
JobNumber = l.JobNumber,
|
||||
CustomerName = l.CustomerName,
|
||||
CoatName = l.CoatName,
|
||||
ColorName = l.ColorName,
|
||||
ColorCode = l.ColorCode,
|
||||
Finish = l.Finish,
|
||||
SKU = l.SKU,
|
||||
LbsToOrder = l.LbsToOrder,
|
||||
CostPerLb = l.CostPerLb,
|
||||
OrderedAt = l.OrderedAt,
|
||||
HasInventoryItem = l.HasInventoryItem,
|
||||
VendorId = l.VendorId,
|
||||
}).OrderBy(l => l.OrderedAt).ThenBy(l => l.JobNumber).ToList()
|
||||
};
|
||||
})
|
||||
.OrderBy(g => g.VendorName)
|
||||
.ToList();
|
||||
var powderPlacedGroups = MapPowderOrderGroups(data.PowderOrdersPlaced);
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// Bills due
|
||||
@@ -495,7 +280,7 @@ public class DashboardController : Controller
|
||||
{
|
||||
Id = b.Id,
|
||||
BillNumber = b.BillNumber,
|
||||
VendorName = b.Vendor.CompanyName,
|
||||
VendorName = b.Vendor?.CompanyName ?? "Unknown",
|
||||
BalanceDue = b.BalanceDue,
|
||||
DueDate = b.DueDate,
|
||||
IsOverdue = b.DueDate.HasValue && b.DueDate.Value.Date < today,
|
||||
@@ -506,28 +291,28 @@ public class DashboardController : Controller
|
||||
var vm = new DashboardViewModel
|
||||
{
|
||||
// Counts
|
||||
ActiveJobsCount = data.ActiveJobs.Count,
|
||||
TodaysJobsCount = todaysJobsCount,
|
||||
OverdueJobsCount = overdueJobsCount,
|
||||
TodaysAppointmentsCount = todaysAppointmentsCount,
|
||||
LowStockCount = lowStockCount,
|
||||
PendingMaintenanceCount = data.UpcomingMaintenance.Count,
|
||||
PendingQuotesCount = data.PendingQuotes.Count,
|
||||
PendingQuoteValue = pendingQuoteValue,
|
||||
ActiveJobsCount = data.ActiveJobsCount,
|
||||
TodaysJobsCount = data.TodaysJobsCount,
|
||||
OverdueJobsCount = data.OverdueJobsCount,
|
||||
TodaysAppointmentsCount = data.TodaysAppointmentsCount,
|
||||
LowStockCount = data.LowStockCount,
|
||||
PendingMaintenanceCount = data.PendingMaintenanceCount,
|
||||
PendingQuotesCount = data.PendingQuotesCount,
|
||||
PendingQuoteValue = data.PendingQuoteValue,
|
||||
MonthlyRevenue = data.MonthlyRevenue,
|
||||
ActiveCustomersCount = activeCustomersCount,
|
||||
ActiveCustomersCount = data.ActiveCustomersCount,
|
||||
|
||||
// Financial KPIs
|
||||
OutstandingAr = outstandingAr,
|
||||
OutstandingAr = data.OutstandingAr,
|
||||
CollectedThisMonth = data.CollectedThisMonth,
|
||||
InvoicedThisMonth = data.InvoicedThisMonth,
|
||||
OverdueInvoicesCount = overdueInvoicesCount,
|
||||
OverdueInvoicesAmount = overdueInvoicesAmount,
|
||||
AgingCurrent = agingCurrent,
|
||||
AgingDays1To30 = aging1To30,
|
||||
AgingDays31To60 = aging31To60,
|
||||
AgingDays61To90 = aging61To90,
|
||||
AgingDaysOver90 = agingOver90,
|
||||
OverdueInvoicesCount = data.OverdueInvoicesCount,
|
||||
OverdueInvoicesAmount = data.OverdueInvoicesAmount,
|
||||
AgingCurrent = data.ArAging.Current,
|
||||
AgingDays1To30 = data.ArAging.Days1To30,
|
||||
AgingDays31To60 = data.ArAging.Days31To60,
|
||||
AgingDays61To90 = data.ArAging.Days61To90,
|
||||
AgingDaysOver90 = data.ArAging.DaysOver90,
|
||||
|
||||
// Sections
|
||||
TodaysJobs = todaysJobs,
|
||||
@@ -545,14 +330,14 @@ public class DashboardController : Controller
|
||||
|
||||
// Bills Due
|
||||
BillsDue = billsDue,
|
||||
BillsDueCount = billsDue.Count,
|
||||
BillsDueAmount = billsDue.Sum(b => b.BalanceDue),
|
||||
BillsDueCount = data.BillsDueCount,
|
||||
BillsDueAmount = data.BillsDueAmount,
|
||||
|
||||
// Powder orders
|
||||
PowderOrdersNeeded = powderOrderGroups,
|
||||
PowderOrdersNeededCount = powderFlat.Count,
|
||||
PowderOrdersNeededCount = data.PowderOrdersNeeded.Count,
|
||||
PowderOrdersPlaced = powderPlacedGroups,
|
||||
PowderOrdersPlacedCount = placedFlat.Count,
|
||||
PowderOrdersPlacedCount = data.PowderOrdersPlaced.Count,
|
||||
|
||||
TipOfTheDay = data.TipOfTheDay
|
||||
};
|
||||
@@ -767,7 +552,7 @@ public class DashboardController : Controller
|
||||
DoneSubLabel = "Your payment defaults are locked in.",
|
||||
Icon = "bi-file-earmark-text",
|
||||
CtaText = "Set payment terms",
|
||||
CtaUrl = Url.Action("Index", "CompanySettings") + "#general"
|
||||
CtaUrl = Url.Action("Index", "CompanySettings") + "#app-defaults"
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1040,76 +825,41 @@ public class DashboardController : Controller
|
||||
try
|
||||
{
|
||||
var today = DateTime.Today;
|
||||
|
||||
var allCompanies = await _unitOfWork.Companies.GetAllAsync(ignoreQueryFilters: true);
|
||||
var companies = allCompanies.Where(c => !c.IsDeleted).ToList();
|
||||
|
||||
var totalUsers = await _dashboardRead.GetTotalUserCountAsync();
|
||||
|
||||
var graceCutoff = today.AddDays(-AppConstants.SubscriptionConstants.GracePeriodDays);
|
||||
|
||||
var planConfigs = (await _unitOfWork.SubscriptionPlanConfigs.FindAsync(
|
||||
c => c.IsActive, ignoreQueryFilters: true))
|
||||
.OrderBy(c => c.SortOrder)
|
||||
.ToList();
|
||||
|
||||
var planLookup = planConfigs.ToDictionary(c => c.Plan, c => c.DisplayName);
|
||||
string PlanName(int plan) => planLookup.TryGetValue(plan, out var name) ? name : plan.ToString();
|
||||
|
||||
var companyAlerts = companies
|
||||
.Where(c => c.SubscriptionEndDate.HasValue && c.SubscriptionEndDate.Value.Date < today)
|
||||
.OrderBy(c => c.SubscriptionEndDate)
|
||||
.Take(20)
|
||||
.Select(c =>
|
||||
{
|
||||
var daysOverdue = (int)(today - c.SubscriptionEndDate!.Value.Date).TotalDays;
|
||||
return new PlatformCompanyAlertDto
|
||||
{
|
||||
Id = c.Id,
|
||||
CompanyName = c.CompanyName,
|
||||
Plan = c.SubscriptionPlan,
|
||||
PlanDisplayName = PlanName(c.SubscriptionPlan),
|
||||
Status = c.SubscriptionStatus,
|
||||
SubscriptionEndDate = c.SubscriptionEndDate,
|
||||
DaysOverdue = daysOverdue,
|
||||
IsActive = c.IsActive
|
||||
};
|
||||
})
|
||||
.ToList();
|
||||
|
||||
var recentCompanies = companies
|
||||
.OrderByDescending(c => c.CreatedAt)
|
||||
.Take(10)
|
||||
.Select(c => new PlatformRecentCompanyDto
|
||||
{
|
||||
Id = c.Id,
|
||||
CompanyName = c.CompanyName,
|
||||
Plan = c.SubscriptionPlan,
|
||||
PlanDisplayName = PlanName(c.SubscriptionPlan),
|
||||
Status = c.SubscriptionStatus,
|
||||
IsActive = c.IsActive,
|
||||
CreatedAt = c.CreatedAt
|
||||
})
|
||||
.ToList();
|
||||
|
||||
var planDistribution = planConfigs.ToDictionary(
|
||||
c => c.Plan,
|
||||
c => (c.DisplayName, companies.Count(comp => comp.SubscriptionPlan == c.Plan)));
|
||||
var data = await _dashboardRead.GetSuperAdminDashboardDataAsync(today);
|
||||
|
||||
var vm = new SuperAdminDashboardViewModel
|
||||
{
|
||||
TotalCompanies = companies.Count,
|
||||
ActiveCompanies = companies.Count(c => c.IsActive),
|
||||
InactiveCompanies = companies.Count(c => !c.IsActive),
|
||||
TotalUsers = totalUsers,
|
||||
PlanDistribution = planDistribution,
|
||||
ActiveSubscriptions = companies.Count(c => c.SubscriptionStatus == SubscriptionStatus.Active),
|
||||
GracePeriodCount = companies.Count(c => c.SubscriptionStatus == SubscriptionStatus.GracePeriod),
|
||||
ExpiredCount = companies.Count(c =>
|
||||
c.SubscriptionStatus == SubscriptionStatus.Expired ||
|
||||
c.SubscriptionStatus == SubscriptionStatus.Canceled),
|
||||
CompanyAlerts = companyAlerts,
|
||||
RecentCompanies = recentCompanies
|
||||
TotalCompanies = data.TotalCompanies,
|
||||
ActiveCompanies = data.ActiveCompanies,
|
||||
InactiveCompanies = data.InactiveCompanies,
|
||||
TotalUsers = data.TotalUsers,
|
||||
PlanDistribution = data.PlanDistribution.ToDictionary(
|
||||
kvp => kvp.Key,
|
||||
kvp => (kvp.Value.DisplayName, kvp.Value.Count)),
|
||||
ActiveSubscriptions = data.ActiveSubscriptions,
|
||||
GracePeriodCount = data.GracePeriodCount,
|
||||
ExpiredCount = data.ExpiredCount,
|
||||
CompanyAlerts = data.CompanyAlerts.Select(c => new PlatformCompanyAlertDto
|
||||
{
|
||||
Id = c.Id,
|
||||
CompanyName = c.CompanyName,
|
||||
Plan = c.Plan,
|
||||
PlanDisplayName = c.PlanDisplayName,
|
||||
Status = c.Status,
|
||||
SubscriptionEndDate = c.SubscriptionEndDate,
|
||||
DaysOverdue = c.DaysOverdue,
|
||||
IsActive = c.IsActive
|
||||
}).ToList(),
|
||||
RecentCompanies = data.RecentCompanies.Select(c => new PlatformRecentCompanyDto
|
||||
{
|
||||
Id = c.Id,
|
||||
CompanyName = c.CompanyName,
|
||||
Plan = c.Plan,
|
||||
PlanDisplayName = c.PlanDisplayName,
|
||||
Status = c.Status,
|
||||
IsActive = c.IsActive,
|
||||
CreatedAt = c.CreatedAt
|
||||
}).ToList()
|
||||
};
|
||||
|
||||
return View(vm);
|
||||
@@ -1122,6 +872,46 @@ public class DashboardController : Controller
|
||||
}
|
||||
}
|
||||
|
||||
private static List<PowderOrderVendorGroupDto> MapPowderOrderGroups(
|
||||
IEnumerable<DashboardPowderOrderLineData> lines) =>
|
||||
lines.GroupBy(l => l.VendorId)
|
||||
.Select(g =>
|
||||
{
|
||||
var first = g.First();
|
||||
return new PowderOrderVendorGroupDto
|
||||
{
|
||||
VendorId = g.Key,
|
||||
VendorName = first.VendorName ?? "No Vendor Assigned",
|
||||
VendorPhone = first.VendorPhone,
|
||||
VendorEmail = first.VendorEmail,
|
||||
TotalLbsNeeded = g.Sum(l => l.LbsToOrder),
|
||||
TotalEstCost = g.Sum(l => l.CostPerLb.HasValue ? l.LbsToOrder * l.CostPerLb.Value : 0m),
|
||||
Lines = g.Select(l => new PowderOrderLineDto
|
||||
{
|
||||
CoatId = l.CoatId,
|
||||
JobId = l.JobId,
|
||||
JobNumber = l.JobNumber,
|
||||
CustomerName = l.CustomerName,
|
||||
CoatName = l.CoatName,
|
||||
ColorName = l.ColorName,
|
||||
ColorCode = l.ColorCode,
|
||||
Finish = l.Finish,
|
||||
SKU = l.SKU,
|
||||
LbsToOrder = l.LbsToOrder,
|
||||
CostPerLb = l.CostPerLb,
|
||||
OrderedAt = l.OrderedAt,
|
||||
HasInventoryItem = l.HasInventoryItem,
|
||||
VendorId = l.VendorId
|
||||
})
|
||||
.OrderBy(l => l.OrderedAt ?? DateTime.MinValue)
|
||||
.ThenBy(l => l.JobNumber)
|
||||
.ThenBy(l => l.CoatName)
|
||||
.ToList()
|
||||
};
|
||||
})
|
||||
.OrderBy(g => g.VendorName)
|
||||
.ToList();
|
||||
|
||||
/// <summary>
|
||||
/// Projects a <see cref="Core.Entities.Job"/> into a lightweight <see cref="DashboardJobDto"/>
|
||||
/// for use in dashboard job lists. Centralising the mapping in one static helper ensures that
|
||||
|
||||
Reference in New Issue
Block a user