diff --git a/PepApi.Core/Controllers/AnalyticsController.cs b/PepApi.Core/Controllers/AnalyticsController.cs
index 08441a5..49ec842 100644
--- a/PepApi.Core/Controllers/AnalyticsController.cs
+++ b/PepApi.Core/Controllers/AnalyticsController.cs
@@ -14,6 +14,9 @@ public class AnalyticsController : ControllerBase
// Status codes for "has been cut"
private static readonly int[] CutStatuses = [2, 5]; // "Has been cut", "Quote, accepted, has been cut"
+ // Status codes for "to be cut"
+ private static readonly int[] ToBeCutStatuses = [0, 4]; // "To be cut", "Quote, accepted, to be cut"
+
public AnalyticsController(PepDB db)
{
_db = db;
@@ -525,4 +528,133 @@ public class AnalyticsController : ControllerBase
return Ok(topMaterials);
}
+
+ ///
+ /// Get materials to order for programs with "To Be Cut" status.
+ ///
+ [HttpGet("materials-to-order")]
+ public async Task> GetMaterialsToOrder(
+ [FromQuery] string? customerId = null,
+ [FromQuery] int? year = null)
+ {
+ var targetYear = year ?? DateTime.Now.Year;
+
+ var nestsQuery = _db.NestHeaders
+ .Where(n => ToBeCutStatuses.Contains(n.Status))
+ .Where(n => n.DateProgrammed != null && n.DateProgrammed.Value.Year == targetYear);
+
+ if (!string.IsNullOrWhiteSpace(customerId))
+ nestsQuery = nestsQuery.Where(n => n.CustomerName == customerId || n.CustID == customerId);
+
+ var nestData = await nestsQuery
+ .Select(n => new
+ {
+ n.NestName,
+ n.CopyID,
+ n.Material,
+ n.MatGrade,
+ n.MatThick
+ })
+ .ToListAsync();
+
+ if (!nestData.Any())
+ {
+ return Ok(new MaterialsToOrderResponse
+ {
+ TotalPrograms = 0,
+ TotalPlates = 0,
+ TotalWeight = 0,
+ Materials = []
+ });
+ }
+
+ var nestKeys = nestData.Select(n => (n.NestName, n.CopyID)).ToHashSet();
+
+ var plateData = await _db.PlateHeaders
+ .Where(p => p.DupNo == 1) // Only count first duplicate entry to avoid double-counting
+ .Select(p => new
+ {
+ p.NestName,
+ p.CopyID,
+ p.PlateSize,
+ p.PlateWeight,
+ p.PlateDuplicates
+ })
+ .ToListAsync();
+
+ // Filter plates to only those belonging to our filtered nests
+ var filteredPlates = plateData
+ .Where(p => nestKeys.Contains((p.NestName, p.CopyID)))
+ .ToList();
+
+ // Create lookup: nest -> material info
+ var nestMaterialLookup = nestData.ToDictionary(
+ n => (n.NestName, n.CopyID),
+ n => (n.Material, n.MatGrade, n.MatThick, n.NestName));
+
+ // Group plates by material/grade/thickness
+ var materialGroups = filteredPlates
+ .Select(p => new
+ {
+ p.PlateSize,
+ p.PlateWeight,
+ Qty = p.PlateDuplicates ?? 1,
+ Material = nestMaterialLookup.TryGetValue((p.NestName, p.CopyID), out var m) ? m : default
+ })
+ .Where(p => p.Material != default)
+ .GroupBy(p => (p.Material.Material, p.Material.MatGrade, p.Material.MatThick))
+ .Select(g => new MaterialToOrder
+ {
+ MaterialNumber = int.TryParse(g.Key.Material, out var num) ? num : 0,
+ MaterialGrade = g.Key.MatGrade ?? "",
+ Thickness = g.Key.MatThick,
+ TotalPlates = g.Sum(x => x.Qty),
+ TotalWeight = g.Sum(x => (x.PlateWeight ?? 0) * x.Qty),
+ Programs = g.Select(x => x.Material.NestName).Distinct().OrderBy(n => n).ToList(),
+ Plates = g
+ .GroupBy(x => x.PlateSize)
+ .Select(sg => new PlateRequirement
+ {
+ Width = ParsePlateSize(sg.Key).Width,
+ Length = ParsePlateSize(sg.Key).Length,
+ Quantity = sg.Sum(x => x.Qty)
+ })
+ .OrderBy(pr => pr.Width)
+ .ThenBy(pr => pr.Length)
+ .ToList()
+ })
+ .OrderBy(m => m.MaterialNumber)
+ .ThenBy(m => m.MaterialGrade)
+ .ThenBy(m => m.Thickness)
+ .ToList();
+
+ var response = new MaterialsToOrderResponse
+ {
+ TotalPrograms = nestData.Select(n => n.NestName).Distinct().Count(),
+ TotalPlates = materialGroups.Sum(m => m.TotalPlates),
+ TotalWeight = materialGroups.Sum(m => m.TotalWeight),
+ Materials = materialGroups
+ };
+
+ return Ok(response);
+ }
+
+ ///
+ /// Parse plate size string (e.g., "48 X 120") into width and length.
+ ///
+ private static (double Width, double Length) ParsePlateSize(string? plateSize)
+ {
+ if (string.IsNullOrWhiteSpace(plateSize))
+ return (0, 0);
+
+ // Split on "X" or "x" with optional surrounding spaces
+ var parts = plateSize.Split(['x', 'X'], StringSplitOptions.TrimEntries);
+ if (parts.Length != 2)
+ return (0, 0);
+
+ double.TryParse(parts[0], out var width);
+ double.TryParse(parts[1], out var length);
+
+ return (width, length);
+ }
}
diff --git a/PepApi.Core/Models/MaterialUsageSummary.cs b/PepApi.Core/Models/MaterialUsageSummary.cs
index 1581d8e..68e3688 100644
--- a/PepApi.Core/Models/MaterialUsageSummary.cs
+++ b/PepApi.Core/Models/MaterialUsageSummary.cs
@@ -59,3 +59,29 @@ public class StockRecommendation
public double SuggestedStockWeight { get; set; }
public int SuggestedStockPlates { get; set; }
}
+
+public class PlateRequirement
+{
+ public double Width { get; set; }
+ public double Length { get; set; }
+ public int Quantity { get; set; }
+}
+
+public class MaterialToOrder
+{
+ public int MaterialNumber { get; set; }
+ public required string MaterialGrade { get; set; }
+ public double Thickness { get; set; }
+ public int TotalPlates { get; set; }
+ public double TotalWeight { get; set; }
+ public required List Programs { get; set; }
+ public required List Plates { get; set; }
+}
+
+public class MaterialsToOrderResponse
+{
+ public int TotalPrograms { get; set; }
+ public int TotalPlates { get; set; }
+ public double TotalWeight { get; set; }
+ public required List Materials { get; set; }
+}