2ad6df1195
- Companies list and Company Health now hide Expired/Canceled accounts whose subscription ended 14+ days ago; show/hide toggle via banner - KPI cards on Company Health exclude churned tenants when hidden - showChurned param threads through sort, pagination, search, and filter forms - Powder catalog: fix missing UnitPrice on user-contributed entries; add back-sync to fill catalog gaps on existing matches; wire AiAugmentFromUrl and manual inventory Create into catalog contribute path Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
154 lines
6.4 KiB
C#
154 lines
6.4 KiB
C#
using Microsoft.EntityFrameworkCore;
|
|
using PowderCoating.Core.Entities;
|
|
using PowderCoating.Core.Enums;
|
|
using PowderCoating.Core.Interfaces.Services;
|
|
using PowderCoating.Infrastructure.Data;
|
|
|
|
namespace PowderCoating.Infrastructure.Services;
|
|
|
|
/// <summary>
|
|
/// Implements <see cref="ICompanyListService"/> using <see cref="ApplicationDbContext"/> directly.
|
|
/// Queries require <c>IgnoreQueryFilters()</c> (to bypass the tenant filter and see all companies),
|
|
/// dynamic sort expressions, and cross-entity GROUP BY aggregations — all of which are beyond the
|
|
/// generic <see cref="PowderCoating.Core.Interfaces.IRepository{T}"/>.
|
|
/// </summary>
|
|
public class CompanyListService : ICompanyListService
|
|
{
|
|
private readonly ApplicationDbContext _context;
|
|
|
|
public CompanyListService(ApplicationDbContext context)
|
|
{
|
|
_context = context;
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public async Task<(List<Company> Companies, int TotalCount, int ChurnedCount)> GetPagedAsync(
|
|
string? searchTerm, string sortColumn, string sortDirection, int page, int pageSize,
|
|
bool hideChurned = true)
|
|
{
|
|
var cutoff = DateTime.UtcNow.AddDays(-14);
|
|
|
|
// Always count churned regardless of hideChurned so the banner can show a number.
|
|
var churnedCount = await _context.Companies
|
|
.AsNoTracking()
|
|
.IgnoreQueryFilters()
|
|
.Where(c => !c.IsDeleted
|
|
&& (c.SubscriptionStatus == SubscriptionStatus.Expired || c.SubscriptionStatus == SubscriptionStatus.Canceled)
|
|
&& c.SubscriptionEndDate != null
|
|
&& c.SubscriptionEndDate < cutoff)
|
|
.CountAsync();
|
|
|
|
var query = _context.Companies
|
|
.AsNoTracking()
|
|
.IgnoreQueryFilters()
|
|
.Where(c => !c.IsDeleted)
|
|
.AsQueryable();
|
|
|
|
if (hideChurned)
|
|
query = query.Where(c =>
|
|
!((c.SubscriptionStatus == SubscriptionStatus.Expired || c.SubscriptionStatus == SubscriptionStatus.Canceled)
|
|
&& c.SubscriptionEndDate != null
|
|
&& c.SubscriptionEndDate < cutoff));
|
|
|
|
if (!string.IsNullOrWhiteSpace(searchTerm))
|
|
{
|
|
var s = searchTerm.ToLower();
|
|
query = query.Where(c =>
|
|
c.CompanyName.ToLower().Contains(s) ||
|
|
(c.CompanyCode != null && c.CompanyCode.ToLower().Contains(s)) ||
|
|
(c.PrimaryContactEmail != null && c.PrimaryContactEmail.ToLower().Contains(s)) ||
|
|
(c.Phone != null && c.Phone.ToLower().Contains(s)));
|
|
}
|
|
|
|
query = (sortColumn, sortDirection == "asc") switch
|
|
{
|
|
("CompanyName", true) => query.OrderBy(c => c.CompanyName),
|
|
("CompanyName", false) => query.OrderByDescending(c => c.CompanyName),
|
|
("Plan", true) => query.OrderBy(c => c.SubscriptionPlan),
|
|
("Plan", false) => query.OrderByDescending(c => c.SubscriptionPlan),
|
|
("Status", true) => query.OrderBy(c => c.IsActive),
|
|
("Status", false) => query.OrderByDescending(c => c.IsActive),
|
|
("Created", true) => query.OrderBy(c => c.CreatedAt),
|
|
("Created", false) => query.OrderByDescending(c => c.CreatedAt),
|
|
_ => query.OrderBy(c => c.CompanyName)
|
|
};
|
|
|
|
var totalCount = await query.CountAsync();
|
|
|
|
var companies = await query
|
|
.Include(c => c.Users)
|
|
.Skip((page - 1) * pageSize)
|
|
.Take(pageSize)
|
|
.ToListAsync();
|
|
|
|
return (companies, totalCount, churnedCount);
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public async Task<CompanyCountSummary> GetCountSummaryAsync(IReadOnlyList<int> companyIds)
|
|
{
|
|
var now = DateTime.UtcNow;
|
|
var d30 = now.AddDays(-30);
|
|
var d90 = now.AddDays(-90);
|
|
|
|
var jobCounts = await _context.Jobs
|
|
.IgnoreQueryFilters()
|
|
.Where(j => companyIds.Contains(j.CompanyId) && !j.IsDeleted)
|
|
.GroupBy(j => j.CompanyId)
|
|
.Select(g => new { g.Key, Count = g.Count() })
|
|
.ToDictionaryAsync(x => x.Key, x => x.Count);
|
|
|
|
var quoteCounts = await _context.Quotes
|
|
.IgnoreQueryFilters()
|
|
.Where(q => companyIds.Contains(q.CompanyId) && !q.IsDeleted)
|
|
.GroupBy(q => q.CompanyId)
|
|
.Select(g => new { g.Key, Count = g.Count() })
|
|
.ToDictionaryAsync(x => x.Key, x => x.Count);
|
|
|
|
var customerCounts = await _context.Customers
|
|
.IgnoreQueryFilters()
|
|
.Where(c => companyIds.Contains(c.CompanyId) && !c.IsDeleted)
|
|
.GroupBy(c => c.CompanyId)
|
|
.Select(g => new { g.Key, Count = g.Count() })
|
|
.ToDictionaryAsync(x => x.Key, x => x.Count);
|
|
|
|
var wizardRaw = await _context.CompanyPreferences
|
|
.IgnoreQueryFilters()
|
|
.Where(p => companyIds.Contains(p.CompanyId) && p.SetupWizardCompleted)
|
|
.Select(p => new { p.CompanyId, p.SetupWizardCompletedAt, p.SetupWizardCompletedByName })
|
|
.ToListAsync();
|
|
|
|
var wizardInfo = wizardRaw.ToDictionary(
|
|
x => x.CompanyId,
|
|
x => new CompanyWizardInfo(true, x.SetupWizardCompletedAt, x.SetupWizardCompletedByName));
|
|
|
|
var jobs30 = await _context.Jobs
|
|
.IgnoreQueryFilters()
|
|
.Where(j => companyIds.Contains(j.CompanyId) && !j.IsDeleted && j.CreatedAt >= d30)
|
|
.GroupBy(j => j.CompanyId)
|
|
.Select(g => new { g.Key, Count = g.Count() })
|
|
.ToDictionaryAsync(x => x.Key, x => x.Count);
|
|
|
|
var jobs90 = await _context.Jobs
|
|
.IgnoreQueryFilters()
|
|
.Where(j => companyIds.Contains(j.CompanyId) && !j.IsDeleted && j.CreatedAt >= d90)
|
|
.GroupBy(j => j.CompanyId)
|
|
.Select(g => new { g.Key, Count = g.Count() })
|
|
.ToDictionaryAsync(x => x.Key, x => x.Count);
|
|
|
|
var lastLoginRaw = await _context.Users
|
|
.IgnoreQueryFilters()
|
|
.Where(u => companyIds.Contains(u.CompanyId) && u.LastLoginDate != null)
|
|
.GroupBy(u => u.CompanyId)
|
|
.Select(g => new { CompanyId = g.Key, Last = g.Max(u => u.LastLoginDate) })
|
|
.ToListAsync();
|
|
|
|
var lastLogins = lastLoginRaw.ToDictionary(
|
|
x => x.CompanyId,
|
|
x => x.Last);
|
|
|
|
return new CompanyCountSummary(jobCounts, quoteCounts, customerCounts, wizardInfo,
|
|
jobs30, jobs90, lastLogins);
|
|
}
|
|
}
|