feat: Add CutList.Web Blazor Server application
Add a new web-based frontend for cut list optimization using: - Blazor Server with .NET 8 - Entity Framework Core with MSSQL LocalDB - Full CRUD for Materials, Suppliers, Projects, and Cutting Tools - Supplier stock length management for quick project setup - Integration with CutList.Core for bin packing optimization - Print-friendly HTML reports with efficiency statistics Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,65 @@
|
||||
using CutList.Core;
|
||||
using CutList.Core.Nesting;
|
||||
using CutList.Web.Data.Entities;
|
||||
|
||||
namespace CutList.Web.Services;
|
||||
|
||||
public class CutListPackingService
|
||||
{
|
||||
public PackResult Pack(IEnumerable<ProjectPart> parts, IEnumerable<ProjectStockBin> stockBins, decimal kerfInches)
|
||||
{
|
||||
var engine = new MultiBinEngine();
|
||||
engine.Spacing = (double)kerfInches;
|
||||
engine.Strategy = PackingStrategy.AdvancedFit;
|
||||
|
||||
// Convert stock bins to MultiBin
|
||||
var multiBins = stockBins
|
||||
.Where(b => b.LengthInches > 0)
|
||||
.Select(b => new MultiBin((double)b.LengthInches, b.Quantity, b.Priority))
|
||||
.ToList();
|
||||
|
||||
engine.SetBins(multiBins);
|
||||
|
||||
// Convert parts to BinItem (expand quantity)
|
||||
var items = parts
|
||||
.SelectMany(p => Enumerable.Range(0, p.Quantity)
|
||||
.Select(_ => new BinItem(p.Name, (double)p.LengthInches)))
|
||||
.ToList();
|
||||
|
||||
return engine.Pack(items);
|
||||
}
|
||||
|
||||
public PackingSummary GetSummary(PackResult result)
|
||||
{
|
||||
var summary = new PackingSummary();
|
||||
|
||||
foreach (var bin in result.Bins)
|
||||
{
|
||||
summary.TotalBins++;
|
||||
summary.TotalMaterial += bin.Length;
|
||||
summary.TotalUsed += bin.UsedLength;
|
||||
summary.TotalWaste += bin.RemainingLength;
|
||||
summary.TotalPieces += bin.Items.Count;
|
||||
}
|
||||
|
||||
summary.ItemsNotPlaced = result.ItemsNotUsed.Count;
|
||||
|
||||
if (summary.TotalMaterial > 0)
|
||||
{
|
||||
summary.Efficiency = summary.TotalUsed / summary.TotalMaterial * 100;
|
||||
}
|
||||
|
||||
return summary;
|
||||
}
|
||||
}
|
||||
|
||||
public class PackingSummary
|
||||
{
|
||||
public int TotalBins { get; set; }
|
||||
public int TotalPieces { get; set; }
|
||||
public double TotalMaterial { get; set; }
|
||||
public double TotalUsed { get; set; }
|
||||
public double TotalWaste { get; set; }
|
||||
public double Efficiency { get; set; }
|
||||
public int ItemsNotPlaced { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
using CutList.Web.Data;
|
||||
using CutList.Web.Data.Entities;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace CutList.Web.Services;
|
||||
|
||||
public class MaterialService
|
||||
{
|
||||
private readonly ApplicationDbContext _context;
|
||||
|
||||
public static readonly string[] CommonShapes =
|
||||
{
|
||||
"Round Tube",
|
||||
"Square Tube",
|
||||
"Rectangular Tube",
|
||||
"Angle",
|
||||
"Channel",
|
||||
"Flat Bar",
|
||||
"Round Bar",
|
||||
"Square Bar",
|
||||
"I-Beam",
|
||||
"Pipe"
|
||||
};
|
||||
|
||||
public MaterialService(ApplicationDbContext context)
|
||||
{
|
||||
_context = context;
|
||||
}
|
||||
|
||||
public async Task<List<Material>> GetAllAsync(bool includeInactive = false)
|
||||
{
|
||||
var query = _context.Materials.AsQueryable();
|
||||
if (!includeInactive)
|
||||
{
|
||||
query = query.Where(m => m.IsActive);
|
||||
}
|
||||
return await query.OrderBy(m => m.Shape).ThenBy(m => m.Size).ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<Material?> GetByIdAsync(int id)
|
||||
{
|
||||
return await _context.Materials.FindAsync(id);
|
||||
}
|
||||
|
||||
public async Task<Material> CreateAsync(Material material)
|
||||
{
|
||||
material.CreatedAt = DateTime.UtcNow;
|
||||
_context.Materials.Add(material);
|
||||
await _context.SaveChangesAsync();
|
||||
return material;
|
||||
}
|
||||
|
||||
public async Task UpdateAsync(Material material)
|
||||
{
|
||||
material.UpdatedAt = DateTime.UtcNow;
|
||||
_context.Materials.Update(material);
|
||||
await _context.SaveChangesAsync();
|
||||
}
|
||||
|
||||
public async Task DeleteAsync(int id)
|
||||
{
|
||||
var material = await _context.Materials.FindAsync(id);
|
||||
if (material != null)
|
||||
{
|
||||
material.IsActive = false;
|
||||
await _context.SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<bool> ExistsAsync(string shape, string size, int? excludeId = null)
|
||||
{
|
||||
var query = _context.Materials.Where(m => m.Shape == shape && m.Size == size && m.IsActive);
|
||||
if (excludeId.HasValue)
|
||||
{
|
||||
query = query.Where(m => m.Id != excludeId.Value);
|
||||
}
|
||||
return await query.AnyAsync();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,321 @@
|
||||
using CutList.Web.Data;
|
||||
using CutList.Web.Data.Entities;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace CutList.Web.Services;
|
||||
|
||||
public class ProjectService
|
||||
{
|
||||
private readonly ApplicationDbContext _context;
|
||||
|
||||
public ProjectService(ApplicationDbContext context)
|
||||
{
|
||||
_context = context;
|
||||
}
|
||||
|
||||
public async Task<List<Project>> GetAllAsync()
|
||||
{
|
||||
return await _context.Projects
|
||||
.Include(p => p.Material)
|
||||
.Include(p => p.CuttingTool)
|
||||
.OrderByDescending(p => p.UpdatedAt ?? p.CreatedAt)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<Project?> GetByIdAsync(int id)
|
||||
{
|
||||
return await _context.Projects
|
||||
.Include(p => p.Material)
|
||||
.Include(p => p.CuttingTool)
|
||||
.Include(p => p.Parts.OrderBy(pt => pt.SortOrder))
|
||||
.Include(p => p.StockBins.OrderBy(sb => sb.SortOrder))
|
||||
.FirstOrDefaultAsync(p => p.Id == id);
|
||||
}
|
||||
|
||||
public async Task<Project> CreateAsync(Project project)
|
||||
{
|
||||
project.CreatedAt = DateTime.UtcNow;
|
||||
_context.Projects.Add(project);
|
||||
await _context.SaveChangesAsync();
|
||||
return project;
|
||||
}
|
||||
|
||||
public async Task UpdateAsync(Project project)
|
||||
{
|
||||
project.UpdatedAt = DateTime.UtcNow;
|
||||
_context.Projects.Update(project);
|
||||
await _context.SaveChangesAsync();
|
||||
}
|
||||
|
||||
public async Task DeleteAsync(int id)
|
||||
{
|
||||
var project = await _context.Projects.FindAsync(id);
|
||||
if (project != null)
|
||||
{
|
||||
_context.Projects.Remove(project);
|
||||
await _context.SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<Project> DuplicateAsync(int id)
|
||||
{
|
||||
var original = await GetByIdAsync(id);
|
||||
if (original == null)
|
||||
{
|
||||
throw new ArgumentException("Project not found", nameof(id));
|
||||
}
|
||||
|
||||
var duplicate = new Project
|
||||
{
|
||||
Name = $"{original.Name} (Copy)",
|
||||
MaterialId = original.MaterialId,
|
||||
CuttingToolId = original.CuttingToolId,
|
||||
Notes = original.Notes,
|
||||
CreatedAt = DateTime.UtcNow
|
||||
};
|
||||
|
||||
_context.Projects.Add(duplicate);
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
// Copy parts
|
||||
foreach (var part in original.Parts)
|
||||
{
|
||||
_context.ProjectParts.Add(new ProjectPart
|
||||
{
|
||||
ProjectId = duplicate.Id,
|
||||
Name = part.Name,
|
||||
LengthInches = part.LengthInches,
|
||||
Quantity = part.Quantity,
|
||||
SortOrder = part.SortOrder
|
||||
});
|
||||
}
|
||||
|
||||
// Copy stock bins
|
||||
foreach (var bin in original.StockBins)
|
||||
{
|
||||
_context.ProjectStockBins.Add(new ProjectStockBin
|
||||
{
|
||||
ProjectId = duplicate.Id,
|
||||
LengthInches = bin.LengthInches,
|
||||
Quantity = bin.Quantity,
|
||||
Priority = bin.Priority,
|
||||
SortOrder = bin.SortOrder
|
||||
});
|
||||
}
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
return duplicate;
|
||||
}
|
||||
|
||||
// Parts management
|
||||
public async Task<ProjectPart> AddPartAsync(ProjectPart part)
|
||||
{
|
||||
var maxOrder = await _context.ProjectParts
|
||||
.Where(p => p.ProjectId == part.ProjectId)
|
||||
.MaxAsync(p => (int?)p.SortOrder) ?? -1;
|
||||
part.SortOrder = maxOrder + 1;
|
||||
|
||||
_context.ProjectParts.Add(part);
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
// Update project timestamp
|
||||
var project = await _context.Projects.FindAsync(part.ProjectId);
|
||||
if (project != null)
|
||||
{
|
||||
project.UpdatedAt = DateTime.UtcNow;
|
||||
await _context.SaveChangesAsync();
|
||||
}
|
||||
|
||||
return part;
|
||||
}
|
||||
|
||||
public async Task UpdatePartAsync(ProjectPart part)
|
||||
{
|
||||
_context.ProjectParts.Update(part);
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
var project = await _context.Projects.FindAsync(part.ProjectId);
|
||||
if (project != null)
|
||||
{
|
||||
project.UpdatedAt = DateTime.UtcNow;
|
||||
await _context.SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task DeletePartAsync(int id)
|
||||
{
|
||||
var part = await _context.ProjectParts.FindAsync(id);
|
||||
if (part != null)
|
||||
{
|
||||
var projectId = part.ProjectId;
|
||||
_context.ProjectParts.Remove(part);
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
var project = await _context.Projects.FindAsync(projectId);
|
||||
if (project != null)
|
||||
{
|
||||
project.UpdatedAt = DateTime.UtcNow;
|
||||
await _context.SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Stock bins management
|
||||
public async Task<ProjectStockBin> AddStockBinAsync(ProjectStockBin bin)
|
||||
{
|
||||
var maxOrder = await _context.ProjectStockBins
|
||||
.Where(b => b.ProjectId == bin.ProjectId)
|
||||
.MaxAsync(b => (int?)b.SortOrder) ?? -1;
|
||||
bin.SortOrder = maxOrder + 1;
|
||||
|
||||
_context.ProjectStockBins.Add(bin);
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
var project = await _context.Projects.FindAsync(bin.ProjectId);
|
||||
if (project != null)
|
||||
{
|
||||
project.UpdatedAt = DateTime.UtcNow;
|
||||
await _context.SaveChangesAsync();
|
||||
}
|
||||
|
||||
return bin;
|
||||
}
|
||||
|
||||
public async Task UpdateStockBinAsync(ProjectStockBin bin)
|
||||
{
|
||||
_context.ProjectStockBins.Update(bin);
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
var project = await _context.Projects.FindAsync(bin.ProjectId);
|
||||
if (project != null)
|
||||
{
|
||||
project.UpdatedAt = DateTime.UtcNow;
|
||||
await _context.SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task DeleteStockBinAsync(int id)
|
||||
{
|
||||
var bin = await _context.ProjectStockBins.FindAsync(id);
|
||||
if (bin != null)
|
||||
{
|
||||
var projectId = bin.ProjectId;
|
||||
_context.ProjectStockBins.Remove(bin);
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
var project = await _context.Projects.FindAsync(projectId);
|
||||
if (project != null)
|
||||
{
|
||||
project.UpdatedAt = DateTime.UtcNow;
|
||||
await _context.SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async Task ImportStockFromSupplierAsync(int projectId, int supplierId, int? materialId = null)
|
||||
{
|
||||
var query = _context.SupplierStocks
|
||||
.Where(s => s.SupplierId == supplierId && s.IsActive);
|
||||
|
||||
if (materialId.HasValue)
|
||||
{
|
||||
query = query.Where(s => s.MaterialId == materialId.Value);
|
||||
}
|
||||
|
||||
var stocks = await query.ToListAsync();
|
||||
var maxOrder = await _context.ProjectStockBins
|
||||
.Where(b => b.ProjectId == projectId)
|
||||
.MaxAsync(b => (int?)b.SortOrder) ?? -1;
|
||||
|
||||
foreach (var stock in stocks)
|
||||
{
|
||||
// Check if already exists
|
||||
var exists = await _context.ProjectStockBins
|
||||
.AnyAsync(b => b.ProjectId == projectId && b.LengthInches == stock.LengthInches);
|
||||
|
||||
if (!exists)
|
||||
{
|
||||
_context.ProjectStockBins.Add(new ProjectStockBin
|
||||
{
|
||||
ProjectId = projectId,
|
||||
LengthInches = stock.LengthInches,
|
||||
Quantity = -1,
|
||||
Priority = 25,
|
||||
SortOrder = ++maxOrder
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
var project = await _context.Projects.FindAsync(projectId);
|
||||
if (project != null)
|
||||
{
|
||||
project.UpdatedAt = DateTime.UtcNow;
|
||||
await _context.SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
|
||||
// Cutting tools
|
||||
public async Task<List<CuttingTool>> GetCuttingToolsAsync(bool includeInactive = false)
|
||||
{
|
||||
var query = _context.CuttingTools.AsQueryable();
|
||||
if (!includeInactive)
|
||||
{
|
||||
query = query.Where(t => t.IsActive);
|
||||
}
|
||||
return await query.OrderBy(t => t.Name).ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<CuttingTool?> GetCuttingToolByIdAsync(int id)
|
||||
{
|
||||
return await _context.CuttingTools.FindAsync(id);
|
||||
}
|
||||
|
||||
public async Task<CuttingTool?> GetDefaultCuttingToolAsync()
|
||||
{
|
||||
return await _context.CuttingTools.FirstOrDefaultAsync(t => t.IsDefault && t.IsActive);
|
||||
}
|
||||
|
||||
public async Task<CuttingTool> CreateCuttingToolAsync(CuttingTool tool)
|
||||
{
|
||||
if (tool.IsDefault)
|
||||
{
|
||||
// Clear other defaults
|
||||
var others = await _context.CuttingTools.Where(t => t.IsDefault).ToListAsync();
|
||||
foreach (var other in others)
|
||||
{
|
||||
other.IsDefault = false;
|
||||
}
|
||||
}
|
||||
|
||||
_context.CuttingTools.Add(tool);
|
||||
await _context.SaveChangesAsync();
|
||||
return tool;
|
||||
}
|
||||
|
||||
public async Task UpdateCuttingToolAsync(CuttingTool tool)
|
||||
{
|
||||
if (tool.IsDefault)
|
||||
{
|
||||
var others = await _context.CuttingTools.Where(t => t.IsDefault && t.Id != tool.Id).ToListAsync();
|
||||
foreach (var other in others)
|
||||
{
|
||||
other.IsDefault = false;
|
||||
}
|
||||
}
|
||||
|
||||
_context.CuttingTools.Update(tool);
|
||||
await _context.SaveChangesAsync();
|
||||
}
|
||||
|
||||
public async Task DeleteCuttingToolAsync(int id)
|
||||
{
|
||||
var tool = await _context.CuttingTools.FindAsync(id);
|
||||
if (tool != null)
|
||||
{
|
||||
tool.IsActive = false;
|
||||
await _context.SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
using CutList.Core;
|
||||
using CutList.Core.Formatting;
|
||||
using CutList.Core.Nesting;
|
||||
using CutList.Web.Data.Entities;
|
||||
|
||||
namespace CutList.Web.Services;
|
||||
|
||||
public class ReportService
|
||||
{
|
||||
public string FormatLength(double inches)
|
||||
{
|
||||
return ArchUnits.FormatFromInches(inches);
|
||||
}
|
||||
|
||||
public List<ItemGroup> GroupItems(IReadOnlyList<BinItem> items)
|
||||
{
|
||||
return items
|
||||
.GroupBy(i => new { i.Name, i.Length })
|
||||
.Select(g => new ItemGroup
|
||||
{
|
||||
Name = g.Key.Name,
|
||||
Length = g.Key.Length,
|
||||
Count = g.Count()
|
||||
})
|
||||
.OrderByDescending(g => g.Length)
|
||||
.ToList();
|
||||
}
|
||||
}
|
||||
|
||||
public class ItemGroup
|
||||
{
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public double Length { get; set; }
|
||||
public int Count { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
using CutList.Web.Data;
|
||||
using CutList.Web.Data.Entities;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace CutList.Web.Services;
|
||||
|
||||
public class SupplierService
|
||||
{
|
||||
private readonly ApplicationDbContext _context;
|
||||
|
||||
public SupplierService(ApplicationDbContext context)
|
||||
{
|
||||
_context = context;
|
||||
}
|
||||
|
||||
public async Task<List<Supplier>> GetAllAsync(bool includeInactive = false)
|
||||
{
|
||||
var query = _context.Suppliers.AsQueryable();
|
||||
if (!includeInactive)
|
||||
{
|
||||
query = query.Where(s => s.IsActive);
|
||||
}
|
||||
return await query.OrderBy(s => s.Name).ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<Supplier?> GetByIdAsync(int id)
|
||||
{
|
||||
return await _context.Suppliers
|
||||
.Include(s => s.Stocks)
|
||||
.ThenInclude(st => st.Material)
|
||||
.FirstOrDefaultAsync(s => s.Id == id);
|
||||
}
|
||||
|
||||
public async Task<Supplier> CreateAsync(Supplier supplier)
|
||||
{
|
||||
supplier.CreatedAt = DateTime.UtcNow;
|
||||
_context.Suppliers.Add(supplier);
|
||||
await _context.SaveChangesAsync();
|
||||
return supplier;
|
||||
}
|
||||
|
||||
public async Task UpdateAsync(Supplier supplier)
|
||||
{
|
||||
_context.Suppliers.Update(supplier);
|
||||
await _context.SaveChangesAsync();
|
||||
}
|
||||
|
||||
public async Task DeleteAsync(int id)
|
||||
{
|
||||
var supplier = await _context.Suppliers.FindAsync(id);
|
||||
if (supplier != null)
|
||||
{
|
||||
supplier.IsActive = false;
|
||||
await _context.SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
|
||||
// Stock management
|
||||
public async Task<List<SupplierStock>> GetStocksForSupplierAsync(int supplierId)
|
||||
{
|
||||
return await _context.SupplierStocks
|
||||
.Include(s => s.Material)
|
||||
.Where(s => s.SupplierId == supplierId && s.IsActive)
|
||||
.OrderBy(s => s.Material.Shape)
|
||||
.ThenBy(s => s.Material.Size)
|
||||
.ThenBy(s => s.LengthInches)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<List<SupplierStock>> GetStocksForMaterialAsync(int materialId)
|
||||
{
|
||||
return await _context.SupplierStocks
|
||||
.Include(s => s.Supplier)
|
||||
.Where(s => s.MaterialId == materialId && s.IsActive && s.Supplier.IsActive)
|
||||
.OrderBy(s => s.Supplier.Name)
|
||||
.ThenBy(s => s.LengthInches)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<SupplierStock?> GetStockByIdAsync(int id)
|
||||
{
|
||||
return await _context.SupplierStocks
|
||||
.Include(s => s.Material)
|
||||
.Include(s => s.Supplier)
|
||||
.FirstOrDefaultAsync(s => s.Id == id);
|
||||
}
|
||||
|
||||
public async Task<SupplierStock> AddStockAsync(SupplierStock stock)
|
||||
{
|
||||
_context.SupplierStocks.Add(stock);
|
||||
await _context.SaveChangesAsync();
|
||||
return stock;
|
||||
}
|
||||
|
||||
public async Task UpdateStockAsync(SupplierStock stock)
|
||||
{
|
||||
_context.SupplierStocks.Update(stock);
|
||||
await _context.SaveChangesAsync();
|
||||
}
|
||||
|
||||
public async Task DeleteStockAsync(int id)
|
||||
{
|
||||
var stock = await _context.SupplierStocks.FindAsync(id);
|
||||
if (stock != null)
|
||||
{
|
||||
stock.IsActive = false;
|
||||
await _context.SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<bool> StockExistsAsync(int supplierId, int materialId, decimal lengthInches, int? excludeId = null)
|
||||
{
|
||||
var query = _context.SupplierStocks.Where(s =>
|
||||
s.SupplierId == supplierId &&
|
||||
s.MaterialId == materialId &&
|
||||
s.LengthInches == lengthInches &&
|
||||
s.IsActive);
|
||||
|
||||
if (excludeId.HasValue)
|
||||
{
|
||||
query = query.Where(s => s.Id != excludeId.Value);
|
||||
}
|
||||
|
||||
return await query.AnyAsync();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user