diff --git a/PepApi.Core/Controllers/NestsController.cs b/PepApi.Core/Controllers/NestsController.cs
index c88f5ba..9af86f5 100644
--- a/PepApi.Core/Controllers/NestsController.cs
+++ b/PepApi.Core/Controllers/NestsController.cs
@@ -180,6 +180,123 @@ public class NestsController : ControllerBase
return GetNestPath(nestName, year);
}
+ ///
+ /// Search for parts across nests by part name.
+ /// If year is not specified, searches across all years.
+ ///
+ [HttpGet("parts/search")]
+ public async Task> SearchParts(
+ [FromQuery] string search,
+ [FromQuery] int? year = null,
+ [FromQuery] string? customer = null,
+ [FromQuery] int limit = 100)
+ {
+ if (string.IsNullOrWhiteSpace(search))
+ return BadRequest(new { message = "Search term is required" });
+
+ var searchUpper = search.Trim().ToUpper();
+
+ // Get nest headers - filter by year only if specified
+ var nestsQuery = _db.NestHeaders
+ .Where(n => n.DateProgrammed != null);
+
+ if (year.HasValue)
+ nestsQuery = nestsQuery.Where(n => n.DateProgrammed!.Value.Year == year.Value);
+
+ if (!string.IsNullOrWhiteSpace(customer))
+ nestsQuery = nestsQuery.Where(n => n.CustomerName == customer || n.CustID == customer);
+
+ var nests = await nestsQuery
+ .Select(n => new
+ {
+ n.NestName,
+ n.CopyID,
+ n.CustomerName,
+ n.Comments,
+ n.Material,
+ n.MatGrade,
+ n.MatThick,
+ n.Status,
+ n.DateProgrammed,
+ n.ModifiedDate,
+ n.Application
+ })
+ .ToListAsync();
+
+ if (!nests.Any())
+ {
+ return Ok(new PartSearchResponse
+ {
+ SearchTerm = search,
+ Year = year,
+ TotalMatches = 0,
+ TotalNests = 0,
+ Results = []
+ });
+ }
+
+ var nestKeys = nests.Select(n => (n.NestName, n.CopyID)).ToHashSet();
+
+ // Search for parts matching the search term
+ var plateDetails = await _db.PlateDetails
+ .Where(p => p.Drawing != null && p.Drawing.ToUpper().Contains(searchUpper))
+ .Select(p => new
+ {
+ p.NestName,
+ p.CopyID,
+ p.Drawing,
+ p.QtyNstd,
+ p.QtyReq
+ })
+ .ToListAsync();
+
+ // Filter to only parts in our target nests
+ var matchingParts = plateDetails
+ .Where(p => nestKeys.Contains((p.NestName, p.CopyID)))
+ .ToList();
+
+ // Group by nest and part name
+ var nestLookup = nests.ToDictionary(n => (n.NestName, n.CopyID));
+
+ var allResults = matchingParts
+ .GroupBy(p => (p.NestName, p.CopyID, p.Drawing))
+ .Select(g =>
+ {
+ var nest = nestLookup[(g.Key.NestName, g.Key.CopyID)];
+ return new PartSearchResult
+ {
+ PartName = g.Key.Drawing ?? "",
+ NestName = g.Key.NestName,
+ Status = PepHelper.GetStatus(nest.Status),
+ Customer = nest.CustomerName ?? "",
+ Comments = nest.Comments ?? "",
+ MaterialNumber = int.TryParse(nest.Material, out var num) ? num : 0,
+ MaterialGrade = nest.MatGrade ?? "",
+ Thickness = nest.MatThick,
+ DateProgrammed = nest.DateProgrammed!.Value,
+ DateLastModified = nest.ModifiedDate ?? nest.DateProgrammed!.Value,
+ QtyNested = g.Sum(x => x.QtyNstd ?? 0),
+ QtyRequired = g.Max(x => x.QtyReq ?? 0),
+ Application = PepHelper.GetApplication(nest.Application)
+ };
+ })
+ .OrderByDescending(r => r.DateProgrammed)
+ .ThenBy(r => r.PartName)
+ .ToList();
+
+ var limitedResults = limit > 0 ? allResults.Take(limit).ToList() : allResults;
+
+ return Ok(new PartSearchResponse
+ {
+ SearchTerm = search,
+ Year = year,
+ TotalMatches = allResults.Count,
+ TotalNests = allResults.Select(r => r.NestName).Distinct().Count(),
+ ResultsReturned = limitedResults.Count,
+ Results = limitedResults
+ });
+ }
+
private string GetNestPath(string nestName, int year)
{
var fileName = nestName + ".zip";
diff --git a/PepApi.Core/Models/PartSearchResult.cs b/PepApi.Core/Models/PartSearchResult.cs
new file mode 100644
index 0000000..793787d
--- /dev/null
+++ b/PepApi.Core/Models/PartSearchResult.cs
@@ -0,0 +1,28 @@
+namespace PepApi.Core.Models;
+
+public class PartSearchResult
+{
+ public required string PartName { get; set; }
+ public required string NestName { get; set; }
+ public required string Status { get; set; }
+ public required string Customer { get; set; }
+ public required string Comments { get; set; }
+ public int MaterialNumber { get; set; }
+ public required string MaterialGrade { get; set; }
+ public double Thickness { get; set; }
+ public DateTime DateProgrammed { get; set; }
+ public DateTime DateLastModified { get; set; }
+ public int QtyNested { get; set; }
+ public int QtyRequired { get; set; }
+ public required string Application { get; set; }
+}
+
+public class PartSearchResponse
+{
+ public required string SearchTerm { get; set; }
+ public int? Year { get; set; }
+ public int TotalMatches { get; set; }
+ public int TotalNests { get; set; }
+ public int ResultsReturned { get; set; }
+ public List Results { get; set; } = [];
+}