mirror of
https://github.com/Sea-Haven-Industries/shoc-backend.git
synced 2026-10-07 02:22:11 +00:00
fix(vendors): resolve list location from company address when city/state blank
The Vendors directory renders Location from City/State only, but the roster form keeps its City/State inputs hidden, so vendors captured through the UI store their location in the free-text Address field and the column falls back to an em dash while the detail drawer shows the value. Resolve location through a single rule -- structured City/State when present, otherwise the free-text Address -- and apply it consistently so the column, search, the location facet and the location filter cannot disagree. - Directory rows fall back to the linked company's Address when the technician has none, matching the Email precedence already established for these rows. - BuildLocationLabelBody, which backs both the filter and the facet label, falls back to Address when City/State are blank. - Directory search matches Address so a row is findable by what it displays. - The locations facet offers address-only companies as filter options.
This commit is contained in:
parent
8e749047a3
commit
8086b6c85f
4 changed files with 183 additions and 4 deletions
|
|
@ -472,6 +472,127 @@ public class VendorDataServiceTests
|
||||||
locationRow.State.Should().BeNull();
|
locationRow.State.Should().BeNull();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetDirectoryPagedAsync_UsesCompanyAddressWhenTechnicianAddressBlank()
|
||||||
|
{
|
||||||
|
await using var context = NewContext();
|
||||||
|
var company = new VendorCompany
|
||||||
|
{
|
||||||
|
Name = "Address Co",
|
||||||
|
NormalizedName = "address co",
|
||||||
|
Address = "Santa Monica, CA"
|
||||||
|
};
|
||||||
|
context.VendorCompanies.Add(company);
|
||||||
|
context.Vendors.AddRange(
|
||||||
|
new Vendor
|
||||||
|
{
|
||||||
|
CompanyName = "Address Co",
|
||||||
|
ContactName = "Blank Address",
|
||||||
|
Address = " ",
|
||||||
|
IsActive = true,
|
||||||
|
IsDeleted = false,
|
||||||
|
Company = company
|
||||||
|
},
|
||||||
|
new Vendor
|
||||||
|
{
|
||||||
|
CompanyName = "Address Co",
|
||||||
|
ContactName = "Own Address",
|
||||||
|
Address = "1 Technician Way",
|
||||||
|
IsActive = true,
|
||||||
|
IsDeleted = false,
|
||||||
|
Company = company
|
||||||
|
},
|
||||||
|
new Vendor
|
||||||
|
{
|
||||||
|
CompanyName = "Unlinked Co",
|
||||||
|
ContactName = "Unlinked",
|
||||||
|
Address = "9 Legacy Road",
|
||||||
|
IsActive = true,
|
||||||
|
IsDeleted = false,
|
||||||
|
CompanyId = null
|
||||||
|
});
|
||||||
|
await context.SaveChangesAsync();
|
||||||
|
|
||||||
|
var service = new VendorDataService(context);
|
||||||
|
var (items, _) = await service.GetDirectoryPagedAsync(1, 50, isActive: true);
|
||||||
|
|
||||||
|
items.Single(i => i.ContactName == "Blank Address").Address.Should().Be("Santa Monica, CA");
|
||||||
|
items.Single(i => i.ContactName == "Own Address").Address.Should().Be("1 Technician Way");
|
||||||
|
items.Single(i => i.ContactName == "Unlinked").Address.Should().Be("9 Legacy Road");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetDirectoryPagedAsync_SearchAndLocationFilterFallBackToAddressWhenCityStateBlank()
|
||||||
|
{
|
||||||
|
await using var context = NewContext();
|
||||||
|
var company = new VendorCompany
|
||||||
|
{
|
||||||
|
Name = "Vinewood LLC",
|
||||||
|
NormalizedName = "vinewood llc",
|
||||||
|
Address = "Santa Monica, CA"
|
||||||
|
};
|
||||||
|
context.VendorCompanies.Add(company);
|
||||||
|
context.Vendors.Add(new Vendor
|
||||||
|
{
|
||||||
|
CompanyName = "Vinewood LLC",
|
||||||
|
ContactName = "Trevor",
|
||||||
|
IsActive = true,
|
||||||
|
IsDeleted = false,
|
||||||
|
Company = company
|
||||||
|
});
|
||||||
|
await context.SaveChangesAsync();
|
||||||
|
|
||||||
|
var service = new VendorDataService(context);
|
||||||
|
var search = await service.GetDirectoryPagedAsync(1, 50, search: "santa monica", isActive: true);
|
||||||
|
var location = await service.GetDirectoryPagedAsync(
|
||||||
|
1,
|
||||||
|
50,
|
||||||
|
isActive: true,
|
||||||
|
locations: new[] { "Santa Monica, CA" });
|
||||||
|
|
||||||
|
search.Items.Should().ContainSingle(i => i.ContactName == "Trevor");
|
||||||
|
location.Items.Should().ContainSingle(i => i.ContactName == "Trevor");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetDirectoryPagedAsync_StructuredCityStateStillWinsOverAddress()
|
||||||
|
{
|
||||||
|
await using var context = NewContext();
|
||||||
|
var company = new VendorCompany
|
||||||
|
{
|
||||||
|
Name = "Both Co",
|
||||||
|
NormalizedName = "both co",
|
||||||
|
Address = "Santa Monica, CA",
|
||||||
|
City = "Chicago",
|
||||||
|
State = "IL"
|
||||||
|
};
|
||||||
|
context.VendorCompanies.Add(company);
|
||||||
|
context.Vendors.Add(new Vendor
|
||||||
|
{
|
||||||
|
CompanyName = "Both Co",
|
||||||
|
ContactName = "Sam",
|
||||||
|
IsActive = true,
|
||||||
|
IsDeleted = false,
|
||||||
|
Company = company
|
||||||
|
});
|
||||||
|
await context.SaveChangesAsync();
|
||||||
|
|
||||||
|
var service = new VendorDataService(context);
|
||||||
|
var byStructured = await service.GetDirectoryPagedAsync(
|
||||||
|
1,
|
||||||
|
50,
|
||||||
|
isActive: true,
|
||||||
|
locations: new[] { "Chicago, IL" });
|
||||||
|
var byAddress = await service.GetDirectoryPagedAsync(
|
||||||
|
1,
|
||||||
|
50,
|
||||||
|
isActive: true,
|
||||||
|
locations: new[] { "Santa Monica, CA" });
|
||||||
|
|
||||||
|
byStructured.Items.Should().ContainSingle(i => i.ContactName == "Sam");
|
||||||
|
byAddress.Items.Should().BeEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
[InlineData("trade")]
|
[InlineData("trade")]
|
||||||
[InlineData("location")]
|
[InlineData("location")]
|
||||||
|
|
|
||||||
|
|
@ -682,6 +682,45 @@ public class VendorServiceTests
|
||||||
facets.Trades.Should().BeEmpty();
|
facets.Trades.Should().BeEmpty();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetFacetsAsync_LocationFallsBackToAddressWhenCityAndStateAreBlank()
|
||||||
|
{
|
||||||
|
var data = new Mock<IVendorDataService>();
|
||||||
|
data.Setup(x => x.GetCompaniesForFacetsAsync(true, It.IsAny<CancellationToken>()))
|
||||||
|
.ReturnsAsync(new List<VendorCompany>
|
||||||
|
{
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
Id = 1,
|
||||||
|
Name = "Vinewood LLC",
|
||||||
|
Address = " Santa Monica, CA ",
|
||||||
|
City = null,
|
||||||
|
State = " "
|
||||||
|
},
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
Id = 2,
|
||||||
|
Name = "Structured Co",
|
||||||
|
Address = "Ignored Addr",
|
||||||
|
City = "Chicago",
|
||||||
|
State = "IL"
|
||||||
|
},
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
Id = 3,
|
||||||
|
Name = "No Location Co"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
data.Setup(x => x.GetActiveTradesAsync(It.IsAny<CancellationToken>()))
|
||||||
|
.ReturnsAsync(new List<Trade>());
|
||||||
|
|
||||||
|
var facets = await NewService(data).GetFacetsAsync(true, CancellationToken.None);
|
||||||
|
|
||||||
|
var labels = facets.Locations.Select(l => l.Label).ToList();
|
||||||
|
labels.Should().BeEquivalentTo(new[] { "Chicago, IL", "Santa Monica, CA" });
|
||||||
|
labels.Should().NotContain("Ignored Addr");
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task GetFacetsAsync_CompaniesAreUniqueAndOrderedCaseInsensitively()
|
public async Task GetFacetsAsync_CompaniesAreUniqueAndOrderedCaseInsensitively()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -199,7 +199,11 @@ namespace SeaHaven.DataServices.Implementation
|
||||||
Phone = v.Phone,
|
Phone = v.Phone,
|
||||||
CompanyPhone = v.CompanyPhone,
|
CompanyPhone = v.CompanyPhone,
|
||||||
PreferredContact = v.PreferredContact,
|
PreferredContact = v.PreferredContact,
|
||||||
Address = v.Address,
|
Address = v.Address != null && v.Address.Trim() != ""
|
||||||
|
? v.Address
|
||||||
|
: v.Company != null
|
||||||
|
? v.Company.Address
|
||||||
|
: v.Address,
|
||||||
City = v.Company != null &&
|
City = v.Company != null &&
|
||||||
((v.Company.City != null && v.Company.City.Trim() != "") ||
|
((v.Company.City != null && v.Company.City.Trim() != "") ||
|
||||||
(v.Company.State != null && v.Company.State.Trim() != ""))
|
(v.Company.State != null && v.Company.State.Trim() != ""))
|
||||||
|
|
@ -291,6 +295,7 @@ namespace SeaHaven.DataServices.Implementation
|
||||||
(row.TradeSpecialties ?? "").ToLower().Contains(normalizedSearch) ||
|
(row.TradeSpecialties ?? "").ToLower().Contains(normalizedSearch) ||
|
||||||
(row.City ?? "").ToLower().Contains(normalizedSearch) ||
|
(row.City ?? "").ToLower().Contains(normalizedSearch) ||
|
||||||
(row.State ?? "").ToLower().Contains(normalizedSearch) ||
|
(row.State ?? "").ToLower().Contains(normalizedSearch) ||
|
||||||
|
(row.Address ?? "").ToLower().Contains(normalizedSearch) ||
|
||||||
(row.Zip ?? "").ToLower().Contains(normalizedSearch));
|
(row.Zip ?? "").ToLower().Contains(normalizedSearch));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -507,7 +512,14 @@ namespace SeaHaven.DataServices.Implementation
|
||||||
|
|
||||||
var onlyState = Expression.Condition(stateBlank, Expression.Constant(string.Empty), state);
|
var onlyState = Expression.Condition(stateBlank, Expression.Constant(string.Empty), state);
|
||||||
var cityOrBoth = Expression.Condition(stateBlank, city, cityCommaState);
|
var cityOrBoth = Expression.Condition(stateBlank, city, cityCommaState);
|
||||||
return Expression.Condition(cityBlank, onlyState, cityOrBoth);
|
var structured = Expression.Condition(cityBlank, onlyState, cityOrBoth);
|
||||||
|
|
||||||
|
// Vendors captured through the roster form carry their location in the
|
||||||
|
// free-text Address field, so fall back to it when City/State are blank.
|
||||||
|
// Display, search, facet and filter all resolve location through here.
|
||||||
|
var address = Expression.Property(vendor, nameof(Vendor.Address));
|
||||||
|
var addressOrEmpty = Expression.Coalesce(address, Expression.Constant(string.Empty));
|
||||||
|
return Expression.Condition(IsBlank(structured), addressOrEmpty, structured);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Expression IsBlank(Expression value)
|
private static Expression IsBlank(Expression value)
|
||||||
|
|
|
||||||
|
|
@ -681,13 +681,20 @@ namespace SeaHaven.Services.Implementation
|
||||||
|
|
||||||
var trades = canonicalTrades.Select(t => t.Name).ToList();
|
var trades = canonicalTrades.Select(t => t.Name).ToList();
|
||||||
|
|
||||||
|
// Location resolves to structured City/State when present and otherwise to the
|
||||||
|
// free-text Address, matching the directory column and the location filter, so
|
||||||
|
// a company only reachable by Address is still offered as a filter option.
|
||||||
var locations = companies
|
var locations = companies
|
||||||
.Where(c => !string.IsNullOrWhiteSpace(c.City) || !string.IsNullOrWhiteSpace(c.State))
|
.Where(c => !string.IsNullOrWhiteSpace(c.City)
|
||||||
|
|| !string.IsNullOrWhiteSpace(c.State)
|
||||||
|
|| !string.IsNullOrWhiteSpace(c.Address))
|
||||||
.Select(c => new VendorFacetLocationDTO
|
.Select(c => new VendorFacetLocationDTO
|
||||||
{
|
{
|
||||||
City = c.City,
|
City = c.City,
|
||||||
State = c.State,
|
State = c.State,
|
||||||
Label = string.Join(", ", new[] { c.City, c.State }.Where(value => !string.IsNullOrWhiteSpace(value)))
|
Label = !string.IsNullOrWhiteSpace(c.City) || !string.IsNullOrWhiteSpace(c.State)
|
||||||
|
? string.Join(", ", new[] { c.City, c.State }.Where(value => !string.IsNullOrWhiteSpace(value)))
|
||||||
|
: c.Address!.Trim()
|
||||||
})
|
})
|
||||||
.DistinctBy(location => location.Label, StringComparer.OrdinalIgnoreCase)
|
.DistinctBy(location => location.Label, StringComparer.OrdinalIgnoreCase)
|
||||||
.OrderBy(location => location.Label);
|
.OrderBy(location => location.Label);
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue