From 4d2dd6d985020ef7b4793937ddb4a0a587e9dd78 Mon Sep 17 00:00:00 2001 From: AJ Isaacs Date: Sat, 29 Nov 2025 19:35:27 -0500 Subject: [PATCH] feat(nests): add part search endpoint MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add GET /api/nests/parts/search endpoint to search for parts across nests by part name. Supports filtering by year and customer, with configurable result limit. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- PepApi.Core/Controllers/NestsController.cs | 117 +++++++++++++++++++++ PepApi.Core/Models/PartSearchResult.cs | 28 +++++ 2 files changed, 145 insertions(+) create mode 100644 PepApi.Core/Models/PartSearchResult.cs 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; } = []; +}