using Data.SeaHavenIndustries; using Microsoft.EntityFrameworkCore; using System.Linq.Expressions; using System.Reflection; using SeaHaven.DataServices.Interfaces; namespace SeaHaven.DataServices.Implementation { public class VendorDataService : IVendorDataService { private readonly ApplicationDbContext _context; public VendorDataService(ApplicationDbContext context) { _context = context; } public async Task GetByIdAsync(int id) { return await _context.Vendors.FirstOrDefaultAsync(v => v.Id == id); } public async Task GetByIdWithDetailsAsync(int id) { return await ProjectWithTotalJobs( _context.Vendors .AsNoTracking() .Where(v => v.Id == id)) .FirstOrDefaultAsync(); } public async Task> GetAllAsync() { return await _context.Vendors.ToListAsync(); } public async Task<(IEnumerable Items, int TotalCount)> GetPagedAsync( int page, int pageSize, string? search = null, bool? isActive = true, IReadOnlyCollection? companies = null, IReadOnlyCollection? trades = null, IReadOnlyCollection? locations = null, IReadOnlyCollection? jobBuckets = null) { var projectedQuery = BuildDirectoryQuery( search, isActive, companies, trades, locations, jobBuckets); var totalCount = await projectedQuery.CountAsync(); var items = await projectedQuery .OrderByDescending(v => v.Id) .Skip((page - 1) * pageSize) .Take(pageSize) .ToListAsync(); return (items, totalCount); } internal IQueryable BuildDirectoryQuery( string? search = null, bool? isActive = true, IReadOnlyCollection? companies = null, IReadOnlyCollection? trades = null, IReadOnlyCollection? locations = null, IReadOnlyCollection? jobBuckets = null) { var query = _context.Vendors .AsNoTracking() .Where(v => v.IsDeleted == null || v.IsDeleted == false); if (isActive.HasValue) query = query.Where(v => v.IsActive == isActive.Value); if (!string.IsNullOrWhiteSpace(search)) { var normalizedSearch = search.ToLower(); query = query.Where(v => (v.CompanyName ?? "").ToLower().Contains(normalizedSearch) || (v.ContactName ?? "").ToLower().Contains(normalizedSearch) || (v.Email ?? "").ToLower().Contains(normalizedSearch) || (v.TradeSpecialties ?? "").ToLower().Contains(normalizedSearch) || (v.City ?? "").ToLower().Contains(normalizedSearch) || (v.State ?? "").ToLower().Contains(normalizedSearch) || (v.Zip ?? "").ToLower().Contains(normalizedSearch)); } var selectedCompanies = companies? .Where(value => !string.IsNullOrWhiteSpace(value)) .Select(value => value.Trim().ToLower()) .Distinct() .ToArray() ?? Array.Empty(); if (selectedCompanies.Length > 0) query = query.Where(v => selectedCompanies.Contains((v.CompanyName ?? "").ToLower())); var selectedTrades = trades? .Where(value => !string.IsNullOrWhiteSpace(value)) .Select(value => value.Trim().ToLower()) .Distinct() .ToArray() ?? Array.Empty(); if (selectedTrades.Length > 0) query = query.Where(BuildTradePredicate(selectedTrades)); var selectedLocations = locations? .Where(value => !string.IsNullOrWhiteSpace(value)) .Select(value => value.Trim().ToLower()) .Distinct() .ToArray() ?? Array.Empty(); if (selectedLocations.Length > 0) query = query.Where(BuildLocationPredicate(selectedLocations)); var result = ProjectWithTotalJobs(query); var selectedJobBuckets = jobBuckets? .Where(value => !string.IsNullOrWhiteSpace(value)) .Select(value => value.Trim().ToLower()) .Distinct() .ToArray() ?? Array.Empty(); if (selectedJobBuckets.Length == 0) return result; var under50 = selectedJobBuckets.Contains("under-50"); var from50To99 = selectedJobBuckets.Contains("50-99"); var from100To149 = selectedJobBuckets.Contains("100-149"); var from150 = selectedJobBuckets.Contains("150-plus"); return result.Where(v => (under50 && v.TotalJobs < 50) || (from50To99 && v.TotalJobs >= 50 && v.TotalJobs <= 99) || (from100To149 && v.TotalJobs >= 100 && v.TotalJobs <= 149) || (from150 && v.TotalJobs >= 150)); } private IQueryable ProjectWithTotalJobs(IQueryable query) => query.Select(v => new Vendor { Id = v.Id, CompanyName = v.CompanyName, ContactName = v.ContactName, Email = v.Email, Phone = v.Phone, CompanyPhone = v.CompanyPhone, Address = v.Address, City = v.City, State = v.State, Zip = v.Zip, TradeSpecialties = v.TradeSpecialties, GoogleMapsUrl = v.GoogleMapsUrl, Notes = v.Notes, IsActive = v.IsActive, IsDeleted = v.IsDeleted, TotalJobs = _context.Dispatches .Where(dispatch => dispatch.VendorId == v.Id && dispatch.WorkOrderId.HasValue) .Select(dispatch => dispatch.WorkOrderId!.Value) .Union( _context.DispatchWorkOrders .Where(link => link.Dispatch != null && link.Dispatch.VendorId == v.Id) .Select(link => link.WorkOrderId)) .Count() }); private static readonly MethodInfo StringConcatMethod = typeof(string).GetMethod(nameof(string.Concat), new[] { typeof(string), typeof(string) })!; private static Expression> BuildTradePredicate( IReadOnlyCollection selectedTrades) { var vendor = Expression.Parameter(typeof(Vendor), "vendor"); var specialties = Expression.Coalesce( Expression.Property(vendor, nameof(Vendor.TradeSpecialties)), Expression.Constant(string.Empty)); var lowered = Expression.Call(specialties, nameof(string.ToLower), Type.EmptyTypes); var delimiters = new[] { ", ", "," }; Expression? body = null; foreach (var trade in selectedTrades) { var token = Expression.Constant(trade); body = OrElse(body, Expression.Equal(lowered, token)); foreach (var delimiter in delimiters) { var delim = Expression.Constant(delimiter); var tokenWithTrailing = Concat(token, delim); var leadingToken = Concat(delim, token); var wrappedToken = Concat(leadingToken, delim); body = OrElse(body, StartsWith(lowered, tokenWithTrailing)); body = OrElse(body, EndsWith(lowered, leadingToken)); body = OrElse(body, Contains(lowered, wrappedToken)); } } return Expression.Lambda>(body ?? Expression.Constant(false), vendor); } private static Expression> BuildLocationPredicate( IReadOnlyCollection selectedLocations) { var vendor = Expression.Parameter(typeof(Vendor), "vendor"); var label = BuildLocationLabelBody(vendor); var lowered = Expression.Call(label, nameof(string.ToLower), Type.EmptyTypes); Expression? body = null; foreach (var location in selectedLocations) body = OrElse(body, Expression.Equal(lowered, Expression.Constant(location))); return Expression.Lambda>(body ?? Expression.Constant(false), vendor); } private static Expression BuildLocationLabelBody(ParameterExpression vendor) { var city = Expression.Property(vendor, nameof(Vendor.City)); var state = Expression.Property(vendor, nameof(Vendor.State)); var cityBlank = IsBlank(city); var stateBlank = IsBlank(state); var cityCommaState = Concat(Concat(city, Expression.Constant(", ")), state); var onlyState = Expression.Condition(stateBlank, Expression.Constant(string.Empty), state); var cityOrBoth = Expression.Condition(stateBlank, city, cityCommaState); return Expression.Condition(cityBlank, onlyState, cityOrBoth); } private static Expression IsBlank(Expression value) { var isNull = Expression.Equal(value, Expression.Constant(null, value.Type)); var trimmed = Expression.Call(value, nameof(string.Trim), Type.EmptyTypes); var isEmpty = Expression.Equal(trimmed, Expression.Constant(string.Empty)); return Expression.Condition(isNull, Expression.Constant(true), isEmpty); } private static Expression Concat(Expression left, Expression right) => Expression.Call(StringConcatMethod, left, right); private static Expression StartsWith(Expression instance, Expression value) => Expression.Call(instance, nameof(string.StartsWith), Type.EmptyTypes, value); private static Expression EndsWith(Expression instance, Expression value) => Expression.Call(instance, nameof(string.EndsWith), Type.EmptyTypes, value); private static Expression Contains(Expression instance, Expression value) => Expression.Call(instance, nameof(string.Contains), Type.EmptyTypes, value); private static Expression OrElse(Expression? accumulated, Expression next) => accumulated is null ? next : Expression.OrElse(accumulated, next); public async Task AddAsync(Vendor vendor) { await _context.Vendors.AddAsync(vendor); await _context.SaveChangesAsync(); return vendor; } public async Task UpdateAsync(Vendor vendor) { _context.Vendors.Update(vendor); await _context.SaveChangesAsync(); } public async Task DeleteAsync(int id) { var entity = await GetByIdAsync(id); if (entity != null) { _context.Vendors.Remove(entity); await _context.SaveChangesAsync(); } } public async Task ExistsAsync(int id) { return await _context.Vendors.AnyAsync(v => v.Id == id); } public async Task GetActiveTokenAsync(int vendorId) { return await _context.VendorAccessTokens .FirstOrDefaultAsync(t => t.VendorId == vendorId); } public async Task GetTokenByStringAsync(string token) { return await _context.VendorAccessTokens .FirstOrDefaultAsync(t => t.Token == token); } public async Task CountAsync() { return await _context.Vendors.CountAsync(); } } }