fix(workorders): search registry service labels

This commit is contained in:
Alexandre Brandizzi 2026-09-30 20:19:41 -03:00
parent 0cd0d3b066
commit 969b972581
2 changed files with 212 additions and 0 deletions

View file

@ -48,6 +48,8 @@ namespace SeaHaven.DataServices.Helpers
|| (w.AssignToUser != null && ((w.AssignToUser.FirstName ?? "") + " " + (w.AssignToUser.LastName ?? "")).ToLower().Contains(s))
|| (w.Trade ?? "").ToLower().Contains(s)
|| (w.Problem ?? "").ToLower().Contains(s)
|| (w.ServiceDefinition != null && (w.ServiceDefinition.Name ?? "").ToLower().Contains(s))
|| (w.ServiceNameSnapshot ?? "").ToLower().Contains(s)
|| (w.PrimaryDispatch != null && w.PrimaryDispatch.Vendor != null && (w.PrimaryDispatch.Vendor.CompanyName ?? "").ToLower().Contains(s))
|| (w.TechnicianAssigned != false && w.PrimaryDispatch != null && w.PrimaryDispatch.Vendor != null && (w.PrimaryDispatch.Vendor.ContactName ?? "").ToLower().Contains(s))
|| (w.Status ?? "").ToLower().Contains(s)

View file

@ -0,0 +1,210 @@
using Data.SeaHavenIndustries;
using Data.SeaHavenIndustries.Enums;
using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;
using SeaHaven.DataServices.Helpers;
using SeaHaven.DataServices.Implementation;
using SeaHaven.DataServices.Interfaces;
namespace SeaHavenIndustries.Tests;
public sealed class WorkOrderServiceRegistryKeywordSearchRelationalTests
{
[Fact]
public async Task Search_MatchesCurrentAndHistoricalServiceLabelsAndPreservesExistingAliases()
{
await using var connection = new SqliteConnection("Data Source=:memory:;Foreign Keys=True");
await connection.OpenAsync();
var options = new DbContextOptionsBuilder<ApplicationDbContext>()
.UseSqlite(connection)
.Options;
await using var context = new SqliteSearchTestDbContext(options);
await context.Database.EnsureCreatedAsync();
context.Services.AddRange(
new Service
{
Id = 10,
Name = "Chiller Maintenance Original",
NormalizedName = "CHILLER MAINTENANCE ORIGINAL",
Trade = "HVAC",
IconKey = "hvac",
IsActive = true
},
new Service
{
Id = 20,
Name = "Roof Maintenance Current",
NormalizedName = "ROOF MAINTENANCE CURRENT",
Trade = "Roofing",
IconKey = "roofing",
IsActive = true
},
new Service
{
Id = 30,
Name = "Pump Service Current",
NormalizedName = "PUMP SERVICE CURRENT",
Trade = "Plumbing",
IconKey = "plumbing",
IsActive = false
});
context.Vendors.Add(new Vendor
{
Id = 40,
CompanyName = "Acme Vendor Group",
ContactName = "Morgan Technician"
});
context.Dispatches.Add(new Dispatch { Id = 50, VendorId = 40, Status = "Sent" });
context.workOrders.AddRange(
new WorkOrder
{
Id = 1,
ServiceId = 10,
ServiceNameSnapshot = "Legacy Chiller Label",
Trade = "HVAC",
Problem = "Cooling noise",
LifecycleStatus = LifecycleStatus.Scheduled,
WorkOrderType = WorkOrderType.PM,
ScheduledDate = new DateTime(2026, 9, 15),
PrimaryDispatchId = 50,
TechnicianAssigned = true
},
new WorkOrder
{
Id = 2,
ServiceId = 20,
ServiceNameSnapshot = "Frozen Roof Label",
Trade = "Roofing",
Problem = "Roof leak",
LifecycleStatus = LifecycleStatus.Completed,
WorkOrderType = WorkOrderType.PM,
ScheduledDate = new DateTime(2026, 9, 15)
},
new WorkOrder
{
Id = 3,
ServiceId = 30,
ServiceNameSnapshot = "Historical Pump Label",
Trade = "Plumbing",
Problem = "Pump noise",
LifecycleStatus = LifecycleStatus.Scheduled,
WorkOrderType = WorkOrderType.PM,
ScheduledDate = new DateTime(2026, 9, 15)
},
new WorkOrder
{
Id = 4,
Trade = "Legacy Boiler Service",
Problem = "Oil leak",
LifecycleStatus = LifecycleStatus.Scheduled,
WorkOrderType = WorkOrderType.Reactive,
ScheduledDate = new DateTime(2026, 9, 15)
},
new WorkOrder
{
Id = 5,
Trade = "Unlinked vendor WO",
LifecycleStatus = LifecycleStatus.Scheduled,
WorkOrderType = WorkOrderType.Reactive,
ScheduledDate = new DateTime(2026, 9, 15),
PrimaryDispatchId = 50,
TechnicianAssigned = false
});
await context.SaveChangesAsync();
var renamedService = await context.Services.SingleAsync(service => service.Id == 10);
renamedService.Name = "Northstar Chiller Service";
renamedService.NormalizedName = "NORTHSTAR CHILLER SERVICE";
await context.SaveChangesAsync();
async Task<int[]> SearchIds(string search)
=> await WorkOrderBoardSearchFilter
.Apply(context.workOrders.AsNoTracking(), search)
.Select(workOrder => workOrder.Id)
.OrderBy(id => id)
.ToArrayAsync();
Assert.Equal(new[] { 1 }, await SearchIds("Northstar Chiller Service"));
Assert.Equal(new[] { 1 }, await SearchIds("Legacy Chiller Label"));
Assert.Equal(new[] { 2 }, await SearchIds("Frozen Roof Label"));
Assert.Equal(new[] { 2 }, await SearchIds("Roof Maintenance Current"));
Assert.Equal(new[] { 3 }, await SearchIds("Historical Pump Label"));
Assert.Equal(new[] { 3 }, await SearchIds("Pump Service Current"));
Assert.Equal(new[] { 4 }, await SearchIds("Legacy Boiler Service"));
Assert.Equal(new[] { 4 }, await SearchIds("Oil leak"));
Assert.Equal(new[] { 1, 5 }, await SearchIds("Acme Vendor Group"));
Assert.Equal(new[] { 1 }, await SearchIds("Morgan Technician"));
var advancedSearch = new WorkOrderAdvancedSearchDataService(context);
var matchingFacet = await advancedSearch.SearchAsync(CreateQuery(
search: "Northstar Chiller Service",
pmTypes: new[] { "Northstar Chiller Service" }));
Assert.Equal(new[] { 1 }, matchingFacet.Rows.Select(row => row.Id));
Assert.Equal("Northstar Chiller Service", Assert.Single(matchingFacet.Rows).Trade);
var conflictingFacet = await advancedSearch.SearchAsync(CreateQuery(
search: "Northstar Chiller Service",
pmTypes: new[] { "Roof Maintenance Current" }));
Assert.Empty(conflictingFacet.Rows);
Assert.Equal(0, conflictingFacet.TotalCount);
}
private static WorkOrderAdvancedSearchQuery CreateQuery(string search, IReadOnlyList<string>? pmTypes = null)
=> new(
Search: search,
DateFrom: new DateOnly(2026, 9, 1),
DateTo: new DateOnly(2026, 9, 30),
UnscheduledOnly: false,
Sites: null,
Regions: null,
Types: null,
Overdue: false,
Dispatchers: null,
Statuses: null,
PmTypes: pmTypes,
VendorIds: null,
DocStatuses: null,
Severities: null,
Rescheduled: false,
CarriedOver: false,
AddOn: false,
AvetaOnly: false,
FlagColors: null,
InternalOnly: false,
HasUplift: false,
UpliftStatuses: null,
MyWorkOrders: false,
CurrentUserId: null,
Page: 0,
PageSize: 100,
SortBy: "scheduledDate",
SortDir: "asc");
private sealed class SqliteSearchTestDbContext : ApplicationDbContext
{
public SqliteSearchTestDbContext(DbContextOptions<ApplicationDbContext> options)
: base(options)
{
}
protected override void OnModelCreating(ModelBuilder builder)
{
base.OnModelCreating(builder);
foreach (var index in builder.Model.GetEntityTypes().SelectMany(entity => entity.GetIndexes()))
{
if (index.GetFilter() != null)
index.SetFilter(null);
}
foreach (var entityType in new[] { typeof(WorkOrder), typeof(Dispatch) })
{
var property = builder.Entity(entityType).Property("RowVersion").Metadata;
property.ValueGenerated = Microsoft.EntityFrameworkCore.Metadata.ValueGenerated.Never;
property.IsConcurrencyToken = false;
}
}
}
}