feat(analytics): add material usage analytics endpoints
Add AnalyticsController with endpoints for tracking material consumption: - /analytics/material-usage - usage summary with optional monthly grouping - /analytics/plate-sizes - commonly used plate size analysis - /analytics/thickness-breakdown - consumption by material thickness - /analytics/customer-usage - material breakdown per customer - /analytics/stock-recommendations - stock level suggestions based on history - /analytics/top-materials - quick summary of most used materials 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
528
PepApi.Core/Controllers/AnalyticsController.cs
Normal file
528
PepApi.Core/Controllers/AnalyticsController.cs
Normal file
@@ -0,0 +1,528 @@
|
|||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using PepApi.Core.Models;
|
||||||
|
using PepLib.Data;
|
||||||
|
|
||||||
|
namespace PepApi.Core.Controllers;
|
||||||
|
|
||||||
|
[ApiController]
|
||||||
|
[Route("analytics")]
|
||||||
|
public class AnalyticsController : ControllerBase
|
||||||
|
{
|
||||||
|
private readonly PepDB _db;
|
||||||
|
|
||||||
|
// Status codes for "has been cut"
|
||||||
|
private static readonly int[] CutStatuses = [2, 5]; // "Has been cut", "Quote, accepted, has been cut"
|
||||||
|
|
||||||
|
public AnalyticsController(PepDB db)
|
||||||
|
{
|
||||||
|
_db = db;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get material usage summary for a date range, optionally grouped by month.
|
||||||
|
/// </summary>
|
||||||
|
[HttpGet("material-usage")]
|
||||||
|
public async Task<ActionResult<object>> GetMaterialUsage(
|
||||||
|
[FromQuery] DateTime? startDate = null,
|
||||||
|
[FromQuery] DateTime? endDate = null,
|
||||||
|
[FromQuery] string? groupBy = null,
|
||||||
|
[FromQuery] bool cutOnly = true)
|
||||||
|
{
|
||||||
|
var start = startDate ?? DateTime.Now.AddYears(-1);
|
||||||
|
var end = endDate ?? DateTime.Now;
|
||||||
|
|
||||||
|
var nestsQuery = _db.NestHeaders
|
||||||
|
.Where(n => n.DateProgrammed != null
|
||||||
|
&& n.DateProgrammed >= start
|
||||||
|
&& n.DateProgrammed <= end);
|
||||||
|
|
||||||
|
if (cutOnly)
|
||||||
|
nestsQuery = nestsQuery.Where(n => CutStatuses.Contains(n.Status));
|
||||||
|
|
||||||
|
if (groupBy?.ToLower() == "month")
|
||||||
|
{
|
||||||
|
var nestData = await nestsQuery
|
||||||
|
.Select(n => new
|
||||||
|
{
|
||||||
|
n.NestName,
|
||||||
|
n.CopyID,
|
||||||
|
n.Material,
|
||||||
|
n.MatGrade,
|
||||||
|
n.MatThick,
|
||||||
|
n.PlateCount,
|
||||||
|
Year = n.DateProgrammed!.Value.Year,
|
||||||
|
Month = n.DateProgrammed!.Value.Month
|
||||||
|
})
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
var plateData = await _db.PlateHeaders
|
||||||
|
.Select(p => new { p.NestName, p.CopyID, p.PlateWeight, p.TotalArea1 })
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
var platesByNest = plateData
|
||||||
|
.GroupBy(p => (p.NestName, p.CopyID))
|
||||||
|
.ToDictionary(
|
||||||
|
g => g.Key,
|
||||||
|
g => (Weight: g.Sum(x => x.PlateWeight ?? 0), Area: g.Sum(x => x.TotalArea1 ?? 0)));
|
||||||
|
|
||||||
|
var grouped = nestData
|
||||||
|
.GroupBy(n => (n.Year, n.Month))
|
||||||
|
.OrderBy(g => g.Key.Year).ThenBy(g => g.Key.Month)
|
||||||
|
.Select(g => new MaterialUsageByPeriod
|
||||||
|
{
|
||||||
|
Year = g.Key.Year,
|
||||||
|
Month = g.Key.Month,
|
||||||
|
Materials = g
|
||||||
|
.GroupBy(n => (n.Material, n.MatGrade, n.MatThick))
|
||||||
|
.Select(mg => new MaterialUsageSummary
|
||||||
|
{
|
||||||
|
MaterialNumber = int.TryParse(mg.Key.Material, out var num) ? num : 0,
|
||||||
|
MaterialGrade = mg.Key.MatGrade ?? "",
|
||||||
|
Thickness = mg.Key.MatThick,
|
||||||
|
NestCount = mg.Count(),
|
||||||
|
PlateCount = mg.Sum(x => x.PlateCount),
|
||||||
|
TotalWeight = mg.Sum(x => platesByNest.TryGetValue((x.NestName, x.CopyID), out var v) ? v.Weight : 0),
|
||||||
|
TotalArea = mg.Sum(x => platesByNest.TryGetValue((x.NestName, x.CopyID), out var v) ? v.Area : 0)
|
||||||
|
})
|
||||||
|
.OrderByDescending(m => m.TotalWeight)
|
||||||
|
.ToList()
|
||||||
|
});
|
||||||
|
|
||||||
|
return Ok(grouped);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var nestData = await nestsQuery
|
||||||
|
.Select(n => new
|
||||||
|
{
|
||||||
|
n.NestName,
|
||||||
|
n.CopyID,
|
||||||
|
n.Material,
|
||||||
|
n.MatGrade,
|
||||||
|
n.MatThick,
|
||||||
|
n.PlateCount
|
||||||
|
})
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
var plateData = await _db.PlateHeaders
|
||||||
|
.Select(p => new { p.NestName, p.CopyID, p.PlateWeight, p.TotalArea1 })
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
var platesByNest = plateData
|
||||||
|
.GroupBy(p => (p.NestName, p.CopyID))
|
||||||
|
.ToDictionary(
|
||||||
|
g => g.Key,
|
||||||
|
g => (Weight: g.Sum(x => x.PlateWeight ?? 0), Area: g.Sum(x => x.TotalArea1 ?? 0)));
|
||||||
|
|
||||||
|
var summary = nestData
|
||||||
|
.GroupBy(n => (n.Material, n.MatGrade, n.MatThick))
|
||||||
|
.Select(g => new MaterialUsageSummary
|
||||||
|
{
|
||||||
|
MaterialNumber = int.TryParse(g.Key.Material, out var num) ? num : 0,
|
||||||
|
MaterialGrade = g.Key.MatGrade ?? "",
|
||||||
|
Thickness = g.Key.MatThick,
|
||||||
|
NestCount = g.Count(),
|
||||||
|
PlateCount = g.Sum(x => x.PlateCount),
|
||||||
|
TotalWeight = g.Sum(x => platesByNest.TryGetValue((x.NestName, x.CopyID), out var v) ? v.Weight : 0),
|
||||||
|
TotalArea = g.Sum(x => platesByNest.TryGetValue((x.NestName, x.CopyID), out var v) ? v.Area : 0)
|
||||||
|
})
|
||||||
|
.OrderByDescending(m => m.TotalWeight)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
return Ok(summary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get most commonly used plate sizes, optionally filtered by material.
|
||||||
|
/// </summary>
|
||||||
|
[HttpGet("plate-sizes")]
|
||||||
|
public async Task<ActionResult<List<PlateSizeUsage>>> GetPlateSizes(
|
||||||
|
[FromQuery] int? materialNumber = null,
|
||||||
|
[FromQuery] string? grade = null,
|
||||||
|
[FromQuery] DateTime? startDate = null,
|
||||||
|
[FromQuery] DateTime? endDate = null,
|
||||||
|
[FromQuery] bool cutOnly = true)
|
||||||
|
{
|
||||||
|
var start = startDate ?? DateTime.Now.AddYears(-1);
|
||||||
|
var end = endDate ?? DateTime.Now;
|
||||||
|
|
||||||
|
var nestsQuery = _db.NestHeaders
|
||||||
|
.Where(n => n.DateProgrammed != null
|
||||||
|
&& n.DateProgrammed >= start
|
||||||
|
&& n.DateProgrammed <= end);
|
||||||
|
|
||||||
|
if (cutOnly)
|
||||||
|
nestsQuery = nestsQuery.Where(n => CutStatuses.Contains(n.Status));
|
||||||
|
|
||||||
|
if (materialNumber.HasValue)
|
||||||
|
nestsQuery = nestsQuery.Where(n => n.Material == materialNumber.Value.ToString());
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(grade))
|
||||||
|
nestsQuery = nestsQuery.Where(n => n.MatGrade == grade);
|
||||||
|
|
||||||
|
var nestKeys = await nestsQuery
|
||||||
|
.Select(n => new { n.NestName, n.CopyID })
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
var nestKeySet = nestKeys
|
||||||
|
.Select(n => (n.NestName, n.CopyID))
|
||||||
|
.ToHashSet();
|
||||||
|
|
||||||
|
var plateData = await _db.PlateHeaders
|
||||||
|
.Where(p => !string.IsNullOrEmpty(p.PlateSize))
|
||||||
|
.Select(p => new
|
||||||
|
{
|
||||||
|
p.NestName,
|
||||||
|
p.CopyID,
|
||||||
|
p.PlateSize,
|
||||||
|
p.NestedLength,
|
||||||
|
p.NestedWidth,
|
||||||
|
p.PlateWeight,
|
||||||
|
p.TotalArea1
|
||||||
|
})
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
var filteredPlates = plateData
|
||||||
|
.Where(p => nestKeySet.Contains((p.NestName, p.CopyID)));
|
||||||
|
|
||||||
|
var totalWeight = filteredPlates.Sum(p => p.PlateWeight ?? 0);
|
||||||
|
|
||||||
|
var plateSizes = filteredPlates
|
||||||
|
.GroupBy(p => p.PlateSize)
|
||||||
|
.Select(g => new PlateSizeUsage
|
||||||
|
{
|
||||||
|
PlateSize = g.Key ?? "",
|
||||||
|
Length = g.Max(p => p.NestedLength),
|
||||||
|
Width = g.Max(p => p.NestedWidth),
|
||||||
|
Count = g.Count(),
|
||||||
|
TotalWeight = g.Sum(p => p.PlateWeight ?? 0),
|
||||||
|
TotalArea = g.Sum(p => p.TotalArea1 ?? 0),
|
||||||
|
PercentageOfTotal = totalWeight > 0
|
||||||
|
? Math.Round(g.Sum(p => p.PlateWeight ?? 0) / totalWeight * 100, 2)
|
||||||
|
: 0
|
||||||
|
})
|
||||||
|
.OrderByDescending(p => p.Count)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
return Ok(plateSizes);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get material consumption breakdown by thickness.
|
||||||
|
/// </summary>
|
||||||
|
[HttpGet("thickness-breakdown")]
|
||||||
|
public async Task<ActionResult<List<ThicknessBreakdown>>> GetThicknessBreakdown(
|
||||||
|
[FromQuery] int? year = null,
|
||||||
|
[FromQuery] DateTime? startDate = null,
|
||||||
|
[FromQuery] DateTime? endDate = null,
|
||||||
|
[FromQuery] bool cutOnly = true)
|
||||||
|
{
|
||||||
|
DateTime start, end;
|
||||||
|
|
||||||
|
if (year.HasValue)
|
||||||
|
{
|
||||||
|
start = new DateTime(year.Value, 1, 1);
|
||||||
|
end = new DateTime(year.Value, 12, 31, 23, 59, 59);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
start = startDate ?? DateTime.Now.AddYears(-1);
|
||||||
|
end = endDate ?? DateTime.Now;
|
||||||
|
}
|
||||||
|
|
||||||
|
var nestsQuery = _db.NestHeaders
|
||||||
|
.Where(n => n.DateProgrammed != null
|
||||||
|
&& n.DateProgrammed >= start
|
||||||
|
&& n.DateProgrammed <= end);
|
||||||
|
|
||||||
|
if (cutOnly)
|
||||||
|
nestsQuery = nestsQuery.Where(n => CutStatuses.Contains(n.Status));
|
||||||
|
|
||||||
|
var nestData = await nestsQuery
|
||||||
|
.Select(n => new
|
||||||
|
{
|
||||||
|
n.NestName,
|
||||||
|
n.CopyID,
|
||||||
|
n.MatThick,
|
||||||
|
n.MatGrade,
|
||||||
|
n.PlateCount
|
||||||
|
})
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
var plateData = await _db.PlateHeaders
|
||||||
|
.Select(p => new { p.NestName, p.CopyID, p.PlateWeight })
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
var platesByNest = plateData
|
||||||
|
.GroupBy(p => (p.NestName, p.CopyID))
|
||||||
|
.ToDictionary(g => g.Key, g => g.Sum(x => x.PlateWeight ?? 0));
|
||||||
|
|
||||||
|
var totalWeight = nestData.Sum(n => platesByNest.TryGetValue((n.NestName, n.CopyID), out var w) ? w : 0);
|
||||||
|
|
||||||
|
var breakdown = nestData
|
||||||
|
.GroupBy(n => n.MatThick)
|
||||||
|
.Select(g => new ThicknessBreakdown
|
||||||
|
{
|
||||||
|
Thickness = g.Key,
|
||||||
|
NestCount = g.Count(),
|
||||||
|
PlateCount = g.Sum(x => x.PlateCount),
|
||||||
|
TotalWeight = g.Sum(x => platesByNest.TryGetValue((x.NestName, x.CopyID), out var w) ? w : 0),
|
||||||
|
PercentageOfTotal = totalWeight > 0
|
||||||
|
? Math.Round(g.Sum(x => platesByNest.TryGetValue((x.NestName, x.CopyID), out var w) ? w : 0) / totalWeight * 100, 2)
|
||||||
|
: 0,
|
||||||
|
MaterialGrades = g.Select(x => x.MatGrade).Where(x => !string.IsNullOrWhiteSpace(x)).Distinct().ToList()!
|
||||||
|
})
|
||||||
|
.OrderByDescending(t => t.TotalWeight)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
return Ok(breakdown);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get material usage breakdown by customer.
|
||||||
|
/// </summary>
|
||||||
|
[HttpGet("customer-usage")]
|
||||||
|
public async Task<ActionResult<List<CustomerMaterialUsage>>> GetCustomerUsage(
|
||||||
|
[FromQuery] string? customerId = null,
|
||||||
|
[FromQuery] int months = 12,
|
||||||
|
[FromQuery] bool cutOnly = true)
|
||||||
|
{
|
||||||
|
var start = DateTime.Now.AddMonths(-months);
|
||||||
|
|
||||||
|
var nestsQuery = _db.NestHeaders
|
||||||
|
.Where(n => n.DateProgrammed != null && n.DateProgrammed >= start);
|
||||||
|
|
||||||
|
if (cutOnly)
|
||||||
|
nestsQuery = nestsQuery.Where(n => CutStatuses.Contains(n.Status));
|
||||||
|
|
||||||
|
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.CustomerName,
|
||||||
|
n.Material,
|
||||||
|
n.MatGrade,
|
||||||
|
n.MatThick,
|
||||||
|
n.PlateCount
|
||||||
|
})
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
var plateData = await _db.PlateHeaders
|
||||||
|
.Select(p => new { p.NestName, p.CopyID, p.PlateWeight, p.TotalArea1 })
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
var platesByNest = plateData
|
||||||
|
.GroupBy(p => (p.NestName, p.CopyID))
|
||||||
|
.ToDictionary(
|
||||||
|
g => g.Key,
|
||||||
|
g => (Weight: g.Sum(x => x.PlateWeight ?? 0), Area: g.Sum(x => x.TotalArea1 ?? 0)));
|
||||||
|
|
||||||
|
var customerUsage = nestData
|
||||||
|
.GroupBy(n => n.CustomerName)
|
||||||
|
.Select(cg => new CustomerMaterialUsage
|
||||||
|
{
|
||||||
|
CustomerName = cg.Key ?? "Unknown",
|
||||||
|
TotalNests = cg.Count(),
|
||||||
|
TotalWeight = cg.Sum(x => platesByNest.TryGetValue((x.NestName, x.CopyID), out var v) ? v.Weight : 0),
|
||||||
|
Materials = cg
|
||||||
|
.GroupBy(n => (n.Material, n.MatGrade, n.MatThick))
|
||||||
|
.Select(mg => new MaterialUsageSummary
|
||||||
|
{
|
||||||
|
MaterialNumber = int.TryParse(mg.Key.Material, out var num) ? num : 0,
|
||||||
|
MaterialGrade = mg.Key.MatGrade ?? "",
|
||||||
|
Thickness = mg.Key.MatThick,
|
||||||
|
NestCount = mg.Count(),
|
||||||
|
PlateCount = mg.Sum(x => x.PlateCount),
|
||||||
|
TotalWeight = mg.Sum(x => platesByNest.TryGetValue((x.NestName, x.CopyID), out var v) ? v.Weight : 0),
|
||||||
|
TotalArea = mg.Sum(x => platesByNest.TryGetValue((x.NestName, x.CopyID), out var v) ? v.Area : 0)
|
||||||
|
})
|
||||||
|
.OrderByDescending(m => m.TotalWeight)
|
||||||
|
.ToList()
|
||||||
|
})
|
||||||
|
.OrderByDescending(c => c.TotalWeight)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
return Ok(customerUsage);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get stock recommendations based on historical usage.
|
||||||
|
/// </summary>
|
||||||
|
[HttpGet("stock-recommendations")]
|
||||||
|
public async Task<ActionResult<List<StockRecommendation>>> GetStockRecommendations(
|
||||||
|
[FromQuery] int months = 6,
|
||||||
|
[FromQuery] double stockMultiplier = 1.5,
|
||||||
|
[FromQuery] bool cutOnly = true,
|
||||||
|
[FromQuery] string? customerId = null,
|
||||||
|
[FromQuery] DateTime? startDate = null,
|
||||||
|
[FromQuery] DateTime? endDate = null)
|
||||||
|
{
|
||||||
|
DateTime start, end;
|
||||||
|
int monthsAnalyzed;
|
||||||
|
|
||||||
|
if (startDate.HasValue && endDate.HasValue)
|
||||||
|
{
|
||||||
|
start = startDate.Value;
|
||||||
|
end = endDate.Value;
|
||||||
|
monthsAnalyzed = (int)Math.Ceiling((end - start).TotalDays / 30.0);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
start = DateTime.Now.AddMonths(-months);
|
||||||
|
end = DateTime.Now;
|
||||||
|
monthsAnalyzed = months;
|
||||||
|
}
|
||||||
|
|
||||||
|
var nestsQuery = _db.NestHeaders
|
||||||
|
.Where(n => n.DateProgrammed != null && n.DateProgrammed >= start && n.DateProgrammed <= end);
|
||||||
|
|
||||||
|
if (cutOnly)
|
||||||
|
nestsQuery = nestsQuery.Where(n => CutStatuses.Contains(n.Status));
|
||||||
|
|
||||||
|
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,
|
||||||
|
n.PlateCount
|
||||||
|
})
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
var nestKeys = nestData.Select(n => (n.NestName, n.CopyID)).ToHashSet();
|
||||||
|
|
||||||
|
var plateData = await _db.PlateHeaders
|
||||||
|
.Select(p => new { p.NestName, p.CopyID, p.PlateSize, p.PlateWeight })
|
||||||
|
.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));
|
||||||
|
|
||||||
|
// Group plates by material/grade/thickness, then by plate size to get actual counts
|
||||||
|
var platesByMaterial = filteredPlates
|
||||||
|
.Select(p => new
|
||||||
|
{
|
||||||
|
p.PlateSize,
|
||||||
|
p.PlateWeight,
|
||||||
|
Material = nestMaterialLookup.TryGetValue((p.NestName, p.CopyID), out var m) ? m : default
|
||||||
|
})
|
||||||
|
.Where(p => p.Material != default)
|
||||||
|
.GroupBy(p => p.Material)
|
||||||
|
.ToDictionary(
|
||||||
|
g => g.Key,
|
||||||
|
g => (
|
||||||
|
TotalWeight: g.Sum(x => x.PlateWeight ?? 0),
|
||||||
|
TotalCount: g.Count(),
|
||||||
|
TopSize: g
|
||||||
|
.Where(x => !string.IsNullOrEmpty(x.PlateSize))
|
||||||
|
.GroupBy(x => x.PlateSize)
|
||||||
|
.OrderByDescending(sg => sg.Count())
|
||||||
|
.Select(sg => sg.Key)
|
||||||
|
.FirstOrDefault() ?? ""
|
||||||
|
));
|
||||||
|
|
||||||
|
var recommendations = platesByMaterial
|
||||||
|
.Select(kvp =>
|
||||||
|
{
|
||||||
|
var (material, matGrade, matThick) = kvp.Key;
|
||||||
|
var (totalWeight, totalPlates, topPlateSize) = kvp.Value;
|
||||||
|
|
||||||
|
var avgMonthlyWeight = totalWeight / monthsAnalyzed;
|
||||||
|
var avgMonthlyPlates = (double)totalPlates / monthsAnalyzed;
|
||||||
|
|
||||||
|
return new StockRecommendation
|
||||||
|
{
|
||||||
|
MaterialNumber = int.TryParse(material, out var num) ? num : 0,
|
||||||
|
MaterialGrade = matGrade ?? "",
|
||||||
|
Thickness = matThick,
|
||||||
|
RecommendedPlateSize = topPlateSize,
|
||||||
|
AverageMonthlyWeight = Math.Round(avgMonthlyWeight, 2),
|
||||||
|
AverageMonthlyPlates = Math.Round(avgMonthlyPlates, 2),
|
||||||
|
MonthsAnalyzed = monthsAnalyzed,
|
||||||
|
SuggestedStockWeight = Math.Round(avgMonthlyWeight * stockMultiplier, 2),
|
||||||
|
SuggestedStockPlates = (int)Math.Ceiling(avgMonthlyPlates * stockMultiplier)
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.Where(r => r.AverageMonthlyWeight > 0)
|
||||||
|
.OrderByDescending(r => r.AverageMonthlyWeight)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
return Ok(recommendations);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get a quick summary of top materials used.
|
||||||
|
/// </summary>
|
||||||
|
[HttpGet("top-materials")]
|
||||||
|
public async Task<ActionResult<List<MaterialUsageSummary>>> GetTopMaterials(
|
||||||
|
[FromQuery] int count = 10,
|
||||||
|
[FromQuery] int months = 12,
|
||||||
|
[FromQuery] bool cutOnly = true)
|
||||||
|
{
|
||||||
|
var start = DateTime.Now.AddMonths(-months);
|
||||||
|
|
||||||
|
var nestsQuery = _db.NestHeaders
|
||||||
|
.Where(n => n.DateProgrammed != null && n.DateProgrammed >= start);
|
||||||
|
|
||||||
|
if (cutOnly)
|
||||||
|
nestsQuery = nestsQuery.Where(n => CutStatuses.Contains(n.Status));
|
||||||
|
|
||||||
|
var nestData = await nestsQuery
|
||||||
|
.Select(n => new
|
||||||
|
{
|
||||||
|
n.NestName,
|
||||||
|
n.CopyID,
|
||||||
|
n.Material,
|
||||||
|
n.MatGrade,
|
||||||
|
n.MatThick,
|
||||||
|
n.PlateCount
|
||||||
|
})
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
var plateData = await _db.PlateHeaders
|
||||||
|
.Select(p => new { p.NestName, p.CopyID, p.PlateWeight, p.TotalArea1 })
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
var platesByNest = plateData
|
||||||
|
.GroupBy(p => (p.NestName, p.CopyID))
|
||||||
|
.ToDictionary(
|
||||||
|
g => g.Key,
|
||||||
|
g => (Weight: g.Sum(x => x.PlateWeight ?? 0), Area: g.Sum(x => x.TotalArea1 ?? 0)));
|
||||||
|
|
||||||
|
var topMaterials = nestData
|
||||||
|
.GroupBy(n => (n.Material, n.MatGrade, n.MatThick))
|
||||||
|
.Select(g => new MaterialUsageSummary
|
||||||
|
{
|
||||||
|
MaterialNumber = int.TryParse(g.Key.Material, out var num) ? num : 0,
|
||||||
|
MaterialGrade = g.Key.MatGrade ?? "",
|
||||||
|
Thickness = g.Key.MatThick,
|
||||||
|
NestCount = g.Count(),
|
||||||
|
PlateCount = g.Sum(x => x.PlateCount),
|
||||||
|
TotalWeight = g.Sum(x => platesByNest.TryGetValue((x.NestName, x.CopyID), out var v) ? v.Weight : 0),
|
||||||
|
TotalArea = g.Sum(x => platesByNest.TryGetValue((x.NestName, x.CopyID), out var v) ? v.Area : 0)
|
||||||
|
})
|
||||||
|
.OrderByDescending(m => m.TotalWeight)
|
||||||
|
.Take(count)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
return Ok(topMaterials);
|
||||||
|
}
|
||||||
|
}
|
||||||
61
PepApi.Core/Models/MaterialUsageSummary.cs
Normal file
61
PepApi.Core/Models/MaterialUsageSummary.cs
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
namespace PepApi.Core.Models;
|
||||||
|
|
||||||
|
public class MaterialUsageSummary
|
||||||
|
{
|
||||||
|
public int MaterialNumber { get; set; }
|
||||||
|
public required string MaterialGrade { get; set; }
|
||||||
|
public double Thickness { get; set; }
|
||||||
|
public int NestCount { get; set; }
|
||||||
|
public int PlateCount { get; set; }
|
||||||
|
public double TotalWeight { get; set; }
|
||||||
|
public double TotalArea { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class MaterialUsageByPeriod
|
||||||
|
{
|
||||||
|
public int Year { get; set; }
|
||||||
|
public int Month { get; set; }
|
||||||
|
public required List<MaterialUsageSummary> Materials { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class PlateSizeUsage
|
||||||
|
{
|
||||||
|
public required string PlateSize { get; set; }
|
||||||
|
public double Length { get; set; }
|
||||||
|
public double Width { get; set; }
|
||||||
|
public int Count { get; set; }
|
||||||
|
public double TotalWeight { get; set; }
|
||||||
|
public double TotalArea { get; set; }
|
||||||
|
public double PercentageOfTotal { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ThicknessBreakdown
|
||||||
|
{
|
||||||
|
public double Thickness { get; set; }
|
||||||
|
public int NestCount { get; set; }
|
||||||
|
public int PlateCount { get; set; }
|
||||||
|
public double TotalWeight { get; set; }
|
||||||
|
public double PercentageOfTotal { get; set; }
|
||||||
|
public required List<string> MaterialGrades { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class CustomerMaterialUsage
|
||||||
|
{
|
||||||
|
public required string CustomerName { get; set; }
|
||||||
|
public required List<MaterialUsageSummary> Materials { get; set; }
|
||||||
|
public double TotalWeight { get; set; }
|
||||||
|
public int TotalNests { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class StockRecommendation
|
||||||
|
{
|
||||||
|
public int MaterialNumber { get; set; }
|
||||||
|
public required string MaterialGrade { get; set; }
|
||||||
|
public double Thickness { get; set; }
|
||||||
|
public required string RecommendedPlateSize { get; set; }
|
||||||
|
public double AverageMonthlyWeight { get; set; }
|
||||||
|
public double AverageMonthlyPlates { get; set; }
|
||||||
|
public int MonthsAnalyzed { get; set; }
|
||||||
|
public double SuggestedStockWeight { get; set; }
|
||||||
|
public int SuggestedStockPlates { get; set; }
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user