feat(nests): add batch part search and material descriptions

- Add POST parts/search/batch endpoint for searching multiple parts at once
- Extract SearchPartsInternalAsync helper method to support batch operations
- Include material description in part search results by looking up MaterialHeaders
- Add BatchPartSearchRequest and BatchPartSearchResponse models
- Add ResultsReturned field to single-part search response for consistency

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2025-12-18 06:56:00 -05:00
parent 5c74cb2b4e
commit a1df56f64c
2 changed files with 88 additions and 4 deletions

View File

@@ -160,6 +160,52 @@ public class NestsController : ControllerBase
if (string.IsNullOrWhiteSpace(search)) if (string.IsNullOrWhiteSpace(search))
return BadRequest(new { message = "Search term is required" }); return BadRequest(new { message = "Search term is required" });
var result = await SearchPartsInternalAsync(search, year, customer, limit);
return Ok(result);
}
/// <summary>
/// Batch search for parts across nests by multiple part names.
/// Returns results grouped by search term.
/// </summary>
[HttpPost("parts/search/batch")]
public async Task<ActionResult<BatchPartSearchResponse>> SearchPartsBatch(
[FromBody] BatchPartSearchRequest request)
{
if (request.SearchTerms == null || request.SearchTerms.Count == 0)
return BadRequest(new { message = "At least one search term is required" });
var results = new List<PartSearchResponse>();
foreach (var searchTerm in request.SearchTerms.Distinct())
{
if (string.IsNullOrWhiteSpace(searchTerm))
continue;
var result = await SearchPartsInternalAsync(
searchTerm,
request.Year,
request.Customer,
request.LimitPerTerm);
results.Add(result);
}
return Ok(new BatchPartSearchResponse
{
TotalSearchTerms = results.Count,
TotalMatches = results.Sum(r => r.TotalMatches),
TotalNests = results.SelectMany(r => r.Results).Select(r => r.NestName).Distinct().Count(),
Results = results
});
}
private async Task<PartSearchResponse> SearchPartsInternalAsync(
string search,
int? year,
string? customer,
int limit)
{
var searchUpper = search.Trim().ToUpper(); var searchUpper = search.Trim().ToUpper();
// Get nest headers - filter by year only if specified // Get nest headers - filter by year only if specified
@@ -191,14 +237,15 @@ public class NestsController : ControllerBase
if (!nests.Any()) if (!nests.Any())
{ {
return Ok(new PartSearchResponse return new PartSearchResponse
{ {
SearchTerm = search, SearchTerm = search,
Year = year, Year = year,
TotalMatches = 0, TotalMatches = 0,
TotalNests = 0, TotalNests = 0,
ResultsReturned = 0,
Results = [] Results = []
}); };
} }
var nestKeys = nests.Select(n => (n.NestName, n.CopyID)).ToHashSet(); var nestKeys = nests.Select(n => (n.NestName, n.CopyID)).ToHashSet();
@@ -221,6 +268,22 @@ public class NestsController : ControllerBase
.Where(p => nestKeys.Contains((p.NestName, p.CopyID))) .Where(p => nestKeys.Contains((p.NestName, p.CopyID)))
.ToList(); .ToList();
// Get material descriptions for materials used in matching nests
var materialNumbers = nests
.Select(n => n.Material)
.Where(m => !string.IsNullOrWhiteSpace(m))
.Distinct()
.ToList();
var materialDescriptions = await _db.MaterialHeaders
.Where(m => materialNumbers.Contains(m.Material))
.Select(m => new { m.Material, m.Description })
.ToListAsync();
var materialDescriptionLookup = materialDescriptions
.GroupBy(m => m.Material)
.ToDictionary(g => g.Key, g => g.First().Description);
// Group by nest and part name // Group by nest and part name
var nestLookup = nests.ToDictionary(n => (n.NestName, n.CopyID)); var nestLookup = nests.ToDictionary(n => (n.NestName, n.CopyID));
@@ -229,6 +292,9 @@ public class NestsController : ControllerBase
.Select(g => .Select(g =>
{ {
var nest = nestLookup[(g.Key.NestName, g.Key.CopyID)]; var nest = nestLookup[(g.Key.NestName, g.Key.CopyID)];
var materialDesc = nest.Material != null && materialDescriptionLookup.TryGetValue(nest.Material, out var desc)
? desc
: "";
return new PartSearchResult return new PartSearchResult
{ {
PartName = g.Key.Drawing ?? "", PartName = g.Key.Drawing ?? "",
@@ -238,6 +304,7 @@ public class NestsController : ControllerBase
Comments = nest.Comments ?? "", Comments = nest.Comments ?? "",
MaterialNumber = int.TryParse(nest.Material, out var num) ? num : 0, MaterialNumber = int.TryParse(nest.Material, out var num) ? num : 0,
MaterialGrade = nest.MatGrade ?? "", MaterialGrade = nest.MatGrade ?? "",
MaterialDescription = materialDesc,
Thickness = nest.MatThick, Thickness = nest.MatThick,
DateProgrammed = nest.DateProgrammed!.Value, DateProgrammed = nest.DateProgrammed!.Value,
DateLastModified = nest.ModifiedDate ?? nest.DateProgrammed!.Value, DateLastModified = nest.ModifiedDate ?? nest.DateProgrammed!.Value,
@@ -252,7 +319,7 @@ public class NestsController : ControllerBase
var limitedResults = limit > 0 ? allResults.Take(limit).ToList() : allResults; var limitedResults = limit > 0 ? allResults.Take(limit).ToList() : allResults;
return Ok(new PartSearchResponse return new PartSearchResponse
{ {
SearchTerm = search, SearchTerm = search,
Year = year, Year = year,
@@ -260,7 +327,7 @@ public class NestsController : ControllerBase
TotalNests = allResults.Select(r => r.NestName).Distinct().Count(), TotalNests = allResults.Select(r => r.NestName).Distinct().Count(),
ResultsReturned = limitedResults.Count, ResultsReturned = limitedResults.Count,
Results = limitedResults Results = limitedResults
}); };
} }
private async Task<string?> GetNestPathAsync(string nestName, int? year = null) private async Task<string?> GetNestPathAsync(string nestName, int? year = null)

View File

@@ -9,6 +9,7 @@ public class PartSearchResult
public required string Comments { get; set; } public required string Comments { get; set; }
public int MaterialNumber { get; set; } public int MaterialNumber { get; set; }
public required string MaterialGrade { get; set; } public required string MaterialGrade { get; set; }
public required string MaterialDescription { get; set; }
public double Thickness { get; set; } public double Thickness { get; set; }
public DateTime DateProgrammed { get; set; } public DateTime DateProgrammed { get; set; }
public DateTime DateLastModified { get; set; } public DateTime DateLastModified { get; set; }
@@ -26,3 +27,19 @@ public class PartSearchResponse
public int ResultsReturned { get; set; } public int ResultsReturned { get; set; }
public List<PartSearchResult> Results { get; set; } = []; public List<PartSearchResult> Results { get; set; } = [];
} }
public class BatchPartSearchRequest
{
public required List<string> SearchTerms { get; set; }
public string? Customer { get; set; }
public int? Year { get; set; }
public int LimitPerTerm { get; set; } = 100;
}
public class BatchPartSearchResponse
{
public int TotalSearchTerms { get; set; }
public int TotalMatches { get; set; }
public int TotalNests { get; set; }
public List<PartSearchResponse> Results { get; set; } = [];
}