feat(analytics): add materials-to-order endpoint
Add GET /api/analytics/materials-to-order endpoint to retrieve materials needed for programs with "To Be Cut" status. Returns plate requirements grouped by material/grade/thickness with associated program names. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -14,6 +14,9 @@ public class AnalyticsController : ControllerBase
|
|||||||
// Status codes for "has been cut"
|
// Status codes for "has been cut"
|
||||||
private static readonly int[] CutStatuses = [2, 5]; // "Has been cut", "Quote, accepted, 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)
|
public AnalyticsController(PepDB db)
|
||||||
{
|
{
|
||||||
_db = db;
|
_db = db;
|
||||||
@@ -525,4 +528,133 @@ public class AnalyticsController : ControllerBase
|
|||||||
|
|
||||||
return Ok(topMaterials);
|
return Ok(topMaterials);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get materials to order for programs with "To Be Cut" status.
|
||||||
|
/// </summary>
|
||||||
|
[HttpGet("materials-to-order")]
|
||||||
|
public async Task<ActionResult<MaterialsToOrderResponse>> 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Parse plate size string (e.g., "48 X 120") into width and length.
|
||||||
|
/// </summary>
|
||||||
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -59,3 +59,29 @@ public class StockRecommendation
|
|||||||
public double SuggestedStockWeight { get; set; }
|
public double SuggestedStockWeight { get; set; }
|
||||||
public int SuggestedStockPlates { 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<string> Programs { get; set; }
|
||||||
|
public required List<PlateRequirement> Plates { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class MaterialsToOrderResponse
|
||||||
|
{
|
||||||
|
public int TotalPrograms { get; set; }
|
||||||
|
public int TotalPlates { get; set; }
|
||||||
|
public double TotalWeight { get; set; }
|
||||||
|
public required List<MaterialToOrder> Materials { get; set; }
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user