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; } +}