Compare commits

...

20 Commits

Author SHA1 Message Date
8e73d630d5 chore: Change development port to 5009
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-01 23:57:13 -05:00
079f5b1085 style: Update sidebar color and optimize print layout
Changes sidebar gradient to neutral gray and adds compact print styles
for better paper usage when printing reports.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-01 23:57:02 -05:00
97fa90357b fix: Minor UI improvements to layout and LengthInput
Removes unnecessary top row link and adds default placeholder to
LengthInput component.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-01 23:56:47 -05:00
bf6c4764ed feat: Add custom print title JS function
Adds printWithTitle function to set document title before printing for
better file naming when saving as PDF.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-01 23:56:37 -05:00
ed911a13ba feat: Redesign Results page for multi-material output
Updates Results page to display packing results grouped by material,
showing in-stock vs. to-be-purchased breakdown with order summaries.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-01 23:56:28 -05:00
c99de55fe1 feat: Update Project pages for multi-material parts
Redesigns Project Edit with a tabbed interface and adds material
selection (shape -> size) when adding parts. Updates Index to show
customer instead of material.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-01 23:56:19 -05:00
8b16cbd79f feat: Add stock lengths management UI to Material Edit page
Extends the Material Edit page with a side panel to manage available
stock lengths, including quantity tracking and CRUD operations.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-01 23:56:06 -05:00
cad5ab790a chore: Register API controllers in Program.cs
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-01 23:55:57 -05:00
f8020549fe feat: Add REST API controllers for materials
Adds MaterialsController with bulk import support and SeedController
for development data seeding.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-01 23:55:48 -05:00
66ed19a1ac feat: Implement multi-material packing with inventory awareness
Refactors CutListPackingService to:
- Pack parts grouped by material type
- Distinguish between in-stock and to-be-purchased bins
- Use material stock lengths for finite inventory
- Use supplier stock for unlimited purchase options

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-01 23:55:38 -05:00
051b866c6d refactor: Remove stock bin management from ProjectService
Removes project-level stock bin methods since stock is now derived from
material stock lengths. Updates queries to include Material on parts.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-01 23:55:28 -05:00
3d80adbfff feat: Add stock length management to MaterialService
Adds CRUD methods for managing material stock lengths, including
duplicate checking and quantity tracking.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-01 23:55:18 -05:00
b7b98d4338 chore: Update DbContext for new entity relationships
Updates ApplicationDbContext to configure MaterialStockLength and
revised ProjectPart relationships.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-01 23:55:07 -05:00
ced272d3e3 feat: Support multi-material project parts
Each project part now references its own material, allowing a single
project to use multiple material types. Removes ProjectStockBin entity
since stock is now derived from material stock lengths.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-01 23:54:59 -05:00
35b26e673e feat: Add Customer field to Project entity
Adds a customer name field to projects for better job tracking and
identification on reports.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-01 23:54:48 -05:00
cca569ae81 feat: Add MaterialStockLength entity for inventory tracking
Introduces a new entity to track available stock lengths per material,
enabling in-stock vs. purchase-needed distinction during optimization.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-01 23:54:23 -05:00
fa36d82285 feat: Add material shape dropdown and Save As button to MainForm
UI improvements:
- Add material shape dropdown (Round Tube, Square Tube, Angle, etc.)
- Add Save As toolbar button for saving to a new file
- Simplify toolbar button styling (show text with icons)
- Expose SelectedMaterialShape property for report generation

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-01 21:56:42 -05:00
b0c9470bb7 feat: Add cut method and material shape to saved reports
Include cutting tool name and material shape in the text report output.
This provides better context when reviewing saved cut lists.

Changes:
- BinFileSaver: Add CutMethod and MaterialShape properties
- ResultsForm: Pass cut method and material to file saver
- IMainView: Extend ShowResults with additional parameters
- MainFormPresenter: Use document name for save filename if available

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-01 21:56:33 -05:00
9868df162d 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>
2026-02-01 21:56:21 -05:00
6db8ab21f4 feat: Improve document management with Save/Save As and keyboard shortcuts
- Track file path after save/load so Save doesn't prompt again
- Add Save As (Ctrl+Shift+S) to always prompt for location
- Update window title to show current filename
- Generate incremental default filenames (CutList_1.json, etc.)
- Add keyboard shortcuts: Ctrl+S (Save), Ctrl+O (Open), Ctrl+N (New)
- Enter key in items grid moves to Length column on next row

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-01 16:20:59 -05:00
60 changed files with 7115 additions and 259 deletions

View File

@@ -11,6 +11,16 @@ namespace CutList.Core
public bool OpenFileAfterSave { get; set; }
/// <summary>
/// The cutting method/tool used (e.g., "Miter Saw", "Table Saw").
/// </summary>
public string? CutMethod { get; set; }
/// <summary>
/// The material shape (e.g., "Round Tube", "Square Tube", "Flat Bar").
/// </summary>
public string? MaterialShape { get; set; }
public BinFileSaver(IEnumerable<Bin> bins)
{
_bins = bins ?? throw new ArgumentNullException(nameof(bins));
@@ -74,10 +84,24 @@ namespace CutList.Core
var totalItems = _bins.Sum(b => b.Items.Count);
writer.WriteLine("CUT LIST");
writer.WriteLine($"Date: {DateTime.Now:g}");
writer.WriteLine($"Total stock bars needed: {totalBars}");
writer.WriteLine($"Total pieces to cut: {totalItems}");
WriteSeparator(writer, '=');
writer.WriteLine();
WriteAlignedLine(writer, "Date", DateTime.Now.ToString("g"));
if (!string.IsNullOrEmpty(CutMethod))
WriteAlignedLine(writer, "Cut Method", CutMethod);
if (!string.IsNullOrEmpty(MaterialShape))
WriteAlignedLine(writer, "Material", MaterialShape);
WriteAlignedLine(writer, "Stock Bars Needed", totalBars.ToString());
WriteAlignedLine(writer, "Total Pieces", totalItems.ToString());
writer.WriteLine();
}
private static void WriteAlignedLine(TextWriter writer, string label, string value, int labelWidth = 20)
{
writer.WriteLine($" {label.PadRight(labelWidth)} {value}");
}
private void WriteBinSummary(TextWriter writer, Bin bin, int id)
@@ -85,8 +109,9 @@ namespace CutList.Core
var stockLength = FormatHelper.ConvertToMixedFraction(bin.Length);
var dropLength = FormatHelper.ConvertToMixedFraction(bin.RemainingLength);
writer.WriteLine(new string('─', 50));
writer.WriteLine($"BAR #{id} - Start with {stockLength}\" stock");
WriteSeparator(writer);
writer.WriteLine($"BAR #{id} - Length: {stockLength}\"");
writer.WriteLine();
writer.WriteLine(" Cut these pieces:");
@@ -97,6 +122,11 @@ namespace CutList.Core
writer.WriteLine();
}
private static void WriteSeparator(TextWriter writer, char c = '─', int length = 50)
{
writer.WriteLine(new string(c, length));
}
private void WriteBinItems(TextWriter writer, Bin bin)
{
var groups = bin.Items
@@ -128,14 +158,14 @@ namespace CutList.Core
string fmt(double v) => FormatHelper.ConvertToMixedFraction(v);
writer.WriteLine(new string('═', 50));
WriteSeparator(writer, '=');
writer.WriteLine("SUMMARY");
writer.WriteLine($" Stock bars needed: {totalBars}");
writer.WriteLine($" Total pieces to cut: {totalItems}");
writer.WriteLine($" Total material used: {fmt(totalStock)}\"");
writer.WriteLine($" Total drop/waste: {fmt(totalDrop)}\"");
writer.WriteLine(new string('═', 50));
writer.WriteLine();
WriteAlignedLine(writer, "Stock Bars Needed", totalBars.ToString());
WriteAlignedLine(writer, "Total Pieces", totalItems.ToString());
WriteAlignedLine(writer, "Total Material Used", $"{fmt(totalStock)}\"");
WriteAlignedLine(writer, "Total Drop/Waste", $"{fmt(totalDrop)}\"");
WriteSeparator(writer, '=');
}
}
}

View File

@@ -0,0 +1,35 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<base href="/" />
<link rel="stylesheet" href="css/bootstrap/bootstrap.min.css" />
<link rel="stylesheet" href="css/app.css" />
<link rel="stylesheet" href="css/report.css" />
<link rel="stylesheet" href="CutList.Web.styles.css" />
<link rel="icon" type="image/png" href="favicon.png" />
<HeadOutlet @rendermode="InteractiveServer" />
</head>
<body>
<Routes @rendermode="InteractiveServer" />
<script src="_framework/blazor.web.js"></script>
<script>
function printWithTitle(title) {
const originalTitle = document.title;
document.title = title;
const restoreTitle = () => {
document.title = originalTitle;
window.removeEventListener('afterprint', restoreTitle);
};
window.addEventListener('afterprint', restoreTitle);
window.print();
}
</script>
</body>
</html>

View File

@@ -0,0 +1,13 @@
@inherits LayoutComponentBase
<div class="page">
<div class="sidebar">
<NavMenu />
</div>
<main>
<article class="content px-4">
@Body
</article>
</main>
</div>

View File

@@ -0,0 +1,37 @@
<div class="top-row ps-3 navbar navbar-dark">
<div class="container-fluid">
<a class="navbar-brand" href="">CutList</a>
</div>
</div>
<input type="checkbox" title="Navigation menu" class="navbar-toggler" />
<div class="nav-scrollable" onclick="document.querySelector('.navbar-toggler').click()">
<nav class="flex-column">
<div class="nav-item px-3">
<NavLink class="nav-link" href="" Match="NavLinkMatch.All">
<span class="bi bi-house-door-fill-nav-menu" aria-hidden="true"></span> Home
</NavLink>
</div>
<div class="nav-item px-3">
<NavLink class="nav-link" href="projects">
<span class="bi bi-list-check-nav-menu" aria-hidden="true"></span> Projects
</NavLink>
</div>
<div class="nav-item px-3">
<NavLink class="nav-link" href="materials">
<span class="bi bi-box-nav-menu" aria-hidden="true"></span> Materials
</NavLink>
</div>
<div class="nav-item px-3">
<NavLink class="nav-link" href="suppliers">
<span class="bi bi-building-nav-menu" aria-hidden="true"></span> Suppliers
</NavLink>
</div>
<div class="nav-item px-3">
<NavLink class="nav-link" href="tools">
<span class="bi bi-tools-nav-menu" aria-hidden="true"></span> Cutting Tools
</NavLink>
</div>
</nav>
</div>

View File

@@ -0,0 +1,111 @@
.navbar-toggler {
appearance: none;
cursor: pointer;
width: 3.5rem;
height: 2.5rem;
color: white;
position: absolute;
top: 0.5rem;
right: 1rem;
border: 1px solid rgba(255, 255, 255, 0.1);
background: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e") no-repeat center/1.75rem rgba(255, 255, 255, 0.1);
}
.navbar-toggler:checked {
background-color: rgba(255, 255, 255, 0.5);
}
.top-row {
height: 3.5rem;
background-color: rgba(0,0,0,0.4);
}
.navbar-brand {
font-size: 1.1rem;
}
.bi {
display: inline-block;
position: relative;
width: 1.25rem;
height: 1.25rem;
margin-right: 0.75rem;
top: -1px;
background-size: cover;
}
.bi-house-door-fill-nav-menu {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-house-door-fill' viewBox='0 0 16 16'%3E%3Cpath d='M6.5 14.5v-3.505c0-.245.25-.495.5-.495h2c.25 0 .5.25.5.5v3.5a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5v-7a.5.5 0 0 0-.146-.354L13 5.793V2.5a.5.5 0 0 0-.5-.5h-1a.5.5 0 0 0-.5.5v1.293L8.354 1.146a.5.5 0 0 0-.708 0l-6 6A.5.5 0 0 0 1.5 7.5v7a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5Z'/%3E%3C/svg%3E");
}
.bi-list-check-nav-menu {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-list-check' viewBox='0 0 16 16'%3E%3Cpath fill-rule='evenodd' d='M5 11.5a.5.5 0 0 1 .5-.5h9a.5.5 0 0 1 0 1h-9a.5.5 0 0 1-.5-.5zm0-4a.5.5 0 0 1 .5-.5h9a.5.5 0 0 1 0 1h-9a.5.5 0 0 1-.5-.5zm0-4a.5.5 0 0 1 .5-.5h9a.5.5 0 0 1 0 1h-9a.5.5 0 0 1-.5-.5zM3.854 2.146a.5.5 0 0 1 0 .708l-1.5 1.5a.5.5 0 0 1-.708 0l-.5-.5a.5.5 0 1 1 .708-.708L2 3.293l1.146-1.147a.5.5 0 0 1 .708 0zm0 4a.5.5 0 0 1 0 .708l-1.5 1.5a.5.5 0 0 1-.708 0l-.5-.5a.5.5 0 1 1 .708-.708L2 7.293l1.146-1.147a.5.5 0 0 1 .708 0zm0 4a.5.5 0 0 1 0 .708l-1.5 1.5a.5.5 0 0 1-.708 0l-.5-.5a.5.5 0 0 1 .708-.708l.146.147 1.146-1.147a.5.5 0 0 1 .708 0z'/%3E%3C/svg%3E");
}
.bi-box-nav-menu {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-box' viewBox='0 0 16 16'%3E%3Cpath d='M8.186 1.113a.5.5 0 0 0-.372 0L1.846 3.5 8 5.961 14.154 3.5 8.186 1.113zM15 4.239l-6.5 2.6v7.922l6.5-2.6V4.24zM7.5 14.762V6.838L1 4.239v7.923l6.5 2.6zM7.443.184a1.5 1.5 0 0 1 1.114 0l7.129 2.852A.5.5 0 0 1 16 3.5v8.662a1 1 0 0 1-.629.928l-7.185 2.874a.5.5 0 0 1-.372 0L.63 13.09a1 1 0 0 1-.63-.928V3.5a.5.5 0 0 1 .314-.464L7.443.184z'/%3E%3C/svg%3E");
}
.bi-building-nav-menu {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-building' viewBox='0 0 16 16'%3E%3Cpath d='M4 2.5a.5.5 0 0 1 .5-.5h1a.5.5 0 0 1 .5.5v1a.5.5 0 0 1-.5.5h-1a.5.5 0 0 1-.5-.5v-1Zm3 0a.5.5 0 0 1 .5-.5h1a.5.5 0 0 1 .5.5v1a.5.5 0 0 1-.5.5h-1a.5.5 0 0 1-.5-.5v-1Zm3.5-.5a.5.5 0 0 0-.5.5v1a.5.5 0 0 0 .5.5h1a.5.5 0 0 0 .5-.5v-1a.5.5 0 0 0-.5-.5h-1ZM4 5.5a.5.5 0 0 1 .5-.5h1a.5.5 0 0 1 .5.5v1a.5.5 0 0 1-.5.5h-1a.5.5 0 0 1-.5-.5v-1ZM7.5 5a.5.5 0 0 0-.5.5v1a.5.5 0 0 0 .5.5h1a.5.5 0 0 0 .5-.5v-1a.5.5 0 0 0-.5-.5h-1Zm2.5.5a.5.5 0 0 1 .5-.5h1a.5.5 0 0 1 .5.5v1a.5.5 0 0 1-.5.5h-1a.5.5 0 0 1-.5-.5v-1ZM4.5 8a.5.5 0 0 0-.5.5v1a.5.5 0 0 0 .5.5h1a.5.5 0 0 0 .5-.5v-1a.5.5 0 0 0-.5-.5h-1Zm2.5.5a.5.5 0 0 1 .5-.5h1a.5.5 0 0 1 .5.5v1a.5.5 0 0 1-.5.5h-1a.5.5 0 0 1-.5-.5v-1Zm3.5-.5a.5.5 0 0 0-.5.5v1a.5.5 0 0 0 .5.5h1a.5.5 0 0 0 .5-.5v-1a.5.5 0 0 0-.5-.5h-1Z'/%3E%3Cpath d='M2 1a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1v14a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V1Zm11 0H3v14h3v-2.5a.5.5 0 0 1 .5-.5h3a.5.5 0 0 1 .5.5V15h3V1Z'/%3E%3C/svg%3E");
}
.bi-tools-nav-menu {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-tools' viewBox='0 0 16 16'%3E%3Cpath d='M1 0 0 1l2.2 3.081a1 1 0 0 0 .815.419h.07a1 1 0 0 1 .708.293l2.675 2.675-2.617 2.654A3.003 3.003 0 0 0 0 13a3 3 0 1 0 5.878-.851l2.654-2.617.968.968-.305.914a1 1 0 0 0 .242 1.023l3.27 3.27a.997.997 0 0 0 1.414 0l1.586-1.586a.997.997 0 0 0 0-1.414l-3.27-3.27a1 1 0 0 0-1.023-.242l-.914.305-.968-.968 2.617-2.654A3.003 3.003 0 0 0 13 0a3 3 0 1 0-.851 5.878L9.495 8.53l-2.675-2.675a1 1 0 0 1-.293-.707v-.071a1 1 0 0 0-.419-.814L3.081 2.2 1 0ZM3 13a2 2 0 1 1-4 0 2 2 0 0 1 4 0Zm7.5-8.5a2 2 0 1 1 4 0 2 2 0 0 1-4 0Z'/%3E%3C/svg%3E");
}
.nav-item {
font-size: 0.9rem;
padding-bottom: 0.5rem;
}
.nav-item:first-of-type {
padding-top: 1rem;
}
.nav-item:last-of-type {
padding-bottom: 1rem;
}
.nav-item ::deep .nav-link {
color: #d7d7d7;
background: none;
border: none;
border-radius: 4px;
height: 3rem;
display: flex;
align-items: center;
line-height: 3rem;
width: 100%;
}
.nav-item ::deep a.active {
background-color: rgba(255,255,255,0.37);
color: white;
}
.nav-item ::deep .nav-link:hover {
background-color: rgba(255,255,255,0.1);
color: white;
}
.nav-scrollable {
display: none;
}
.navbar-toggler:checked ~ .nav-scrollable {
display: block;
}
@media (min-width: 641px) {
.navbar-toggler {
display: none;
}
.nav-scrollable {
display: block;
position: relative;
overflow-y: auto;
flex-grow: 1;
}
}

View File

@@ -0,0 +1,58 @@
@page "/"
<PageTitle>CutList - Home</PageTitle>
<h1>CutList</h1>
<p class="lead">1D Bin Packing Optimization for Material Cutting</p>
<div class="row mt-4">
<div class="col-md-6 col-lg-3 mb-4">
<div class="card h-100">
<div class="card-body">
<h5 class="card-title">Projects</h5>
<p class="card-text">Create and manage cut list projects. Add parts and stock bins, then optimize to minimize waste.</p>
<a href="projects" class="btn btn-primary">Go to Projects</a>
</div>
</div>
</div>
<div class="col-md-6 col-lg-3 mb-4">
<div class="card h-100">
<div class="card-body">
<h5 class="card-title">Materials</h5>
<p class="card-text">Manage material types (tube, bar, angle, etc.) with their shapes and sizes.</p>
<a href="materials" class="btn btn-outline-primary">Manage Materials</a>
</div>
</div>
</div>
<div class="col-md-6 col-lg-3 mb-4">
<div class="card h-100">
<div class="card-body">
<h5 class="card-title">Suppliers</h5>
<p class="card-text">Track suppliers and their available stock lengths for quick project setup.</p>
<a href="suppliers" class="btn btn-outline-primary">Manage Suppliers</a>
</div>
</div>
</div>
<div class="col-md-6 col-lg-3 mb-4">
<div class="card h-100">
<div class="card-body">
<h5 class="card-title">Cutting Tools</h5>
<p class="card-text">Configure cutting tools with their kerf widths for accurate waste calculations.</p>
<a href="tools" class="btn btn-outline-primary">Manage Tools</a>
</div>
</div>
</div>
</div>
<hr class="my-4" />
<h4>How It Works</h4>
<ol>
<li><strong>Set up materials</strong> - Define the shapes and sizes of materials you work with</li>
<li><strong>Add suppliers</strong> - Track which stock lengths are available from your suppliers</li>
<li><strong>Create a project</strong> - Add the parts you need to cut with their lengths and quantities</li>
<li><strong>Add stock bins</strong> - Specify which stock lengths to cut from (import from supplier or add manually)</li>
<li><strong>Optimize</strong> - Run the optimizer to find the best cutting pattern</li>
<li><strong>Print report</strong> - Generate a printable cut list to take to the shop</li>
</ol>

View File

@@ -0,0 +1,331 @@
@page "/materials/new"
@page "/materials/{Id:int}"
@inject MaterialService MaterialService
@inject NavigationManager Navigation
@using CutList.Core.Formatting
<PageTitle>@(IsNew ? "Add Material" : "Edit Material")</PageTitle>
<h1>@(IsNew ? "Add Material" : material.DisplayName)</h1>
@if (loading)
{
<p><em>Loading...</em></p>
}
else
{
<div class="row">
<div class="col-lg-6 mb-4">
<div class="card">
<div class="card-header">
<h5 class="mb-0">Material Details</h5>
</div>
<div class="card-body">
<EditForm Model="material" OnValidSubmit="SaveAsync">
<DataAnnotationsValidator />
<div class="mb-3">
<label class="form-label">Shape</label>
<InputSelect class="form-select" @bind-Value="material.Shape">
<option value="">-- Select Shape --</option>
@foreach (var shape in MaterialService.CommonShapes)
{
<option value="@shape">@shape</option>
}
</InputSelect>
<ValidationMessage For="@(() => material.Shape)" />
</div>
<div class="mb-3">
<label class="form-label">Size</label>
<InputText class="form-control" @bind-Value="material.Size" placeholder="e.g., 1&quot; OD x 0.065 wall" />
<ValidationMessage For="@(() => material.Size)" />
<div class="form-text">Examples: "1&quot; OD x 0.065 wall", "2x2", "1.5 x 1.5 x 0.125"</div>
</div>
<div class="mb-3">
<label class="form-label">Description (optional)</label>
<InputText class="form-control" @bind-Value="material.Description" placeholder="Optional description" />
</div>
@if (!string.IsNullOrEmpty(errorMessage))
{
<div class="alert alert-danger">@errorMessage</div>
}
<div class="d-flex gap-2">
<button type="submit" class="btn btn-primary" disabled="@saving">
@if (saving)
{
<span class="spinner-border spinner-border-sm me-1"></span>
}
@(IsNew ? "Create Material" : "Save Changes")
</button>
<a href="materials" class="btn btn-outline-secondary">@(IsNew ? "Cancel" : "Back to List")</a>
</div>
</EditForm>
</div>
</div>
</div>
@if (!IsNew)
{
<div class="col-lg-6">
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="mb-0">Available Stock Lengths</h5>
<button class="btn btn-sm btn-primary" @onclick="ShowAddStockForm">Add Length</button>
</div>
<div class="card-body">
@if (showStockForm)
{
<div class="border rounded p-3 mb-3 bg-light">
<h6>@(editingStock == null ? "Add Stock Length" : "Edit Stock Length")</h6>
<div class="row g-2">
<div class="col-md-4">
<label class="form-label">Length</label>
<LengthInput @bind-Value="newStock.LengthInches" />
</div>
<div class="col-md-3">
<label class="form-label">Qty in Stock</label>
<input type="number" class="form-control" @bind="newStock.Quantity" min="0" />
</div>
<div class="col-md-5">
<label class="form-label">Notes (optional)</label>
<InputText class="form-control" @bind-Value="newStock.Notes" />
</div>
</div>
@if (!string.IsNullOrEmpty(stockErrorMessage))
{
<div class="alert alert-danger mt-2 mb-0">@stockErrorMessage</div>
}
<div class="mt-3 d-flex gap-2">
<button class="btn btn-primary btn-sm" @onclick="SaveStockAsync" disabled="@savingStock">
@if (savingStock)
{
<span class="spinner-border spinner-border-sm me-1"></span>
}
@(editingStock == null ? "Add" : "Save")
</button>
<button class="btn btn-outline-secondary btn-sm" @onclick="CancelStockForm">Cancel</button>
</div>
</div>
}
@if (stockLengths.Count == 0)
{
<p class="text-muted">No stock lengths configured yet.</p>
<p class="text-muted small">Add common stock lengths for this material (e.g., 20', 24') to quickly populate project stock bins.</p>
}
else
{
<table class="table table-sm">
<thead>
<tr>
<th>Length</th>
<th>Qty</th>
<th>Notes</th>
<th style="width: 100px;">Actions</th>
</tr>
</thead>
<tbody>
@foreach (var stock in stockLengths)
{
<tr>
<td>@ArchUnits.FormatFromInches((double)stock.LengthInches)</td>
<td>@stock.Quantity</td>
<td>@(stock.Notes ?? "-")</td>
<td>
<button class="btn btn-sm btn-outline-primary" @onclick="() => EditStock(stock)">Edit</button>
<button class="btn btn-sm btn-outline-danger" @onclick="() => ConfirmDeleteStock(stock)">Delete</button>
</td>
</tr>
}
</tbody>
</table>
}
</div>
</div>
</div>
}
</div>
}
<ConfirmDialog @ref="deleteStockDialog"
Title="Delete Stock Length"
Message="@deleteStockMessage"
ConfirmText="Delete"
OnConfirm="DeleteStockConfirmed" />
@code {
[Parameter]
public int? Id { get; set; }
private Material material = new();
private List<MaterialStockLength> stockLengths = new();
private bool loading = true;
private bool saving;
private string? errorMessage;
// Stock form
private bool showStockForm;
private bool savingStock;
private MaterialStockLength newStock = new();
private MaterialStockLength? editingStock;
private string? stockErrorMessage;
// Delete dialog
private ConfirmDialog deleteStockDialog = null!;
private MaterialStockLength? stockToDelete;
private string deleteStockMessage = "";
private bool IsNew => !Id.HasValue;
protected override async Task OnInitializedAsync()
{
if (Id.HasValue)
{
var existing = await MaterialService.GetByIdAsync(Id.Value);
if (existing == null)
{
Navigation.NavigateTo("materials");
return;
}
material = existing;
stockLengths = await MaterialService.GetStockLengthsAsync(Id.Value);
}
loading = false;
}
private async Task SaveAsync()
{
errorMessage = null;
saving = true;
try
{
if (string.IsNullOrWhiteSpace(material.Shape))
{
errorMessage = "Shape is required";
return;
}
if (string.IsNullOrWhiteSpace(material.Size))
{
errorMessage = "Size is required";
return;
}
// Check for duplicates
if (await MaterialService.ExistsAsync(material.Shape, material.Size, Id))
{
errorMessage = "A material with this shape and size already exists";
return;
}
if (IsNew)
{
var created = await MaterialService.CreateAsync(material);
Navigation.NavigateTo($"materials/{created.Id}");
}
else
{
await MaterialService.UpdateAsync(material);
}
}
finally
{
saving = false;
}
}
// Stock length methods
private void ShowAddStockForm()
{
editingStock = null;
newStock = new MaterialStockLength { MaterialId = Id!.Value };
showStockForm = true;
stockErrorMessage = null;
}
private void EditStock(MaterialStockLength stock)
{
editingStock = stock;
newStock = new MaterialStockLength
{
Id = stock.Id,
MaterialId = stock.MaterialId,
LengthInches = stock.LengthInches,
Quantity = stock.Quantity,
Notes = stock.Notes
};
showStockForm = true;
stockErrorMessage = null;
}
private void CancelStockForm()
{
showStockForm = false;
editingStock = null;
stockErrorMessage = null;
}
private async Task SaveStockAsync()
{
stockErrorMessage = null;
savingStock = true;
try
{
if (newStock.LengthInches <= 0)
{
stockErrorMessage = "Length must be greater than zero";
return;
}
var exists = await MaterialService.StockLengthExistsAsync(
newStock.MaterialId,
newStock.LengthInches,
editingStock?.Id);
if (exists)
{
stockErrorMessage = "This stock length already exists for this material";
return;
}
if (editingStock == null)
{
await MaterialService.AddStockLengthAsync(newStock);
}
else
{
await MaterialService.UpdateStockLengthAsync(newStock);
}
stockLengths = await MaterialService.GetStockLengthsAsync(Id!.Value);
showStockForm = false;
editingStock = null;
}
finally
{
savingStock = false;
}
}
private void ConfirmDeleteStock(MaterialStockLength stock)
{
stockToDelete = stock;
deleteStockMessage = $"Are you sure you want to delete the {ArchUnits.FormatFromInches((double)stock.LengthInches)} stock length?";
deleteStockDialog.Show();
}
private async Task DeleteStockConfirmed()
{
if (stockToDelete != null)
{
await MaterialService.DeleteStockLengthAsync(stockToDelete.Id);
stockLengths = await MaterialService.GetStockLengthsAsync(Id!.Value);
}
}
}

View File

@@ -0,0 +1,84 @@
@page "/materials"
@inject MaterialService MaterialService
@inject NavigationManager Navigation
<PageTitle>Materials</PageTitle>
<div class="d-flex justify-content-between align-items-center mb-3">
<h1>Materials</h1>
<a href="materials/new" class="btn btn-primary">Add Material</a>
</div>
@if (loading)
{
<p><em>Loading...</em></p>
}
else if (materials.Count == 0)
{
<div class="alert alert-info">
No materials found. <a href="materials/new">Add your first material</a>.
</div>
}
else
{
<table class="table table-striped table-hover">
<thead>
<tr>
<th>Shape</th>
<th>Size</th>
<th>Description</th>
<th style="width: 120px;">Actions</th>
</tr>
</thead>
<tbody>
@foreach (var material in materials)
{
<tr>
<td>@material.Shape</td>
<td>@material.Size</td>
<td>@material.Description</td>
<td>
<a href="materials/@material.Id" class="btn btn-sm btn-outline-primary">Edit</a>
<button class="btn btn-sm btn-outline-danger" @onclick="() => ConfirmDelete(material)">Delete</button>
</td>
</tr>
}
</tbody>
</table>
}
<ConfirmDialog @ref="deleteDialog"
Title="Delete Material"
Message="@deleteMessage"
ConfirmText="Delete"
OnConfirm="DeleteConfirmed" />
@code {
private List<Material> materials = new();
private bool loading = true;
private ConfirmDialog deleteDialog = null!;
private Material? materialToDelete;
private string deleteMessage = "";
protected override async Task OnInitializedAsync()
{
materials = await MaterialService.GetAllAsync();
loading = false;
}
private void ConfirmDelete(Material material)
{
materialToDelete = material;
deleteMessage = $"Are you sure you want to delete \"{material.Shape} - {material.Size}\"?";
deleteDialog.Show();
}
private async Task DeleteConfirmed()
{
if (materialToDelete != null)
{
await MaterialService.DeleteAsync(materialToDelete.Id);
materials = await MaterialService.GetAllAsync();
}
}
}

View File

@@ -0,0 +1,399 @@
@page "/projects/new"
@page "/projects/{Id:int}"
@inject ProjectService ProjectService
@inject MaterialService MaterialService
@inject NavigationManager Navigation
@using CutList.Core.Formatting
<PageTitle>@(IsNew ? "New Project" : project.Name)</PageTitle>
<div class="d-flex justify-content-between align-items-center mb-3">
<h1>@(IsNew ? "New Project" : project.Name)</h1>
@if (!IsNew)
{
<a href="projects/@Id/results" class="btn btn-success">Run Optimization</a>
}
</div>
@if (loading)
{
<p><em>Loading...</em></p>
}
else if (IsNew)
{
<!-- New Project: Simple form -->
<div class="row">
<div class="col-lg-6">
@RenderDetailsForm()
</div>
</div>
}
else
{
<!-- Existing Project: Tabbed interface -->
<ul class="nav nav-tabs mb-3" role="tablist">
<li class="nav-item" role="presentation">
<button class="nav-link @(activeTab == Tab.Details ? "active" : "")"
@onclick="() => SetTab(Tab.Details)" type="button">
Details
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link @(activeTab == Tab.Parts ? "active" : "")"
@onclick="() => SetTab(Tab.Parts)" type="button">
Parts
@if (project.Parts.Count > 0)
{
<span class="badge bg-secondary ms-1">@project.Parts.Sum(p => p.Quantity)</span>
}
</button>
</li>
</ul>
<div class="tab-content">
@if (activeTab == Tab.Details)
{
<div class="row">
<div class="col-lg-6">
@RenderDetailsForm()
</div>
</div>
}
else if (activeTab == Tab.Parts)
{
@RenderPartsTab()
}
</div>
}
@code {
private enum Tab { Details, Parts }
[Parameter]
public int? Id { get; set; }
private Project project = new();
private List<Material> materials = new();
private List<CuttingTool> cuttingTools = new();
private bool loading = true;
private bool savingProject;
private string? projectErrorMessage;
private Tab activeTab = Tab.Details;
private void SetTab(Tab tab) => activeTab = tab;
// Parts form
private bool showPartForm;
private ProjectPart newPart = new();
private ProjectPart? editingPart;
private string? partErrorMessage;
private string selectedShape = string.Empty;
private IEnumerable<string> DistinctShapes => materials.Select(m => m.Shape).Distinct().OrderBy(s => s);
private IEnumerable<Material> FilteredMaterials => string.IsNullOrEmpty(selectedShape)
? Enumerable.Empty<Material>()
: materials.Where(m => m.Shape == selectedShape).OrderBy(m => m.Size);
private bool IsNew => !Id.HasValue;
protected override async Task OnInitializedAsync()
{
materials = await MaterialService.GetAllAsync();
cuttingTools = await ProjectService.GetCuttingToolsAsync();
if (Id.HasValue)
{
var existing = await ProjectService.GetByIdAsync(Id.Value);
if (existing == null)
{
Navigation.NavigateTo("projects");
return;
}
project = existing;
}
else
{
// Set default cutting tool for new projects
var defaultTool = await ProjectService.GetDefaultCuttingToolAsync();
if (defaultTool != null)
{
project.CuttingToolId = defaultTool.Id;
}
}
loading = false;
}
private RenderFragment RenderDetailsForm() => __builder =>
{
<div class="card">
<div class="card-header">
<h5 class="mb-0">Project Details</h5>
</div>
<div class="card-body">
<EditForm Model="project" OnValidSubmit="SaveProjectAsync">
<div class="mb-3">
<label class="form-label">Project Name</label>
<InputText class="form-control" @bind-Value="project.Name" />
</div>
<div class="mb-3">
<label class="form-label">Customer</label>
<InputText class="form-control" @bind-Value="project.Customer" placeholder="Customer name" />
</div>
<div class="mb-3">
<label class="form-label">Cutting Tool</label>
<InputSelect class="form-select" @bind-Value="project.CuttingToolId">
<option value="">-- Select Tool --</option>
@foreach (var tool in cuttingTools)
{
<option value="@tool.Id">@tool.Name (@tool.KerfInches" kerf)</option>
}
</InputSelect>
</div>
<div class="mb-3">
<label class="form-label">Notes</label>
<InputTextArea class="form-control" @bind-Value="project.Notes" rows="3" />
</div>
@if (!string.IsNullOrEmpty(projectErrorMessage))
{
<div class="alert alert-danger">@projectErrorMessage</div>
}
<div class="d-flex gap-2">
<button type="submit" class="btn btn-primary" disabled="@savingProject">
@if (savingProject)
{
<span class="spinner-border spinner-border-sm me-1"></span>
}
@(IsNew ? "Create Project" : "Save")
</button>
<a href="projects" class="btn btn-outline-secondary">Back</a>
</div>
</EditForm>
</div>
</div>
};
private RenderFragment RenderPartsTab() => __builder =>
{
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="mb-0">Parts to Cut</h5>
<button class="btn btn-primary" @onclick="ShowAddPartForm">Add Part</button>
</div>
<div class="card-body">
@if (showPartForm)
{
<div class="border rounded p-3 mb-3 bg-light">
<h6>@(editingPart == null ? "Add Part" : "Edit Part")</h6>
<div class="row g-3">
<div class="col-md-2">
<label class="form-label">Shape</label>
<select class="form-select" @bind="selectedShape" @bind:after="OnShapeChanged">
<option value="">-- Select --</option>
@foreach (var shape in DistinctShapes)
{
<option value="@shape">@shape</option>
}
</select>
</div>
<div class="col-md-2">
<label class="form-label">Size</label>
<select class="form-select" @bind="newPart.MaterialId" disabled="@string.IsNullOrEmpty(selectedShape)">
<option value="0">-- Select --</option>
@foreach (var material in FilteredMaterials)
{
<option value="@material.Id">@material.Size</option>
}
</select>
</div>
<div class="col-md-3">
<label class="form-label">Length</label>
<LengthInput @bind-Value="newPart.LengthInches" />
</div>
<div class="col-md-2">
<label class="form-label">Qty</label>
<input type="number" class="form-control" @bind="newPart.Quantity" min="1" />
</div>
<div class="col-md-3">
<label class="form-label">Name <span class="text-muted fw-normal">(optional)</span></label>
<input type="text" class="form-control" @bind="newPart.Name" placeholder="Part name" />
</div>
</div>
@if (!string.IsNullOrEmpty(partErrorMessage))
{
<div class="alert alert-danger mt-3 mb-0">@partErrorMessage</div>
}
<div class="mt-3 d-flex gap-2">
<button class="btn btn-primary" @onclick="SavePartAsync">@(editingPart == null ? "Add Part" : "Save Changes")</button>
<button class="btn btn-outline-secondary" @onclick="CancelPartForm">Cancel</button>
</div>
</div>
}
@if (project.Parts.Count == 0)
{
<div class="text-center py-4 text-muted">
<p class="mb-2">No parts added yet.</p>
<p class="small">Add the parts you need to cut, selecting the material for each.</p>
</div>
}
else
{
<div class="table-responsive">
<table class="table table-hover mb-0">
<thead>
<tr>
<th>Material</th>
<th>Length</th>
<th>Qty</th>
<th>Name</th>
<th style="width: 120px;">Actions</th>
</tr>
</thead>
<tbody>
@foreach (var part in project.Parts)
{
<tr>
<td>@part.Material.DisplayName</td>
<td>@ArchUnits.FormatFromInches((double)part.LengthInches)</td>
<td>@part.Quantity</td>
<td>@(string.IsNullOrWhiteSpace(part.Name) ? "-" : part.Name)</td>
<td>
<button class="btn btn-sm btn-outline-primary me-1" @onclick="() => EditPart(part)">Edit</button>
<button class="btn btn-sm btn-outline-danger" @onclick="() => DeletePart(part)">Delete</button>
</td>
</tr>
}
</tbody>
</table>
</div>
<div class="mt-3 text-muted">
Total: @project.Parts.Sum(p => p.Quantity) pieces
</div>
}
</div>
</div>
};
private async Task SaveProjectAsync()
{
projectErrorMessage = null;
savingProject = true;
try
{
if (string.IsNullOrWhiteSpace(project.Name))
{
projectErrorMessage = "Project name is required";
return;
}
if (IsNew)
{
var created = await ProjectService.CreateAsync(project);
Navigation.NavigateTo($"projects/{created.Id}");
}
else
{
await ProjectService.UpdateAsync(project);
}
}
finally
{
savingProject = false;
}
}
// Parts methods
private void ShowAddPartForm()
{
editingPart = null;
newPart = new ProjectPart { ProjectId = Id!.Value, Quantity = 1 };
selectedShape = string.Empty;
showPartForm = true;
partErrorMessage = null;
}
private void OnShapeChanged()
{
newPart.MaterialId = 0;
}
private void EditPart(ProjectPart part)
{
editingPart = part;
newPart = new ProjectPart
{
Id = part.Id,
ProjectId = part.ProjectId,
MaterialId = part.MaterialId,
Name = part.Name,
LengthInches = part.LengthInches,
Quantity = part.Quantity,
SortOrder = part.SortOrder
};
selectedShape = part.Material?.Shape ?? string.Empty;
showPartForm = true;
partErrorMessage = null;
}
private void CancelPartForm()
{
showPartForm = false;
editingPart = null;
}
private async Task SavePartAsync()
{
partErrorMessage = null;
if (string.IsNullOrEmpty(selectedShape))
{
partErrorMessage = "Please select a shape";
return;
}
if (newPart.MaterialId == 0)
{
partErrorMessage = "Please select a size";
return;
}
if (newPart.LengthInches <= 0)
{
partErrorMessage = "Length must be greater than zero";
return;
}
if (newPart.Quantity < 1)
{
partErrorMessage = "Quantity must be at least 1";
return;
}
if (editingPart == null)
{
await ProjectService.AddPartAsync(newPart);
}
else
{
await ProjectService.UpdatePartAsync(newPart);
}
project = (await ProjectService.GetByIdAsync(Id!.Value))!;
showPartForm = false;
editingPart = null;
}
private async Task DeletePart(ProjectPart part)
{
await ProjectService.DeletePartAsync(part.Id);
project = (await ProjectService.GetByIdAsync(Id!.Value))!;
}
}

View File

@@ -0,0 +1,94 @@
@page "/projects"
@inject ProjectService ProjectService
@inject NavigationManager Navigation
<PageTitle>Projects</PageTitle>
<div class="d-flex justify-content-between align-items-center mb-3">
<h1>Projects</h1>
<a href="projects/new" class="btn btn-primary">New Project</a>
</div>
@if (loading)
{
<p><em>Loading...</em></p>
}
else if (projects.Count == 0)
{
<div class="alert alert-info">
No projects found. <a href="projects/new">Create your first project</a>.
</div>
}
else
{
<table class="table table-striped table-hover">
<thead>
<tr>
<th>Name</th>
<th>Customer</th>
<th>Cutting Tool</th>
<th>Last Modified</th>
<th style="width: 200px;">Actions</th>
</tr>
</thead>
<tbody>
@foreach (var project in projects)
{
<tr>
<td><a href="projects/@project.Id">@project.Name</a></td>
<td>@(project.Customer ?? "-")</td>
<td>@(project.CuttingTool?.Name ?? "-")</td>
<td>@((project.UpdatedAt ?? project.CreatedAt).ToLocalTime().ToString("g"))</td>
<td>
<a href="projects/@project.Id" class="btn btn-sm btn-outline-primary">Edit</a>
<a href="projects/@project.Id/results" class="btn btn-sm btn-success">Optimize</a>
<button class="btn btn-sm btn-outline-secondary" @onclick="() => DuplicateProject(project)">Copy</button>
<button class="btn btn-sm btn-outline-danger" @onclick="() => ConfirmDelete(project)">Delete</button>
</td>
</tr>
}
</tbody>
</table>
}
<ConfirmDialog @ref="deleteDialog"
Title="Delete Project"
Message="@deleteMessage"
ConfirmText="Delete"
OnConfirm="DeleteConfirmed" />
@code {
private List<Project> projects = new();
private bool loading = true;
private ConfirmDialog deleteDialog = null!;
private Project? projectToDelete;
private string deleteMessage = "";
protected override async Task OnInitializedAsync()
{
projects = await ProjectService.GetAllAsync();
loading = false;
}
private void ConfirmDelete(Project project)
{
projectToDelete = project;
deleteMessage = $"Are you sure you want to delete \"{project.Name}\"? This will also delete all parts and stock bins.";
deleteDialog.Show();
}
private async Task DeleteConfirmed()
{
if (projectToDelete != null)
{
await ProjectService.DeleteAsync(projectToDelete.Id);
projects = await ProjectService.GetAllAsync();
}
}
private async Task DuplicateProject(Project project)
{
var duplicate = await ProjectService.DuplicateAsync(project.Id);
Navigation.NavigateTo($"projects/{duplicate.Id}");
}
}

View File

@@ -0,0 +1,257 @@
@page "/projects/{Id:int}/results"
@inject ProjectService ProjectService
@inject CutListPackingService PackingService
@inject NavigationManager Navigation
@inject IJSRuntime JS
@using CutList.Core
@using CutList.Core.Nesting
@using CutList.Core.Formatting
<PageTitle>Results - @(project?.Name ?? "Project")</PageTitle>
@if (loading)
{
<p><em>Loading...</em></p>
}
else if (project == null)
{
<div class="alert alert-danger">Project not found.</div>
}
else
{
<div class="d-flex justify-content-between align-items-center mb-3">
<div>
<h1>@project.Name</h1>
@if (!string.IsNullOrWhiteSpace(project.Customer))
{
<p class="text-muted mb-0">Customer: @project.Customer</p>
}
</div>
<div>
<a href="projects/@Id" class="btn btn-outline-secondary me-2">Edit Project</a>
<button class="btn btn-primary" @onclick="PrintReport">Print Report</button>
</div>
</div>
@if (!CanOptimize)
{
<div class="alert alert-warning">
<h4>Cannot Optimize</h4>
<ul class="mb-0">
@if (project.Parts.Count == 0)
{
<li>No parts defined. <a href="projects/@Id">Add parts to the project</a>.</li>
}
@if (project.CuttingToolId == null)
{
<li>No cutting tool selected. <a href="projects/@Id">Select a cutting tool</a>.</li>
}
</ul>
</div>
}
else if (packResult != null)
{
@if (summary!.TotalItemsNotPlaced > 0)
{
<div class="alert alert-warning">
<h5>Items Not Placed</h5>
<p>Some items could not be placed. This usually means no stock lengths are configured for the material, or parts are too long.</p>
</div>
}
<!-- Overall Summary Cards -->
<div class="row mb-4">
<div class="col-md-3 col-6 mb-3">
<div class="card text-center">
<div class="card-body">
<h2 class="card-title mb-0">@(summary.TotalInStockBins + summary.TotalToBePurchasedBins)</h2>
<p class="card-text text-muted">Total Stock Bars</p>
</div>
</div>
</div>
<div class="col-md-3 col-6 mb-3">
<div class="card text-center">
<div class="card-body">
<h2 class="card-title mb-0">@summary.TotalPieces</h2>
<p class="card-text text-muted">Total Pieces</p>
</div>
</div>
</div>
<div class="col-md-3 col-6 mb-3">
<div class="card text-center">
<div class="card-body">
<h2 class="card-title mb-0">@ArchUnits.FormatFromInches(summary.TotalWaste)</h2>
<p class="card-text text-muted">Total Waste</p>
</div>
</div>
</div>
<div class="col-md-3 col-6 mb-3">
<div class="card text-center">
<div class="card-body">
<h2 class="card-title mb-0">@summary.Efficiency.ToString("F1")%</h2>
<p class="card-text text-muted">Efficiency</p>
</div>
</div>
</div>
</div>
<!-- Stock Summary -->
<div class="row mb-4">
<div class="col-md-6 mb-3">
<div class="card border-success">
<div class="card-header bg-success text-white">
<h5 class="mb-0">In Stock</h5>
</div>
<div class="card-body">
<h3>@summary.TotalInStockBins bars</h3>
<p class="text-muted mb-0">Ready to cut from existing inventory</p>
</div>
</div>
</div>
<div class="col-md-6 mb-3">
<div class="card border-warning">
<div class="card-header bg-warning">
<h5 class="mb-0">To Be Purchased</h5>
</div>
<div class="card-body">
<h3>@summary.TotalToBePurchasedBins bars</h3>
<p class="text-muted mb-0">Need to order from supplier</p>
</div>
</div>
</div>
</div>
<!-- Results by Material -->
@foreach (var materialResult in packResult.MaterialResults)
{
var materialSummary = summary.MaterialSummaries.First(s => s.Material.Id == materialResult.Material.Id);
<div class="card mb-4">
<div class="card-header">
<h4 class="mb-0">@materialResult.Material.DisplayName</h4>
</div>
<div class="card-body">
<!-- Material Summary -->
<div class="row mb-3">
<div class="col-md-2 col-4">
<strong>@(materialSummary.InStockBins + materialSummary.ToBePurchasedBins)</strong> bars
</div>
<div class="col-md-2 col-4">
<strong>@materialSummary.TotalPieces</strong> pieces
</div>
<div class="col-md-2 col-4">
<strong>@materialSummary.Efficiency.ToString("F1")%</strong> efficiency
</div>
<div class="col-md-3 col-6">
<span class="text-success">@materialSummary.InStockBins in stock</span>
</div>
<div class="col-md-3 col-6">
<span class="text-warning">@materialSummary.ToBePurchasedBins to purchase</span>
</div>
</div>
@if (materialResult.PackResult.ItemsNotUsed.Count > 0)
{
<div class="alert alert-danger">
<strong>@materialResult.PackResult.ItemsNotUsed.Count items not placed</strong> -
No stock lengths available or parts too long.
</div>
}
@if (materialResult.InStockBins.Count > 0)
{
<h5 class="text-success mt-3">In Stock (@materialResult.InStockBins.Count bars)</h5>
@RenderBinList(materialResult.InStockBins)
}
@if (materialResult.ToBePurchasedBins.Count > 0)
{
<h5 class="text-warning mt-3">To Be Purchased (@materialResult.ToBePurchasedBins.Count bars)</h5>
@RenderBinList(materialResult.ToBePurchasedBins)
<!-- Purchase Summary -->
<div class="mt-3 p-3 bg-light rounded">
<strong>Order Summary:</strong>
<ul class="mb-0 mt-2">
@foreach (var group in materialResult.ToBePurchasedBins.GroupBy(b => b.Length).OrderByDescending(g => g.Key))
{
<li>@group.Count() x @ArchUnits.FormatFromInches(group.Key)</li>
}
</ul>
</div>
}
</div>
</div>
}
}
}
@code {
[Parameter]
public int Id { get; set; }
private Project? project;
private MultiMaterialPackResult? packResult;
private MultiMaterialPackingSummary? summary;
private bool loading = true;
private bool CanOptimize => project != null &&
project.Parts.Count > 0 &&
project.CuttingToolId != null;
protected override async Task OnInitializedAsync()
{
project = await ProjectService.GetByIdAsync(Id);
if (project != null && CanOptimize)
{
var kerf = project.CuttingTool?.KerfInches ?? 0.125m;
packResult = await PackingService.PackAsync(project.Parts, kerf);
summary = PackingService.GetSummary(packResult);
}
loading = false;
}
private RenderFragment RenderBinList(List<Bin> bins) => __builder =>
{
<div class="table-responsive">
<table class="table table-sm table-striped">
<thead>
<tr>
<th style="width: 80px;">#</th>
<th>Stock Length</th>
<th>Cuts</th>
<th>Waste</th>
</tr>
</thead>
<tbody>
@{ var binNumber = 1; }
@foreach (var bin in bins)
{
<tr>
<td>@binNumber</td>
<td>@ArchUnits.FormatFromInches(bin.Length)</td>
<td>
@foreach (var item in bin.Items)
{
<span class="badge bg-primary me-1">
@(string.IsNullOrWhiteSpace(item.Name) ? ArchUnits.FormatFromInches(item.Length) : $"{item.Name} ({ArchUnits.FormatFromInches(item.Length)})")
</span>
}
</td>
<td>@ArchUnits.FormatFromInches(bin.RemainingLength)</td>
</tr>
binNumber++;
}
</tbody>
</table>
</div>
};
private async Task PrintReport()
{
var filename = $"CutList - {project!.Name} - {DateTime.Now:yyyy-MM-dd}";
await JS.InvokeVoidAsync("printWithTitle", filename);
}
}

View File

@@ -0,0 +1,328 @@
@page "/suppliers/new"
@page "/suppliers/{Id:int}"
@inject SupplierService SupplierService
@inject MaterialService MaterialService
@inject NavigationManager Navigation
@using CutList.Core.Formatting
<PageTitle>@(IsNew ? "Add Supplier" : "Edit Supplier")</PageTitle>
<h1>@(IsNew ? "Add Supplier" : supplier.Name)</h1>
@if (loading)
{
<p><em>Loading...</em></p>
}
else
{
<div class="row">
<div class="col-lg-6 mb-4">
<div class="card">
<div class="card-header">
<h5 class="mb-0">Supplier Details</h5>
</div>
<div class="card-body">
<EditForm Model="supplier" OnValidSubmit="SaveSupplierAsync">
<DataAnnotationsValidator />
<div class="mb-3">
<label class="form-label">Name</label>
<InputText class="form-control" @bind-Value="supplier.Name" />
<ValidationMessage For="@(() => supplier.Name)" />
</div>
<div class="mb-3">
<label class="form-label">Contact Info</label>
<InputTextArea class="form-control" @bind-Value="supplier.ContactInfo" rows="2" placeholder="Phone, email, address..." />
</div>
<div class="mb-3">
<label class="form-label">Notes</label>
<InputTextArea class="form-control" @bind-Value="supplier.Notes" rows="3" />
</div>
@if (!string.IsNullOrEmpty(supplierErrorMessage))
{
<div class="alert alert-danger">@supplierErrorMessage</div>
}
<div class="d-flex gap-2">
<button type="submit" class="btn btn-primary" disabled="@savingSupplier">
@if (savingSupplier)
{
<span class="spinner-border spinner-border-sm me-1"></span>
}
@(IsNew ? "Create Supplier" : "Save Changes")
</button>
<a href="suppliers" class="btn btn-outline-secondary">@(IsNew ? "Cancel" : "Back to List")</a>
</div>
</EditForm>
</div>
</div>
</div>
@if (!IsNew)
{
<div class="col-lg-6">
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="mb-0">Stock Lengths</h5>
<button class="btn btn-sm btn-primary" @onclick="ShowAddStockForm">Add Stock</button>
</div>
<div class="card-body">
@if (showStockForm)
{
<div class="border rounded p-3 mb-3 bg-light">
<h6>@(editingStock == null ? "Add Stock Length" : "Edit Stock Length")</h6>
<div class="row g-2">
<div class="col-md-6">
<label class="form-label">Material</label>
<select class="form-select" @bind="newStock.MaterialId">
<option value="0">-- Select Material --</option>
@foreach (var material in materials)
{
<option value="@material.Id">@material.DisplayName</option>
}
</select>
</div>
<div class="col-md-6">
<label class="form-label">Length</label>
<LengthInput @bind-Value="newStock.LengthInches" />
</div>
<div class="col-md-6">
<label class="form-label">Price (optional)</label>
<InputNumber class="form-control" @bind-Value="newStock.Price" placeholder="0.00" />
</div>
<div class="col-md-6">
<label class="form-label">Notes</label>
<InputText class="form-control" @bind-Value="newStock.Notes" />
</div>
</div>
@if (!string.IsNullOrEmpty(stockErrorMessage))
{
<div class="alert alert-danger mt-2 mb-0">@stockErrorMessage</div>
}
<div class="mt-3 d-flex gap-2">
<button class="btn btn-primary btn-sm" @onclick="SaveStockAsync" disabled="@savingStock">
@if (savingStock)
{
<span class="spinner-border spinner-border-sm me-1"></span>
}
@(editingStock == null ? "Add" : "Save")
</button>
<button class="btn btn-outline-secondary btn-sm" @onclick="CancelStockForm">Cancel</button>
</div>
</div>
}
@if (stocks.Count == 0)
{
<p class="text-muted">No stock lengths configured yet.</p>
}
else
{
<table class="table table-sm">
<thead>
<tr>
<th>Material</th>
<th>Length</th>
<th>Price</th>
<th style="width: 100px;">Actions</th>
</tr>
</thead>
<tbody>
@foreach (var stock in stocks)
{
<tr>
<td>@stock.Material.DisplayName</td>
<td>@ArchUnits.FormatFromInches((double)stock.LengthInches)</td>
<td>@(stock.Price.HasValue ? stock.Price.Value.ToString("C") : "-")</td>
<td>
<button class="btn btn-sm btn-outline-primary" @onclick="() => EditStock(stock)">Edit</button>
<button class="btn btn-sm btn-outline-danger" @onclick="() => ConfirmDeleteStock(stock)">Delete</button>
</td>
</tr>
}
</tbody>
</table>
}
</div>
</div>
</div>
}
</div>
}
<ConfirmDialog @ref="deleteStockDialog"
Title="Delete Stock Length"
Message="@deleteStockMessage"
ConfirmText="Delete"
OnConfirm="DeleteStockConfirmed" />
@code {
[Parameter]
public int? Id { get; set; }
private Supplier supplier = new();
private List<SupplierStock> stocks = new();
private List<Material> materials = new();
private bool loading = true;
private bool savingSupplier;
private bool savingStock;
private string? supplierErrorMessage;
private string? stockErrorMessage;
private bool showStockForm;
private SupplierStock newStock = new();
private SupplierStock? editingStock;
private ConfirmDialog deleteStockDialog = null!;
private SupplierStock? stockToDelete;
private string deleteStockMessage = "";
private bool IsNew => !Id.HasValue;
protected override async Task OnInitializedAsync()
{
materials = await MaterialService.GetAllAsync();
if (Id.HasValue)
{
var existing = await SupplierService.GetByIdAsync(Id.Value);
if (existing == null)
{
Navigation.NavigateTo("suppliers");
return;
}
supplier = existing;
stocks = await SupplierService.GetStocksForSupplierAsync(Id.Value);
}
loading = false;
}
private async Task SaveSupplierAsync()
{
supplierErrorMessage = null;
savingSupplier = true;
try
{
if (string.IsNullOrWhiteSpace(supplier.Name))
{
supplierErrorMessage = "Name is required";
return;
}
if (IsNew)
{
var created = await SupplierService.CreateAsync(supplier);
Navigation.NavigateTo($"suppliers/{created.Id}");
}
else
{
await SupplierService.UpdateAsync(supplier);
}
}
finally
{
savingSupplier = false;
}
}
private void ShowAddStockForm()
{
editingStock = null;
newStock = new SupplierStock { SupplierId = Id!.Value };
showStockForm = true;
stockErrorMessage = null;
}
private void EditStock(SupplierStock stock)
{
editingStock = stock;
newStock = new SupplierStock
{
Id = stock.Id,
SupplierId = stock.SupplierId,
MaterialId = stock.MaterialId,
LengthInches = stock.LengthInches,
Price = stock.Price,
Notes = stock.Notes
};
showStockForm = true;
stockErrorMessage = null;
}
private void CancelStockForm()
{
showStockForm = false;
editingStock = null;
stockErrorMessage = null;
}
private async Task SaveStockAsync()
{
stockErrorMessage = null;
savingStock = true;
try
{
if (newStock.MaterialId == 0)
{
stockErrorMessage = "Please select a material";
return;
}
if (newStock.LengthInches <= 0)
{
stockErrorMessage = "Length must be greater than zero";
return;
}
var exists = await SupplierService.StockExistsAsync(
newStock.SupplierId,
newStock.MaterialId,
newStock.LengthInches,
editingStock?.Id);
if (exists)
{
stockErrorMessage = "This stock length already exists for this material";
return;
}
if (editingStock == null)
{
await SupplierService.AddStockAsync(newStock);
}
else
{
await SupplierService.UpdateStockAsync(newStock);
}
stocks = await SupplierService.GetStocksForSupplierAsync(Id!.Value);
showStockForm = false;
editingStock = null;
}
finally
{
savingStock = false;
}
}
private void ConfirmDeleteStock(SupplierStock stock)
{
stockToDelete = stock;
deleteStockMessage = $"Are you sure you want to delete the {ArchUnits.FormatFromInches((double)stock.LengthInches)} stock length?";
deleteStockDialog.Show();
}
private async Task DeleteStockConfirmed()
{
if (stockToDelete != null)
{
await SupplierService.DeleteStockAsync(stockToDelete.Id);
stocks = await SupplierService.GetStocksForSupplierAsync(Id!.Value);
}
}
}

View File

@@ -0,0 +1,91 @@
@page "/suppliers"
@inject SupplierService SupplierService
@inject NavigationManager Navigation
<PageTitle>Suppliers</PageTitle>
<div class="d-flex justify-content-between align-items-center mb-3">
<h1>Suppliers</h1>
<a href="suppliers/new" class="btn btn-primary">Add Supplier</a>
</div>
@if (loading)
{
<p><em>Loading...</em></p>
}
else if (suppliers.Count == 0)
{
<div class="alert alert-info">
No suppliers found. <a href="suppliers/new">Add your first supplier</a>.
</div>
}
else
{
<table class="table table-striped table-hover">
<thead>
<tr>
<th>Name</th>
<th>Contact Info</th>
<th>Notes</th>
<th style="width: 120px;">Actions</th>
</tr>
</thead>
<tbody>
@foreach (var supplier in suppliers)
{
<tr>
<td><a href="suppliers/@supplier.Id">@supplier.Name</a></td>
<td>@supplier.ContactInfo</td>
<td>@TruncateText(supplier.Notes, 50)</td>
<td>
<a href="suppliers/@supplier.Id" class="btn btn-sm btn-outline-primary">Edit</a>
<button class="btn btn-sm btn-outline-danger" @onclick="() => ConfirmDelete(supplier)">Delete</button>
</td>
</tr>
}
</tbody>
</table>
}
<ConfirmDialog @ref="deleteDialog"
Title="Delete Supplier"
Message="@deleteMessage"
ConfirmText="Delete"
OnConfirm="DeleteConfirmed" />
@code {
private List<Supplier> suppliers = new();
private bool loading = true;
private ConfirmDialog deleteDialog = null!;
private Supplier? supplierToDelete;
private string deleteMessage = "";
protected override async Task OnInitializedAsync()
{
suppliers = await SupplierService.GetAllAsync();
loading = false;
}
private void ConfirmDelete(Supplier supplier)
{
supplierToDelete = supplier;
deleteMessage = $"Are you sure you want to delete \"{supplier.Name}\"? This will also remove all stock lengths associated with this supplier.";
deleteDialog.Show();
}
private async Task DeleteConfirmed()
{
if (supplierToDelete != null)
{
await SupplierService.DeleteAsync(supplierToDelete.Id);
suppliers = await SupplierService.GetAllAsync();
}
}
private string? TruncateText(string? text, int maxLength)
{
if (string.IsNullOrEmpty(text) || text.Length <= maxLength)
return text;
return text.Substring(0, maxLength) + "...";
}
}

View File

@@ -0,0 +1,220 @@
@page "/tools"
@inject ProjectService ProjectService
@using CutList.Core.Formatting
<PageTitle>Cutting Tools</PageTitle>
<div class="d-flex justify-content-between align-items-center mb-3">
<h1>Cutting Tools</h1>
<button class="btn btn-primary" @onclick="ShowAddForm">Add Tool</button>
</div>
@if (loading)
{
<p><em>Loading...</em></p>
}
else
{
@if (showForm)
{
<div class="card mb-4">
<div class="card-header">
<h5 class="mb-0">@(editingTool == null ? "Add Cutting Tool" : "Edit Cutting Tool")</h5>
</div>
<div class="card-body">
<div class="row g-3">
<div class="col-md-4">
<label class="form-label">Name</label>
<input type="text" class="form-control" @bind="formTool.Name" placeholder="e.g., Bandsaw" />
</div>
<div class="col-md-4">
<label class="form-label">Kerf Width (inches)</label>
<input type="number" step="0.0001" class="form-control" @bind="formTool.KerfInches" />
<div class="form-text">Common: 1/16" = 0.0625, 1/8" = 0.125</div>
</div>
<div class="col-md-4">
<label class="form-label">Default</label>
<div class="form-check mt-2">
<input type="checkbox" class="form-check-input" id="isDefault" @bind="formTool.IsDefault" />
<label class="form-check-label" for="isDefault">Set as default tool</label>
</div>
</div>
</div>
@if (!string.IsNullOrEmpty(errorMessage))
{
<div class="alert alert-danger mt-3">@errorMessage</div>
}
<div class="mt-3 d-flex gap-2">
<button class="btn btn-primary" @onclick="SaveAsync" disabled="@saving">
@if (saving)
{
<span class="spinner-border spinner-border-sm me-1"></span>
}
@(editingTool == null ? "Add Tool" : "Save Changes")
</button>
<button class="btn btn-outline-secondary" @onclick="CancelForm">Cancel</button>
</div>
</div>
</div>
}
@if (tools.Count == 0)
{
<div class="alert alert-info">
No cutting tools found. Add your first cutting tool to get started.
</div>
}
else
{
<table class="table table-striped table-hover">
<thead>
<tr>
<th>Name</th>
<th>Kerf Width</th>
<th>Default</th>
<th style="width: 140px;">Actions</th>
</tr>
</thead>
<tbody>
@foreach (var tool in tools)
{
<tr>
<td>@tool.Name</td>
<td>@FormatKerf(tool.KerfInches)</td>
<td>
@if (tool.IsDefault)
{
<span class="badge bg-primary">Default</span>
}
</td>
<td>
<button class="btn btn-sm btn-outline-primary" @onclick="() => Edit(tool)">Edit</button>
<button class="btn btn-sm btn-outline-danger" @onclick="() => ConfirmDelete(tool)">Delete</button>
</td>
</tr>
}
</tbody>
</table>
}
}
<ConfirmDialog @ref="deleteDialog"
Title="Delete Cutting Tool"
Message="@deleteMessage"
ConfirmText="Delete"
OnConfirm="DeleteConfirmed" />
@code {
private List<CuttingTool> tools = new();
private bool loading = true;
private bool showForm;
private bool saving;
private string? errorMessage;
private CuttingTool formTool = new();
private CuttingTool? editingTool;
private ConfirmDialog deleteDialog = null!;
private CuttingTool? toolToDelete;
private string deleteMessage = "";
protected override async Task OnInitializedAsync()
{
tools = await ProjectService.GetCuttingToolsAsync();
loading = false;
}
private void ShowAddForm()
{
editingTool = null;
formTool = new CuttingTool { KerfInches = 0.125m };
showForm = true;
errorMessage = null;
}
private void Edit(CuttingTool tool)
{
editingTool = tool;
formTool = new CuttingTool
{
Id = tool.Id,
Name = tool.Name,
KerfInches = tool.KerfInches,
IsDefault = tool.IsDefault,
IsActive = tool.IsActive
};
showForm = true;
errorMessage = null;
}
private void CancelForm()
{
showForm = false;
editingTool = null;
errorMessage = null;
}
private async Task SaveAsync()
{
errorMessage = null;
saving = true;
try
{
if (string.IsNullOrWhiteSpace(formTool.Name))
{
errorMessage = "Name is required";
return;
}
if (formTool.KerfInches < 0)
{
errorMessage = "Kerf width cannot be negative";
return;
}
if (editingTool == null)
{
await ProjectService.CreateCuttingToolAsync(formTool);
}
else
{
await ProjectService.UpdateCuttingToolAsync(formTool);
}
tools = await ProjectService.GetCuttingToolsAsync();
showForm = false;
editingTool = null;
}
finally
{
saving = false;
}
}
private void ConfirmDelete(CuttingTool tool)
{
toolToDelete = tool;
deleteMessage = $"Are you sure you want to delete \"{tool.Name}\"?";
deleteDialog.Show();
}
private async Task DeleteConfirmed()
{
if (toolToDelete != null)
{
await ProjectService.DeleteCuttingToolAsync(toolToDelete.Id);
tools = await ProjectService.GetCuttingToolsAsync();
}
}
private string FormatKerf(decimal kerf)
{
// Show as fraction if it's a common value
var inches = (double)kerf;
var formatted = FormatHelper.ConvertToMixedFraction(inches);
return $"{formatted}\" ({kerf:0.####}\")";
}
}

View File

@@ -0,0 +1,6 @@
<Router AppAssembly="typeof(Program).Assembly">
<Found Context="routeData">
<RouteView RouteData="routeData" DefaultLayout="typeof(Layout.MainLayout)" />
<FocusOnNavigate RouteData="routeData" Selector="h1" />
</Found>
</Router>

View File

@@ -0,0 +1,66 @@
@if (IsVisible)
{
<div class="modal fade show d-block" tabindex="-1" style="background-color: rgba(0,0,0,0.5);">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">@Title</h5>
<button type="button" class="btn-close" @onclick="Cancel"></button>
</div>
<div class="modal-body">
<p>@Message</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" @onclick="Cancel">Cancel</button>
<button type="button" class="btn @ConfirmButtonClass" @onclick="Confirm">@ConfirmText</button>
</div>
</div>
</div>
</div>
}
@code {
[Parameter]
public string Title { get; set; } = "Confirm";
[Parameter]
public string Message { get; set; } = "Are you sure?";
[Parameter]
public string ConfirmText { get; set; } = "Confirm";
[Parameter]
public string ConfirmButtonClass { get; set; } = "btn-danger";
[Parameter]
public EventCallback OnConfirm { get; set; }
[Parameter]
public EventCallback OnCancel { get; set; }
public bool IsVisible { get; private set; }
public void Show()
{
IsVisible = true;
StateHasChanged();
}
public void Hide()
{
IsVisible = false;
StateHasChanged();
}
private async Task Confirm()
{
Hide();
await OnConfirm.InvokeAsync();
}
private async Task Cancel()
{
Hide();
await OnCancel.InvokeAsync();
}
}

View File

@@ -0,0 +1,88 @@
@using CutList.Core
@using CutList.Core.Nesting
@using CutList.Core.Formatting
@inject ReportService ReportService
<div class="cut-list-report">
<header class="report-header">
<h1>CUT LIST</h1>
<div class="meta-info">
<div class="meta-row"><span>Date:</span> @DateTime.Now.ToString("g")</div>
<div class="meta-row"><span>Project:</span> @Project.Name</div>
@if (!string.IsNullOrWhiteSpace(Project.Customer))
{
<div class="meta-row"><span>Customer:</span> @Project.Customer</div>
}
@if (Project.CuttingTool != null)
{
<div class="meta-row"><span>Cut Method:</span> @Project.CuttingTool.Name (kerf: @Project.CuttingTool.KerfInches")</div>
}
<div class="meta-row"><span>Stock Bars:</span> @PackResult.Bins.Count</div>
<div class="meta-row"><span>Total Pieces:</span> @TotalPieces</div>
</div>
</header>
@foreach (var (bin, index) in PackResult.Bins.Select((b, i) => (b, i + 1)))
{
<section class="bin-section">
<h2>BAR #@index - Length: @ReportService.FormatLength(bin.Length)</h2>
<table class="cuts-table">
<thead>
<tr>
<th style="width: 60px;">Qty</th>
<th style="width: 120px;">Length</th>
<th>Label</th>
</tr>
</thead>
<tbody>
@foreach (var group in ReportService.GroupItems(bin.Items))
{
<tr>
<td>@group.Count</td>
<td>@ReportService.FormatLength(group.Length)</td>
<td>@group.Name</td>
</tr>
}
</tbody>
</table>
<div class="drop">
Remaining drop: @ReportService.FormatLength(bin.RemainingLength)
(@((bin.Utilization * 100).ToString("F1"))% utilization)
</div>
</section>
}
<footer class="summary">
<h2>SUMMARY</h2>
<div class="summary-grid">
<div class="summary-row"><span>Stock Bars Needed:</span> <strong>@PackResult.Bins.Count</strong></div>
<div class="summary-row"><span>Total Pieces:</span> <strong>@TotalPieces</strong></div>
<div class="summary-row"><span>Total Material:</span> <strong>@ReportService.FormatLength(TotalMaterial)</strong></div>
<div class="summary-row"><span>Total Used:</span> <strong>@ReportService.FormatLength(TotalUsed)</strong></div>
<div class="summary-row"><span>Total Waste:</span> <strong>@ReportService.FormatLength(TotalWaste)</strong></div>
<div class="summary-row"><span>Efficiency:</span> <strong>@Efficiency.ToString("F1")%</strong></div>
</div>
</footer>
@if (!string.IsNullOrEmpty(Project.Notes))
{
<div class="notes-section">
<h3>Notes</h3>
<p>@Project.Notes</p>
</div>
}
</div>
@code {
[Parameter, EditorRequired]
public Project Project { get; set; } = null!;
[Parameter, EditorRequired]
public PackResult PackResult { get; set; } = null!;
private int TotalPieces => PackResult.Bins.Sum(b => b.Items.Count);
private double TotalMaterial => PackResult.Bins.Sum(b => b.Length);
private double TotalUsed => PackResult.Bins.Sum(b => b.UsedLength);
private double TotalWaste => PackResult.Bins.Sum(b => b.RemainingLength);
private double Efficiency => TotalMaterial > 0 ? TotalUsed / TotalMaterial * 100 : 0;
}

View File

@@ -0,0 +1,75 @@
@using CutList.Core.Formatting
<div class="length-input">
<input type="text"
class="form-control @(HasError ? "is-invalid" : "")"
value="@DisplayValue"
@onchange="OnInputChange"
placeholder="@Placeholder" />
@if (HasError)
{
<div class="invalid-feedback">@ErrorMessage</div>
}
</div>
@code {
[Parameter]
public decimal Value { get; set; }
[Parameter]
public EventCallback<decimal> ValueChanged { get; set; }
[Parameter]
public string Placeholder { get; set; } = "e.g., 12' 6\" or 144";
private string DisplayValue { get; set; } = string.Empty;
private bool HasError { get; set; }
private string ErrorMessage { get; set; } = string.Empty;
protected override void OnParametersSet()
{
if (Value > 0 && string.IsNullOrEmpty(DisplayValue))
{
DisplayValue = ArchUnits.FormatFromInches((double)Value);
}
}
private async Task OnInputChange(ChangeEventArgs e)
{
var input = e.Value?.ToString() ?? string.Empty;
DisplayValue = input;
HasError = false;
ErrorMessage = string.Empty;
if (string.IsNullOrWhiteSpace(input))
{
await ValueChanged.InvokeAsync(0);
return;
}
try
{
// Try to parse as architectural units
var inches = ArchUnits.ParseToInches(input);
await ValueChanged.InvokeAsync((decimal)inches);
}
catch
{
// Try to parse as plain decimal (inches)
if (decimal.TryParse(input, out var decimalValue))
{
await ValueChanged.InvokeAsync(decimalValue);
}
else
{
HasError = true;
ErrorMessage = "Invalid format. Use feet (12'), inches (6\"), or decimal (144)";
}
}
}
public static string FormatLength(decimal inches)
{
return ArchUnits.FormatFromInches((double)inches);
}
}

View File

@@ -0,0 +1,14 @@
@using System.Net.Http
@using System.Net.Http.Json
@using Microsoft.AspNetCore.Components.Forms
@using Microsoft.AspNetCore.Components.Routing
@using Microsoft.AspNetCore.Components.Web
@using static Microsoft.AspNetCore.Components.Web.RenderMode
@using Microsoft.AspNetCore.Components.Web.Virtualization
@using Microsoft.JSInterop
@using CutList.Web
@using CutList.Web.Components
@using CutList.Web.Components.Shared
@using CutList.Web.Data
@using CutList.Web.Data.Entities
@using CutList.Web.Services

View File

@@ -0,0 +1,170 @@
using CutList.Web.Data;
using CutList.Web.Data.Entities;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
namespace CutList.Web.Controllers;
[ApiController]
[Route("api/[controller]")]
public class MaterialsController : ControllerBase
{
private readonly ApplicationDbContext _context;
public MaterialsController(ApplicationDbContext context)
{
_context = context;
}
[HttpGet]
public async Task<ActionResult<IEnumerable<MaterialDto>>> GetMaterials()
{
var materials = await _context.Materials
.Where(m => m.IsActive)
.OrderBy(m => m.Shape)
.ThenBy(m => m.Size)
.Select(m => new MaterialDto
{
Id = m.Id,
Shape = m.Shape,
Size = m.Size,
Description = m.Description
})
.ToListAsync();
return Ok(materials);
}
[HttpGet("{id}")]
public async Task<ActionResult<MaterialDto>> GetMaterial(int id)
{
var material = await _context.Materials.FindAsync(id);
if (material == null || !material.IsActive)
return NotFound();
return Ok(new MaterialDto
{
Id = material.Id,
Shape = material.Shape,
Size = material.Size,
Description = material.Description
});
}
[HttpPost]
public async Task<ActionResult<MaterialDto>> CreateMaterial(CreateMaterialDto dto)
{
if (string.IsNullOrWhiteSpace(dto.Shape))
return BadRequest("Shape is required");
if (string.IsNullOrWhiteSpace(dto.Size))
return BadRequest("Size is required");
// Check for duplicates
var exists = await _context.Materials
.AnyAsync(m => m.Shape == dto.Shape && m.Size == dto.Size && m.IsActive);
if (exists)
return Conflict($"Material '{dto.Shape} - {dto.Size}' already exists");
var material = new Material
{
Shape = dto.Shape,
Size = dto.Size,
Description = dto.Description,
CreatedAt = DateTime.UtcNow
};
_context.Materials.Add(material);
await _context.SaveChangesAsync();
return CreatedAtAction(nameof(GetMaterial), new { id = material.Id }, new MaterialDto
{
Id = material.Id,
Shape = material.Shape,
Size = material.Size,
Description = material.Description
});
}
[HttpPost("bulk")]
public async Task<ActionResult<BulkCreateResult>> CreateMaterialsBulk(List<CreateMaterialDto> materials)
{
var created = 0;
var skipped = 0;
var errors = new List<string>();
foreach (var dto in materials)
{
if (string.IsNullOrWhiteSpace(dto.Shape) || string.IsNullOrWhiteSpace(dto.Size))
{
errors.Add($"Invalid material: Shape and Size are required");
continue;
}
var exists = await _context.Materials
.AnyAsync(m => m.Shape == dto.Shape && m.Size == dto.Size && m.IsActive);
if (exists)
{
skipped++;
continue;
}
_context.Materials.Add(new Material
{
Shape = dto.Shape,
Size = dto.Size,
Description = dto.Description,
CreatedAt = DateTime.UtcNow
});
created++;
}
await _context.SaveChangesAsync();
return Ok(new BulkCreateResult
{
Created = created,
Skipped = skipped,
Errors = errors
});
}
[HttpDelete("{id}")]
public async Task<IActionResult> DeleteMaterial(int id)
{
var material = await _context.Materials.FindAsync(id);
if (material == null)
return NotFound();
material.IsActive = false;
await _context.SaveChangesAsync();
return NoContent();
}
}
public class MaterialDto
{
public int Id { get; set; }
public string Shape { get; set; } = string.Empty;
public string Size { get; set; } = string.Empty;
public string? Description { get; set; }
}
public class CreateMaterialDto
{
public string Shape { get; set; } = string.Empty;
public string Size { get; set; } = string.Empty;
public string? Description { get; set; }
}
public class BulkCreateResult
{
public int Created { get; set; }
public int Skipped { get; set; }
public List<string> Errors { get; set; } = new();
}

View File

@@ -0,0 +1,94 @@
using CutList.Web.Data;
using CutList.Web.Data.Entities;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
namespace CutList.Web.Controllers;
[ApiController]
[Route("api/[controller]")]
public class SeedController : ControllerBase
{
private readonly ApplicationDbContext _context;
public SeedController(ApplicationDbContext context)
{
_context = context;
}
[HttpPost("alro-1018-round")]
public async Task<ActionResult> SeedAlro1018Round()
{
// Add Alro supplier if not exists
var alro = await _context.Suppliers.FirstOrDefaultAsync(s => s.Name == "Alro");
if (alro == null)
{
alro = new Supplier
{
Name = "Alro",
ContactInfo = "https://www.alro.com",
CreatedAt = DateTime.UtcNow
};
_context.Suppliers.Add(alro);
await _context.SaveChangesAsync();
}
// 1018 CF Round bar sizes from the screenshot
var sizes = new[]
{
"1/8\"",
"5/32\"",
"3/16\"",
"7/32\"",
".236\"",
"1/4\"",
"9/32\"",
"5/16\"",
"11/32\"",
"3/8\"",
".394\"",
"13/32\"",
"7/16\"",
"15/32\"",
".472\"",
"1/2\"",
"17/32\"",
"9/16\"",
".593\""
};
var created = 0;
var skipped = 0;
foreach (var size in sizes)
{
var exists = await _context.Materials
.AnyAsync(m => m.Shape == "Round Bar" && m.Size == size && m.IsActive);
if (exists)
{
skipped++;
continue;
}
_context.Materials.Add(new Material
{
Shape = "Round Bar",
Size = size,
Description = "1018 Cold Finished",
CreatedAt = DateTime.UtcNow
});
created++;
}
await _context.SaveChangesAsync();
return Ok(new
{
Message = "Alro 1018 CF Round materials seeded",
SupplierId = alro.Id,
MaterialsCreated = created,
MaterialsSkipped = skipped
});
}
}

View File

@@ -0,0 +1,21 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\CutList.Core\CutList.Core.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="8.0.11" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="8.0.11">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
</Project>

View File

@@ -0,0 +1,128 @@
using CutList.Web.Data.Entities;
using Microsoft.EntityFrameworkCore;
namespace CutList.Web.Data;
public class ApplicationDbContext : DbContext
{
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
: base(options)
{
}
public DbSet<Material> Materials => Set<Material>();
public DbSet<MaterialStockLength> MaterialStockLengths => Set<MaterialStockLength>();
public DbSet<Supplier> Suppliers => Set<Supplier>();
public DbSet<SupplierStock> SupplierStocks => Set<SupplierStock>();
public DbSet<CuttingTool> CuttingTools => Set<CuttingTool>();
public DbSet<Project> Projects => Set<Project>();
public DbSet<ProjectPart> ProjectParts => Set<ProjectPart>();
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
// Material
modelBuilder.Entity<Material>(entity =>
{
entity.HasKey(e => e.Id);
entity.Property(e => e.Shape).HasMaxLength(50).IsRequired();
entity.Property(e => e.Size).HasMaxLength(100).IsRequired();
entity.Property(e => e.Description).HasMaxLength(255);
entity.Property(e => e.CreatedAt).HasDefaultValueSql("GETUTCDATE()");
});
// MaterialStockLength
modelBuilder.Entity<MaterialStockLength>(entity =>
{
entity.HasKey(e => e.Id);
entity.Property(e => e.LengthInches).HasPrecision(10, 4);
entity.Property(e => e.Notes).HasMaxLength(255);
entity.HasOne(e => e.Material)
.WithMany(m => m.StockLengths)
.HasForeignKey(e => e.MaterialId)
.OnDelete(DeleteBehavior.Cascade);
entity.HasIndex(e => new { e.MaterialId, e.LengthInches }).IsUnique();
});
// Supplier
modelBuilder.Entity<Supplier>(entity =>
{
entity.HasKey(e => e.Id);
entity.Property(e => e.Name).HasMaxLength(100).IsRequired();
entity.Property(e => e.ContactInfo).HasMaxLength(500);
entity.Property(e => e.CreatedAt).HasDefaultValueSql("GETUTCDATE()");
});
// SupplierStock
modelBuilder.Entity<SupplierStock>(entity =>
{
entity.HasKey(e => e.Id);
entity.Property(e => e.LengthInches).HasPrecision(10, 4);
entity.Property(e => e.Price).HasPrecision(10, 2);
entity.Property(e => e.Notes).HasMaxLength(255);
entity.HasOne(e => e.Supplier)
.WithMany(s => s.Stocks)
.HasForeignKey(e => e.SupplierId)
.OnDelete(DeleteBehavior.Cascade);
entity.HasOne(e => e.Material)
.WithMany(m => m.SupplierStocks)
.HasForeignKey(e => e.MaterialId)
.OnDelete(DeleteBehavior.Cascade);
entity.HasIndex(e => new { e.SupplierId, e.MaterialId, e.LengthInches }).IsUnique();
});
// CuttingTool
modelBuilder.Entity<CuttingTool>(entity =>
{
entity.HasKey(e => e.Id);
entity.Property(e => e.Name).HasMaxLength(50).IsRequired();
entity.Property(e => e.KerfInches).HasPrecision(6, 4);
});
// Project
modelBuilder.Entity<Project>(entity =>
{
entity.HasKey(e => e.Id);
entity.Property(e => e.Name).HasMaxLength(100).IsRequired();
entity.Property(e => e.Customer).HasMaxLength(100);
entity.Property(e => e.CreatedAt).HasDefaultValueSql("GETUTCDATE()");
entity.HasOne(e => e.CuttingTool)
.WithMany(t => t.Projects)
.HasForeignKey(e => e.CuttingToolId)
.OnDelete(DeleteBehavior.SetNull);
});
// ProjectPart
modelBuilder.Entity<ProjectPart>(entity =>
{
entity.HasKey(e => e.Id);
entity.Property(e => e.Name).HasMaxLength(100);
entity.Property(e => e.LengthInches).HasPrecision(10, 4);
entity.HasOne(e => e.Project)
.WithMany(p => p.Parts)
.HasForeignKey(e => e.ProjectId)
.OnDelete(DeleteBehavior.Cascade);
entity.HasOne(e => e.Material)
.WithMany(m => m.ProjectParts)
.HasForeignKey(e => e.MaterialId)
.OnDelete(DeleteBehavior.Restrict);
});
// Seed default cutting tools
modelBuilder.Entity<CuttingTool>().HasData(
new CuttingTool { Id = 1, Name = "Bandsaw", KerfInches = 0.0625m, IsDefault = true, IsActive = true },
new CuttingTool { Id = 2, Name = "Chop Saw", KerfInches = 0.125m, IsDefault = false, IsActive = true },
new CuttingTool { Id = 3, Name = "Cold Cut Saw", KerfInches = 0.0625m, IsDefault = false, IsActive = true },
new CuttingTool { Id = 4, Name = "Hacksaw", KerfInches = 0.0625m, IsDefault = false, IsActive = true }
);
}
}

View File

@@ -0,0 +1,12 @@
namespace CutList.Web.Data.Entities;
public class CuttingTool
{
public int Id { get; set; }
public string Name { get; set; } = string.Empty;
public decimal KerfInches { get; set; }
public bool IsDefault { get; set; }
public bool IsActive { get; set; } = true;
public ICollection<Project> Projects { get; set; } = new List<Project>();
}

View File

@@ -0,0 +1,18 @@
namespace CutList.Web.Data.Entities;
public class Material
{
public int Id { get; set; }
public string Shape { get; set; } = string.Empty;
public string Size { get; set; } = string.Empty;
public string? Description { get; set; }
public bool IsActive { get; set; } = true;
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
public DateTime? UpdatedAt { get; set; }
public ICollection<SupplierStock> SupplierStocks { get; set; } = new List<SupplierStock>();
public ICollection<MaterialStockLength> StockLengths { get; set; } = new List<MaterialStockLength>();
public ICollection<ProjectPart> ProjectParts { get; set; } = new List<ProjectPart>();
public string DisplayName => $"{Shape} - {Size}";
}

View File

@@ -0,0 +1,13 @@
namespace CutList.Web.Data.Entities;
public class MaterialStockLength
{
public int Id { get; set; }
public int MaterialId { get; set; }
public decimal LengthInches { get; set; }
public int Quantity { get; set; } = 0;
public string? Notes { get; set; }
public bool IsActive { get; set; } = true;
public Material Material { get; set; } = null!;
}

View File

@@ -0,0 +1,15 @@
namespace CutList.Web.Data.Entities;
public class Project
{
public int Id { get; set; }
public string Name { get; set; } = string.Empty;
public string? Customer { get; set; }
public int? CuttingToolId { get; set; }
public string? Notes { get; set; }
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
public DateTime? UpdatedAt { get; set; }
public CuttingTool? CuttingTool { get; set; }
public ICollection<ProjectPart> Parts { get; set; } = new List<ProjectPart>();
}

View File

@@ -0,0 +1,15 @@
namespace CutList.Web.Data.Entities;
public class ProjectPart
{
public int Id { get; set; }
public int ProjectId { get; set; }
public int MaterialId { get; set; }
public string Name { get; set; } = string.Empty;
public decimal LengthInches { get; set; }
public int Quantity { get; set; } = 1;
public int SortOrder { get; set; }
public Project Project { get; set; } = null!;
public Material Material { get; set; } = null!;
}

View File

@@ -0,0 +1,13 @@
namespace CutList.Web.Data.Entities;
public class Supplier
{
public int Id { get; set; }
public string Name { get; set; } = string.Empty;
public string? ContactInfo { get; set; }
public string? Notes { get; set; }
public bool IsActive { get; set; } = true;
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
public ICollection<SupplierStock> Stocks { get; set; } = new List<SupplierStock>();
}

View File

@@ -0,0 +1,15 @@
namespace CutList.Web.Data.Entities;
public class SupplierStock
{
public int Id { get; set; }
public int SupplierId { get; set; }
public int MaterialId { get; set; }
public decimal LengthInches { get; set; }
public decimal? Price { get; set; }
public string? Notes { get; set; }
public bool IsActive { get; set; } = true;
public Supplier Supplier { get; set; } = null!;
public Material Material { get; set; } = null!;
}

View File

@@ -0,0 +1,387 @@
// <auto-generated />
using System;
using CutList.Web.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace CutList.Web.Migrations
{
[DbContext(typeof(ApplicationDbContext))]
[Migration("20260202024820_InitialCreate")]
partial class InitialCreate
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "8.0.11")
.HasAnnotation("Relational:MaxIdentifierLength", 128);
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
modelBuilder.Entity("CutList.Web.Data.Entities.CuttingTool", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<bool>("IsActive")
.HasColumnType("bit");
b.Property<bool>("IsDefault")
.HasColumnType("bit");
b.Property<decimal>("KerfInches")
.HasPrecision(6, 4)
.HasColumnType("decimal(6,4)");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("nvarchar(50)");
b.HasKey("Id");
b.ToTable("CuttingTools");
b.HasData(
new
{
Id = 1,
IsActive = true,
IsDefault = true,
KerfInches = 0.0625m,
Name = "Bandsaw"
},
new
{
Id = 2,
IsActive = true,
IsDefault = false,
KerfInches = 0.125m,
Name = "Chop Saw"
},
new
{
Id = 3,
IsActive = true,
IsDefault = false,
KerfInches = 0.0625m,
Name = "Cold Cut Saw"
},
new
{
Id = 4,
IsActive = true,
IsDefault = false,
KerfInches = 0.0625m,
Name = "Hacksaw"
});
});
modelBuilder.Entity("CutList.Web.Data.Entities.Material", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<DateTime>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("datetime2")
.HasDefaultValueSql("GETUTCDATE()");
b.Property<string>("Description")
.HasMaxLength(255)
.HasColumnType("nvarchar(255)");
b.Property<bool>("IsActive")
.HasColumnType("bit");
b.Property<string>("Shape")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("nvarchar(50)");
b.Property<string>("Size")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<DateTime?>("UpdatedAt")
.HasColumnType("datetime2");
b.HasKey("Id");
b.ToTable("Materials");
});
modelBuilder.Entity("CutList.Web.Data.Entities.Project", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<DateTime>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("datetime2")
.HasDefaultValueSql("GETUTCDATE()");
b.Property<int?>("CuttingToolId")
.HasColumnType("int");
b.Property<int?>("MaterialId")
.HasColumnType("int");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<string>("Notes")
.HasColumnType("nvarchar(max)");
b.Property<DateTime?>("UpdatedAt")
.HasColumnType("datetime2");
b.HasKey("Id");
b.HasIndex("CuttingToolId");
b.HasIndex("MaterialId");
b.ToTable("Projects");
});
modelBuilder.Entity("CutList.Web.Data.Entities.ProjectPart", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<decimal>("LengthInches")
.HasPrecision(10, 4)
.HasColumnType("decimal(10,4)");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<int>("ProjectId")
.HasColumnType("int");
b.Property<int>("Quantity")
.HasColumnType("int");
b.Property<int>("SortOrder")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("ProjectId");
b.ToTable("ProjectParts");
});
modelBuilder.Entity("CutList.Web.Data.Entities.ProjectStockBin", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<decimal>("LengthInches")
.HasPrecision(10, 4)
.HasColumnType("decimal(10,4)");
b.Property<int>("Priority")
.HasColumnType("int");
b.Property<int>("ProjectId")
.HasColumnType("int");
b.Property<int>("Quantity")
.HasColumnType("int");
b.Property<int>("SortOrder")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("ProjectId");
b.ToTable("ProjectStockBins");
});
modelBuilder.Entity("CutList.Web.Data.Entities.Supplier", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<string>("ContactInfo")
.HasMaxLength(500)
.HasColumnType("nvarchar(500)");
b.Property<DateTime>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("datetime2")
.HasDefaultValueSql("GETUTCDATE()");
b.Property<bool>("IsActive")
.HasColumnType("bit");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<string>("Notes")
.HasColumnType("nvarchar(max)");
b.HasKey("Id");
b.ToTable("Suppliers");
});
modelBuilder.Entity("CutList.Web.Data.Entities.SupplierStock", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<bool>("IsActive")
.HasColumnType("bit");
b.Property<decimal>("LengthInches")
.HasPrecision(10, 4)
.HasColumnType("decimal(10,4)");
b.Property<int>("MaterialId")
.HasColumnType("int");
b.Property<string>("Notes")
.HasMaxLength(255)
.HasColumnType("nvarchar(255)");
b.Property<decimal?>("Price")
.HasPrecision(10, 2)
.HasColumnType("decimal(10,2)");
b.Property<int>("SupplierId")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("MaterialId");
b.HasIndex("SupplierId", "MaterialId", "LengthInches")
.IsUnique();
b.ToTable("SupplierStocks");
});
modelBuilder.Entity("CutList.Web.Data.Entities.Project", b =>
{
b.HasOne("CutList.Web.Data.Entities.CuttingTool", "CuttingTool")
.WithMany("Projects")
.HasForeignKey("CuttingToolId")
.OnDelete(DeleteBehavior.SetNull);
b.HasOne("CutList.Web.Data.Entities.Material", "Material")
.WithMany("Projects")
.HasForeignKey("MaterialId")
.OnDelete(DeleteBehavior.SetNull);
b.Navigation("CuttingTool");
b.Navigation("Material");
});
modelBuilder.Entity("CutList.Web.Data.Entities.ProjectPart", b =>
{
b.HasOne("CutList.Web.Data.Entities.Project", "Project")
.WithMany("Parts")
.HasForeignKey("ProjectId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Project");
});
modelBuilder.Entity("CutList.Web.Data.Entities.ProjectStockBin", b =>
{
b.HasOne("CutList.Web.Data.Entities.Project", "Project")
.WithMany("StockBins")
.HasForeignKey("ProjectId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Project");
});
modelBuilder.Entity("CutList.Web.Data.Entities.SupplierStock", b =>
{
b.HasOne("CutList.Web.Data.Entities.Material", "Material")
.WithMany("SupplierStocks")
.HasForeignKey("MaterialId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("CutList.Web.Data.Entities.Supplier", "Supplier")
.WithMany("Stocks")
.HasForeignKey("SupplierId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Material");
b.Navigation("Supplier");
});
modelBuilder.Entity("CutList.Web.Data.Entities.CuttingTool", b =>
{
b.Navigation("Projects");
});
modelBuilder.Entity("CutList.Web.Data.Entities.Material", b =>
{
b.Navigation("Projects");
b.Navigation("SupplierStocks");
});
modelBuilder.Entity("CutList.Web.Data.Entities.Project", b =>
{
b.Navigation("Parts");
b.Navigation("StockBins");
});
modelBuilder.Entity("CutList.Web.Data.Entities.Supplier", b =>
{
b.Navigation("Stocks");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,241 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
#pragma warning disable CA1814 // Prefer jagged arrays over multidimensional
namespace CutList.Web.Migrations
{
/// <inheritdoc />
public partial class InitialCreate : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "CuttingTools",
columns: table => new
{
Id = table.Column<int>(type: "int", nullable: false)
.Annotation("SqlServer:Identity", "1, 1"),
Name = table.Column<string>(type: "nvarchar(50)", maxLength: 50, nullable: false),
KerfInches = table.Column<decimal>(type: "decimal(6,4)", precision: 6, scale: 4, nullable: false),
IsDefault = table.Column<bool>(type: "bit", nullable: false),
IsActive = table.Column<bool>(type: "bit", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_CuttingTools", x => x.Id);
});
migrationBuilder.CreateTable(
name: "Materials",
columns: table => new
{
Id = table.Column<int>(type: "int", nullable: false)
.Annotation("SqlServer:Identity", "1, 1"),
Shape = table.Column<string>(type: "nvarchar(50)", maxLength: 50, nullable: false),
Size = table.Column<string>(type: "nvarchar(100)", maxLength: 100, nullable: false),
Description = table.Column<string>(type: "nvarchar(255)", maxLength: 255, nullable: true),
IsActive = table.Column<bool>(type: "bit", nullable: false),
CreatedAt = table.Column<DateTime>(type: "datetime2", nullable: false, defaultValueSql: "GETUTCDATE()"),
UpdatedAt = table.Column<DateTime>(type: "datetime2", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_Materials", x => x.Id);
});
migrationBuilder.CreateTable(
name: "Suppliers",
columns: table => new
{
Id = table.Column<int>(type: "int", nullable: false)
.Annotation("SqlServer:Identity", "1, 1"),
Name = table.Column<string>(type: "nvarchar(100)", maxLength: 100, nullable: false),
ContactInfo = table.Column<string>(type: "nvarchar(500)", maxLength: 500, nullable: true),
Notes = table.Column<string>(type: "nvarchar(max)", nullable: true),
IsActive = table.Column<bool>(type: "bit", nullable: false),
CreatedAt = table.Column<DateTime>(type: "datetime2", nullable: false, defaultValueSql: "GETUTCDATE()")
},
constraints: table =>
{
table.PrimaryKey("PK_Suppliers", x => x.Id);
});
migrationBuilder.CreateTable(
name: "Projects",
columns: table => new
{
Id = table.Column<int>(type: "int", nullable: false)
.Annotation("SqlServer:Identity", "1, 1"),
Name = table.Column<string>(type: "nvarchar(100)", maxLength: 100, nullable: false),
MaterialId = table.Column<int>(type: "int", nullable: true),
CuttingToolId = table.Column<int>(type: "int", nullable: true),
Notes = table.Column<string>(type: "nvarchar(max)", nullable: true),
CreatedAt = table.Column<DateTime>(type: "datetime2", nullable: false, defaultValueSql: "GETUTCDATE()"),
UpdatedAt = table.Column<DateTime>(type: "datetime2", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_Projects", x => x.Id);
table.ForeignKey(
name: "FK_Projects_CuttingTools_CuttingToolId",
column: x => x.CuttingToolId,
principalTable: "CuttingTools",
principalColumn: "Id",
onDelete: ReferentialAction.SetNull);
table.ForeignKey(
name: "FK_Projects_Materials_MaterialId",
column: x => x.MaterialId,
principalTable: "Materials",
principalColumn: "Id",
onDelete: ReferentialAction.SetNull);
});
migrationBuilder.CreateTable(
name: "SupplierStocks",
columns: table => new
{
Id = table.Column<int>(type: "int", nullable: false)
.Annotation("SqlServer:Identity", "1, 1"),
SupplierId = table.Column<int>(type: "int", nullable: false),
MaterialId = table.Column<int>(type: "int", nullable: false),
LengthInches = table.Column<decimal>(type: "decimal(10,4)", precision: 10, scale: 4, nullable: false),
Price = table.Column<decimal>(type: "decimal(10,2)", precision: 10, scale: 2, nullable: true),
Notes = table.Column<string>(type: "nvarchar(255)", maxLength: 255, nullable: true),
IsActive = table.Column<bool>(type: "bit", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_SupplierStocks", x => x.Id);
table.ForeignKey(
name: "FK_SupplierStocks_Materials_MaterialId",
column: x => x.MaterialId,
principalTable: "Materials",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_SupplierStocks_Suppliers_SupplierId",
column: x => x.SupplierId,
principalTable: "Suppliers",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "ProjectParts",
columns: table => new
{
Id = table.Column<int>(type: "int", nullable: false)
.Annotation("SqlServer:Identity", "1, 1"),
ProjectId = table.Column<int>(type: "int", nullable: false),
Name = table.Column<string>(type: "nvarchar(100)", maxLength: 100, nullable: false),
LengthInches = table.Column<decimal>(type: "decimal(10,4)", precision: 10, scale: 4, nullable: false),
Quantity = table.Column<int>(type: "int", nullable: false),
SortOrder = table.Column<int>(type: "int", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_ProjectParts", x => x.Id);
table.ForeignKey(
name: "FK_ProjectParts_Projects_ProjectId",
column: x => x.ProjectId,
principalTable: "Projects",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "ProjectStockBins",
columns: table => new
{
Id = table.Column<int>(type: "int", nullable: false)
.Annotation("SqlServer:Identity", "1, 1"),
ProjectId = table.Column<int>(type: "int", nullable: false),
LengthInches = table.Column<decimal>(type: "decimal(10,4)", precision: 10, scale: 4, nullable: false),
Quantity = table.Column<int>(type: "int", nullable: false),
Priority = table.Column<int>(type: "int", nullable: false),
SortOrder = table.Column<int>(type: "int", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_ProjectStockBins", x => x.Id);
table.ForeignKey(
name: "FK_ProjectStockBins_Projects_ProjectId",
column: x => x.ProjectId,
principalTable: "Projects",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.InsertData(
table: "CuttingTools",
columns: new[] { "Id", "IsActive", "IsDefault", "KerfInches", "Name" },
values: new object[,]
{
{ 1, true, true, 0.0625m, "Bandsaw" },
{ 2, true, false, 0.125m, "Chop Saw" },
{ 3, true, false, 0.0625m, "Cold Cut Saw" },
{ 4, true, false, 0.0625m, "Hacksaw" }
});
migrationBuilder.CreateIndex(
name: "IX_ProjectParts_ProjectId",
table: "ProjectParts",
column: "ProjectId");
migrationBuilder.CreateIndex(
name: "IX_Projects_CuttingToolId",
table: "Projects",
column: "CuttingToolId");
migrationBuilder.CreateIndex(
name: "IX_Projects_MaterialId",
table: "Projects",
column: "MaterialId");
migrationBuilder.CreateIndex(
name: "IX_ProjectStockBins_ProjectId",
table: "ProjectStockBins",
column: "ProjectId");
migrationBuilder.CreateIndex(
name: "IX_SupplierStocks_MaterialId",
table: "SupplierStocks",
column: "MaterialId");
migrationBuilder.CreateIndex(
name: "IX_SupplierStocks_SupplierId_MaterialId_LengthInches",
table: "SupplierStocks",
columns: new[] { "SupplierId", "MaterialId", "LengthInches" },
unique: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "ProjectParts");
migrationBuilder.DropTable(
name: "ProjectStockBins");
migrationBuilder.DropTable(
name: "SupplierStocks");
migrationBuilder.DropTable(
name: "Projects");
migrationBuilder.DropTable(
name: "Suppliers");
migrationBuilder.DropTable(
name: "CuttingTools");
migrationBuilder.DropTable(
name: "Materials");
}
}
}

View File

@@ -0,0 +1,430 @@
// <auto-generated />
using System;
using CutList.Web.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace CutList.Web.Migrations
{
[DbContext(typeof(ApplicationDbContext))]
[Migration("20260202033321_AddMaterialStockLengths")]
partial class AddMaterialStockLengths
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "8.0.11")
.HasAnnotation("Relational:MaxIdentifierLength", 128);
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
modelBuilder.Entity("CutList.Web.Data.Entities.CuttingTool", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<bool>("IsActive")
.HasColumnType("bit");
b.Property<bool>("IsDefault")
.HasColumnType("bit");
b.Property<decimal>("KerfInches")
.HasPrecision(6, 4)
.HasColumnType("decimal(6,4)");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("nvarchar(50)");
b.HasKey("Id");
b.ToTable("CuttingTools");
b.HasData(
new
{
Id = 1,
IsActive = true,
IsDefault = true,
KerfInches = 0.0625m,
Name = "Bandsaw"
},
new
{
Id = 2,
IsActive = true,
IsDefault = false,
KerfInches = 0.125m,
Name = "Chop Saw"
},
new
{
Id = 3,
IsActive = true,
IsDefault = false,
KerfInches = 0.0625m,
Name = "Cold Cut Saw"
},
new
{
Id = 4,
IsActive = true,
IsDefault = false,
KerfInches = 0.0625m,
Name = "Hacksaw"
});
});
modelBuilder.Entity("CutList.Web.Data.Entities.Material", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<DateTime>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("datetime2")
.HasDefaultValueSql("GETUTCDATE()");
b.Property<string>("Description")
.HasMaxLength(255)
.HasColumnType("nvarchar(255)");
b.Property<bool>("IsActive")
.HasColumnType("bit");
b.Property<string>("Shape")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("nvarchar(50)");
b.Property<string>("Size")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<DateTime?>("UpdatedAt")
.HasColumnType("datetime2");
b.HasKey("Id");
b.ToTable("Materials");
});
modelBuilder.Entity("CutList.Web.Data.Entities.MaterialStockLength", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<bool>("IsActive")
.HasColumnType("bit");
b.Property<decimal>("LengthInches")
.HasPrecision(10, 4)
.HasColumnType("decimal(10,4)");
b.Property<int>("MaterialId")
.HasColumnType("int");
b.Property<string>("Notes")
.HasMaxLength(255)
.HasColumnType("nvarchar(255)");
b.HasKey("Id");
b.HasIndex("MaterialId", "LengthInches")
.IsUnique();
b.ToTable("MaterialStockLengths");
});
modelBuilder.Entity("CutList.Web.Data.Entities.Project", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<DateTime>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("datetime2")
.HasDefaultValueSql("GETUTCDATE()");
b.Property<int?>("CuttingToolId")
.HasColumnType("int");
b.Property<int?>("MaterialId")
.HasColumnType("int");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<string>("Notes")
.HasColumnType("nvarchar(max)");
b.Property<DateTime?>("UpdatedAt")
.HasColumnType("datetime2");
b.HasKey("Id");
b.HasIndex("CuttingToolId");
b.HasIndex("MaterialId");
b.ToTable("Projects");
});
modelBuilder.Entity("CutList.Web.Data.Entities.ProjectPart", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<decimal>("LengthInches")
.HasPrecision(10, 4)
.HasColumnType("decimal(10,4)");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<int>("ProjectId")
.HasColumnType("int");
b.Property<int>("Quantity")
.HasColumnType("int");
b.Property<int>("SortOrder")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("ProjectId");
b.ToTable("ProjectParts");
});
modelBuilder.Entity("CutList.Web.Data.Entities.ProjectStockBin", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<decimal>("LengthInches")
.HasPrecision(10, 4)
.HasColumnType("decimal(10,4)");
b.Property<int>("Priority")
.HasColumnType("int");
b.Property<int>("ProjectId")
.HasColumnType("int");
b.Property<int>("Quantity")
.HasColumnType("int");
b.Property<int>("SortOrder")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("ProjectId");
b.ToTable("ProjectStockBins");
});
modelBuilder.Entity("CutList.Web.Data.Entities.Supplier", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<string>("ContactInfo")
.HasMaxLength(500)
.HasColumnType("nvarchar(500)");
b.Property<DateTime>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("datetime2")
.HasDefaultValueSql("GETUTCDATE()");
b.Property<bool>("IsActive")
.HasColumnType("bit");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<string>("Notes")
.HasColumnType("nvarchar(max)");
b.HasKey("Id");
b.ToTable("Suppliers");
});
modelBuilder.Entity("CutList.Web.Data.Entities.SupplierStock", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<bool>("IsActive")
.HasColumnType("bit");
b.Property<decimal>("LengthInches")
.HasPrecision(10, 4)
.HasColumnType("decimal(10,4)");
b.Property<int>("MaterialId")
.HasColumnType("int");
b.Property<string>("Notes")
.HasMaxLength(255)
.HasColumnType("nvarchar(255)");
b.Property<decimal?>("Price")
.HasPrecision(10, 2)
.HasColumnType("decimal(10,2)");
b.Property<int>("SupplierId")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("MaterialId");
b.HasIndex("SupplierId", "MaterialId", "LengthInches")
.IsUnique();
b.ToTable("SupplierStocks");
});
modelBuilder.Entity("CutList.Web.Data.Entities.MaterialStockLength", b =>
{
b.HasOne("CutList.Web.Data.Entities.Material", "Material")
.WithMany("StockLengths")
.HasForeignKey("MaterialId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Material");
});
modelBuilder.Entity("CutList.Web.Data.Entities.Project", b =>
{
b.HasOne("CutList.Web.Data.Entities.CuttingTool", "CuttingTool")
.WithMany("Projects")
.HasForeignKey("CuttingToolId")
.OnDelete(DeleteBehavior.SetNull);
b.HasOne("CutList.Web.Data.Entities.Material", "Material")
.WithMany("Projects")
.HasForeignKey("MaterialId")
.OnDelete(DeleteBehavior.SetNull);
b.Navigation("CuttingTool");
b.Navigation("Material");
});
modelBuilder.Entity("CutList.Web.Data.Entities.ProjectPart", b =>
{
b.HasOne("CutList.Web.Data.Entities.Project", "Project")
.WithMany("Parts")
.HasForeignKey("ProjectId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Project");
});
modelBuilder.Entity("CutList.Web.Data.Entities.ProjectStockBin", b =>
{
b.HasOne("CutList.Web.Data.Entities.Project", "Project")
.WithMany("StockBins")
.HasForeignKey("ProjectId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Project");
});
modelBuilder.Entity("CutList.Web.Data.Entities.SupplierStock", b =>
{
b.HasOne("CutList.Web.Data.Entities.Material", "Material")
.WithMany("SupplierStocks")
.HasForeignKey("MaterialId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("CutList.Web.Data.Entities.Supplier", "Supplier")
.WithMany("Stocks")
.HasForeignKey("SupplierId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Material");
b.Navigation("Supplier");
});
modelBuilder.Entity("CutList.Web.Data.Entities.CuttingTool", b =>
{
b.Navigation("Projects");
});
modelBuilder.Entity("CutList.Web.Data.Entities.Material", b =>
{
b.Navigation("Projects");
b.Navigation("StockLengths");
b.Navigation("SupplierStocks");
});
modelBuilder.Entity("CutList.Web.Data.Entities.Project", b =>
{
b.Navigation("Parts");
b.Navigation("StockBins");
});
modelBuilder.Entity("CutList.Web.Data.Entities.Supplier", b =>
{
b.Navigation("Stocks");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,49 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace CutList.Web.Migrations
{
/// <inheritdoc />
public partial class AddMaterialStockLengths : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "MaterialStockLengths",
columns: table => new
{
Id = table.Column<int>(type: "int", nullable: false)
.Annotation("SqlServer:Identity", "1, 1"),
MaterialId = table.Column<int>(type: "int", nullable: false),
LengthInches = table.Column<decimal>(type: "decimal(10,4)", precision: 10, scale: 4, nullable: false),
Notes = table.Column<string>(type: "nvarchar(255)", maxLength: 255, nullable: true),
IsActive = table.Column<bool>(type: "bit", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_MaterialStockLengths", x => x.Id);
table.ForeignKey(
name: "FK_MaterialStockLengths_Materials_MaterialId",
column: x => x.MaterialId,
principalTable: "Materials",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_MaterialStockLengths_MaterialId_LengthInches",
table: "MaterialStockLengths",
columns: new[] { "MaterialId", "LengthInches" },
unique: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "MaterialStockLengths");
}
}
}

View File

@@ -0,0 +1,433 @@
// <auto-generated />
using System;
using CutList.Web.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace CutList.Web.Migrations
{
[DbContext(typeof(ApplicationDbContext))]
[Migration("20260202041453_AddProjectCustomer")]
partial class AddProjectCustomer
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "8.0.11")
.HasAnnotation("Relational:MaxIdentifierLength", 128);
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
modelBuilder.Entity("CutList.Web.Data.Entities.CuttingTool", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<bool>("IsActive")
.HasColumnType("bit");
b.Property<bool>("IsDefault")
.HasColumnType("bit");
b.Property<decimal>("KerfInches")
.HasPrecision(6, 4)
.HasColumnType("decimal(6,4)");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("nvarchar(50)");
b.HasKey("Id");
b.ToTable("CuttingTools");
b.HasData(
new
{
Id = 1,
IsActive = true,
IsDefault = true,
KerfInches = 0.0625m,
Name = "Bandsaw"
},
new
{
Id = 2,
IsActive = true,
IsDefault = false,
KerfInches = 0.125m,
Name = "Chop Saw"
},
new
{
Id = 3,
IsActive = true,
IsDefault = false,
KerfInches = 0.0625m,
Name = "Cold Cut Saw"
},
new
{
Id = 4,
IsActive = true,
IsDefault = false,
KerfInches = 0.0625m,
Name = "Hacksaw"
});
});
modelBuilder.Entity("CutList.Web.Data.Entities.Material", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<DateTime>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("datetime2")
.HasDefaultValueSql("GETUTCDATE()");
b.Property<string>("Description")
.HasMaxLength(255)
.HasColumnType("nvarchar(255)");
b.Property<bool>("IsActive")
.HasColumnType("bit");
b.Property<string>("Shape")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("nvarchar(50)");
b.Property<string>("Size")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<DateTime?>("UpdatedAt")
.HasColumnType("datetime2");
b.HasKey("Id");
b.ToTable("Materials");
});
modelBuilder.Entity("CutList.Web.Data.Entities.MaterialStockLength", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<bool>("IsActive")
.HasColumnType("bit");
b.Property<decimal>("LengthInches")
.HasPrecision(10, 4)
.HasColumnType("decimal(10,4)");
b.Property<int>("MaterialId")
.HasColumnType("int");
b.Property<string>("Notes")
.HasMaxLength(255)
.HasColumnType("nvarchar(255)");
b.HasKey("Id");
b.HasIndex("MaterialId", "LengthInches")
.IsUnique();
b.ToTable("MaterialStockLengths");
});
modelBuilder.Entity("CutList.Web.Data.Entities.Project", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<DateTime>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("datetime2")
.HasDefaultValueSql("GETUTCDATE()");
b.Property<string>("Customer")
.HasColumnType("nvarchar(max)");
b.Property<int?>("CuttingToolId")
.HasColumnType("int");
b.Property<int?>("MaterialId")
.HasColumnType("int");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<string>("Notes")
.HasColumnType("nvarchar(max)");
b.Property<DateTime?>("UpdatedAt")
.HasColumnType("datetime2");
b.HasKey("Id");
b.HasIndex("CuttingToolId");
b.HasIndex("MaterialId");
b.ToTable("Projects");
});
modelBuilder.Entity("CutList.Web.Data.Entities.ProjectPart", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<decimal>("LengthInches")
.HasPrecision(10, 4)
.HasColumnType("decimal(10,4)");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<int>("ProjectId")
.HasColumnType("int");
b.Property<int>("Quantity")
.HasColumnType("int");
b.Property<int>("SortOrder")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("ProjectId");
b.ToTable("ProjectParts");
});
modelBuilder.Entity("CutList.Web.Data.Entities.ProjectStockBin", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<decimal>("LengthInches")
.HasPrecision(10, 4)
.HasColumnType("decimal(10,4)");
b.Property<int>("Priority")
.HasColumnType("int");
b.Property<int>("ProjectId")
.HasColumnType("int");
b.Property<int>("Quantity")
.HasColumnType("int");
b.Property<int>("SortOrder")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("ProjectId");
b.ToTable("ProjectStockBins");
});
modelBuilder.Entity("CutList.Web.Data.Entities.Supplier", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<string>("ContactInfo")
.HasMaxLength(500)
.HasColumnType("nvarchar(500)");
b.Property<DateTime>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("datetime2")
.HasDefaultValueSql("GETUTCDATE()");
b.Property<bool>("IsActive")
.HasColumnType("bit");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<string>("Notes")
.HasColumnType("nvarchar(max)");
b.HasKey("Id");
b.ToTable("Suppliers");
});
modelBuilder.Entity("CutList.Web.Data.Entities.SupplierStock", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<bool>("IsActive")
.HasColumnType("bit");
b.Property<decimal>("LengthInches")
.HasPrecision(10, 4)
.HasColumnType("decimal(10,4)");
b.Property<int>("MaterialId")
.HasColumnType("int");
b.Property<string>("Notes")
.HasMaxLength(255)
.HasColumnType("nvarchar(255)");
b.Property<decimal?>("Price")
.HasPrecision(10, 2)
.HasColumnType("decimal(10,2)");
b.Property<int>("SupplierId")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("MaterialId");
b.HasIndex("SupplierId", "MaterialId", "LengthInches")
.IsUnique();
b.ToTable("SupplierStocks");
});
modelBuilder.Entity("CutList.Web.Data.Entities.MaterialStockLength", b =>
{
b.HasOne("CutList.Web.Data.Entities.Material", "Material")
.WithMany("StockLengths")
.HasForeignKey("MaterialId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Material");
});
modelBuilder.Entity("CutList.Web.Data.Entities.Project", b =>
{
b.HasOne("CutList.Web.Data.Entities.CuttingTool", "CuttingTool")
.WithMany("Projects")
.HasForeignKey("CuttingToolId")
.OnDelete(DeleteBehavior.SetNull);
b.HasOne("CutList.Web.Data.Entities.Material", "Material")
.WithMany("Projects")
.HasForeignKey("MaterialId")
.OnDelete(DeleteBehavior.SetNull);
b.Navigation("CuttingTool");
b.Navigation("Material");
});
modelBuilder.Entity("CutList.Web.Data.Entities.ProjectPart", b =>
{
b.HasOne("CutList.Web.Data.Entities.Project", "Project")
.WithMany("Parts")
.HasForeignKey("ProjectId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Project");
});
modelBuilder.Entity("CutList.Web.Data.Entities.ProjectStockBin", b =>
{
b.HasOne("CutList.Web.Data.Entities.Project", "Project")
.WithMany("StockBins")
.HasForeignKey("ProjectId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Project");
});
modelBuilder.Entity("CutList.Web.Data.Entities.SupplierStock", b =>
{
b.HasOne("CutList.Web.Data.Entities.Material", "Material")
.WithMany("SupplierStocks")
.HasForeignKey("MaterialId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("CutList.Web.Data.Entities.Supplier", "Supplier")
.WithMany("Stocks")
.HasForeignKey("SupplierId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Material");
b.Navigation("Supplier");
});
modelBuilder.Entity("CutList.Web.Data.Entities.CuttingTool", b =>
{
b.Navigation("Projects");
});
modelBuilder.Entity("CutList.Web.Data.Entities.Material", b =>
{
b.Navigation("Projects");
b.Navigation("StockLengths");
b.Navigation("SupplierStocks");
});
modelBuilder.Entity("CutList.Web.Data.Entities.Project", b =>
{
b.Navigation("Parts");
b.Navigation("StockBins");
});
modelBuilder.Entity("CutList.Web.Data.Entities.Supplier", b =>
{
b.Navigation("Stocks");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,28 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace CutList.Web.Migrations
{
/// <inheritdoc />
public partial class AddProjectCustomer : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "Customer",
table: "Projects",
type: "nvarchar(max)",
nullable: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "Customer",
table: "Projects");
}
}
}

View File

@@ -0,0 +1,394 @@
// <auto-generated />
using System;
using CutList.Web.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace CutList.Web.Migrations
{
[DbContext(typeof(ApplicationDbContext))]
[Migration("20260202043251_MultiMaterialProjectParts")]
partial class MultiMaterialProjectParts
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "8.0.11")
.HasAnnotation("Relational:MaxIdentifierLength", 128);
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
modelBuilder.Entity("CutList.Web.Data.Entities.CuttingTool", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<bool>("IsActive")
.HasColumnType("bit");
b.Property<bool>("IsDefault")
.HasColumnType("bit");
b.Property<decimal>("KerfInches")
.HasPrecision(6, 4)
.HasColumnType("decimal(6,4)");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("nvarchar(50)");
b.HasKey("Id");
b.ToTable("CuttingTools");
b.HasData(
new
{
Id = 1,
IsActive = true,
IsDefault = true,
KerfInches = 0.0625m,
Name = "Bandsaw"
},
new
{
Id = 2,
IsActive = true,
IsDefault = false,
KerfInches = 0.125m,
Name = "Chop Saw"
},
new
{
Id = 3,
IsActive = true,
IsDefault = false,
KerfInches = 0.0625m,
Name = "Cold Cut Saw"
},
new
{
Id = 4,
IsActive = true,
IsDefault = false,
KerfInches = 0.0625m,
Name = "Hacksaw"
});
});
modelBuilder.Entity("CutList.Web.Data.Entities.Material", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<DateTime>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("datetime2")
.HasDefaultValueSql("GETUTCDATE()");
b.Property<string>("Description")
.HasMaxLength(255)
.HasColumnType("nvarchar(255)");
b.Property<bool>("IsActive")
.HasColumnType("bit");
b.Property<string>("Shape")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("nvarchar(50)");
b.Property<string>("Size")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<DateTime?>("UpdatedAt")
.HasColumnType("datetime2");
b.HasKey("Id");
b.ToTable("Materials");
});
modelBuilder.Entity("CutList.Web.Data.Entities.MaterialStockLength", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<bool>("IsActive")
.HasColumnType("bit");
b.Property<decimal>("LengthInches")
.HasPrecision(10, 4)
.HasColumnType("decimal(10,4)");
b.Property<int>("MaterialId")
.HasColumnType("int");
b.Property<string>("Notes")
.HasMaxLength(255)
.HasColumnType("nvarchar(255)");
b.Property<int>("Quantity")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("MaterialId", "LengthInches")
.IsUnique();
b.ToTable("MaterialStockLengths");
});
modelBuilder.Entity("CutList.Web.Data.Entities.Project", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<DateTime>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("datetime2")
.HasDefaultValueSql("GETUTCDATE()");
b.Property<string>("Customer")
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<int?>("CuttingToolId")
.HasColumnType("int");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<string>("Notes")
.HasColumnType("nvarchar(max)");
b.Property<DateTime?>("UpdatedAt")
.HasColumnType("datetime2");
b.HasKey("Id");
b.HasIndex("CuttingToolId");
b.ToTable("Projects");
});
modelBuilder.Entity("CutList.Web.Data.Entities.ProjectPart", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<decimal>("LengthInches")
.HasPrecision(10, 4)
.HasColumnType("decimal(10,4)");
b.Property<int>("MaterialId")
.HasColumnType("int");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<int>("ProjectId")
.HasColumnType("int");
b.Property<int>("Quantity")
.HasColumnType("int");
b.Property<int>("SortOrder")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("MaterialId");
b.HasIndex("ProjectId");
b.ToTable("ProjectParts");
});
modelBuilder.Entity("CutList.Web.Data.Entities.Supplier", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<string>("ContactInfo")
.HasMaxLength(500)
.HasColumnType("nvarchar(500)");
b.Property<DateTime>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("datetime2")
.HasDefaultValueSql("GETUTCDATE()");
b.Property<bool>("IsActive")
.HasColumnType("bit");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<string>("Notes")
.HasColumnType("nvarchar(max)");
b.HasKey("Id");
b.ToTable("Suppliers");
});
modelBuilder.Entity("CutList.Web.Data.Entities.SupplierStock", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<bool>("IsActive")
.HasColumnType("bit");
b.Property<decimal>("LengthInches")
.HasPrecision(10, 4)
.HasColumnType("decimal(10,4)");
b.Property<int>("MaterialId")
.HasColumnType("int");
b.Property<string>("Notes")
.HasMaxLength(255)
.HasColumnType("nvarchar(255)");
b.Property<decimal?>("Price")
.HasPrecision(10, 2)
.HasColumnType("decimal(10,2)");
b.Property<int>("SupplierId")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("MaterialId");
b.HasIndex("SupplierId", "MaterialId", "LengthInches")
.IsUnique();
b.ToTable("SupplierStocks");
});
modelBuilder.Entity("CutList.Web.Data.Entities.MaterialStockLength", b =>
{
b.HasOne("CutList.Web.Data.Entities.Material", "Material")
.WithMany("StockLengths")
.HasForeignKey("MaterialId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Material");
});
modelBuilder.Entity("CutList.Web.Data.Entities.Project", b =>
{
b.HasOne("CutList.Web.Data.Entities.CuttingTool", "CuttingTool")
.WithMany("Projects")
.HasForeignKey("CuttingToolId")
.OnDelete(DeleteBehavior.SetNull);
b.Navigation("CuttingTool");
});
modelBuilder.Entity("CutList.Web.Data.Entities.ProjectPart", b =>
{
b.HasOne("CutList.Web.Data.Entities.Material", "Material")
.WithMany("ProjectParts")
.HasForeignKey("MaterialId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.HasOne("CutList.Web.Data.Entities.Project", "Project")
.WithMany("Parts")
.HasForeignKey("ProjectId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Material");
b.Navigation("Project");
});
modelBuilder.Entity("CutList.Web.Data.Entities.SupplierStock", b =>
{
b.HasOne("CutList.Web.Data.Entities.Material", "Material")
.WithMany("SupplierStocks")
.HasForeignKey("MaterialId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("CutList.Web.Data.Entities.Supplier", "Supplier")
.WithMany("Stocks")
.HasForeignKey("SupplierId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Material");
b.Navigation("Supplier");
});
modelBuilder.Entity("CutList.Web.Data.Entities.CuttingTool", b =>
{
b.Navigation("Projects");
});
modelBuilder.Entity("CutList.Web.Data.Entities.Material", b =>
{
b.Navigation("ProjectParts");
b.Navigation("StockLengths");
b.Navigation("SupplierStocks");
});
modelBuilder.Entity("CutList.Web.Data.Entities.Project", b =>
{
b.Navigation("Parts");
});
modelBuilder.Entity("CutList.Web.Data.Entities.Supplier", b =>
{
b.Navigation("Stocks");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,143 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace CutList.Web.Migrations
{
/// <inheritdoc />
public partial class MultiMaterialProjectParts : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_Projects_Materials_MaterialId",
table: "Projects");
migrationBuilder.DropTable(
name: "ProjectStockBins");
migrationBuilder.DropIndex(
name: "IX_Projects_MaterialId",
table: "Projects");
migrationBuilder.DropColumn(
name: "MaterialId",
table: "Projects");
migrationBuilder.AlterColumn<string>(
name: "Customer",
table: "Projects",
type: "nvarchar(100)",
maxLength: 100,
nullable: true,
oldClrType: typeof(string),
oldType: "nvarchar(max)",
oldNullable: true);
migrationBuilder.AddColumn<int>(
name: "MaterialId",
table: "ProjectParts",
type: "int",
nullable: false,
defaultValue: 0);
migrationBuilder.AddColumn<int>(
name: "Quantity",
table: "MaterialStockLengths",
type: "int",
nullable: false,
defaultValue: 0);
migrationBuilder.CreateIndex(
name: "IX_ProjectParts_MaterialId",
table: "ProjectParts",
column: "MaterialId");
migrationBuilder.AddForeignKey(
name: "FK_ProjectParts_Materials_MaterialId",
table: "ProjectParts",
column: "MaterialId",
principalTable: "Materials",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_ProjectParts_Materials_MaterialId",
table: "ProjectParts");
migrationBuilder.DropIndex(
name: "IX_ProjectParts_MaterialId",
table: "ProjectParts");
migrationBuilder.DropColumn(
name: "MaterialId",
table: "ProjectParts");
migrationBuilder.DropColumn(
name: "Quantity",
table: "MaterialStockLengths");
migrationBuilder.AlterColumn<string>(
name: "Customer",
table: "Projects",
type: "nvarchar(max)",
nullable: true,
oldClrType: typeof(string),
oldType: "nvarchar(100)",
oldMaxLength: 100,
oldNullable: true);
migrationBuilder.AddColumn<int>(
name: "MaterialId",
table: "Projects",
type: "int",
nullable: true);
migrationBuilder.CreateTable(
name: "ProjectStockBins",
columns: table => new
{
Id = table.Column<int>(type: "int", nullable: false)
.Annotation("SqlServer:Identity", "1, 1"),
ProjectId = table.Column<int>(type: "int", nullable: false),
LengthInches = table.Column<decimal>(type: "decimal(10,4)", precision: 10, scale: 4, nullable: false),
Priority = table.Column<int>(type: "int", nullable: false),
Quantity = table.Column<int>(type: "int", nullable: false),
SortOrder = table.Column<int>(type: "int", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_ProjectStockBins", x => x.Id);
table.ForeignKey(
name: "FK_ProjectStockBins_Projects_ProjectId",
column: x => x.ProjectId,
principalTable: "Projects",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_Projects_MaterialId",
table: "Projects",
column: "MaterialId");
migrationBuilder.CreateIndex(
name: "IX_ProjectStockBins_ProjectId",
table: "ProjectStockBins",
column: "ProjectId");
migrationBuilder.AddForeignKey(
name: "FK_Projects_Materials_MaterialId",
table: "Projects",
column: "MaterialId",
principalTable: "Materials",
principalColumn: "Id",
onDelete: ReferentialAction.SetNull);
}
}
}

View File

@@ -0,0 +1,391 @@
// <auto-generated />
using System;
using CutList.Web.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace CutList.Web.Migrations
{
[DbContext(typeof(ApplicationDbContext))]
partial class ApplicationDbContextModelSnapshot : ModelSnapshot
{
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "8.0.11")
.HasAnnotation("Relational:MaxIdentifierLength", 128);
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
modelBuilder.Entity("CutList.Web.Data.Entities.CuttingTool", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<bool>("IsActive")
.HasColumnType("bit");
b.Property<bool>("IsDefault")
.HasColumnType("bit");
b.Property<decimal>("KerfInches")
.HasPrecision(6, 4)
.HasColumnType("decimal(6,4)");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("nvarchar(50)");
b.HasKey("Id");
b.ToTable("CuttingTools");
b.HasData(
new
{
Id = 1,
IsActive = true,
IsDefault = true,
KerfInches = 0.0625m,
Name = "Bandsaw"
},
new
{
Id = 2,
IsActive = true,
IsDefault = false,
KerfInches = 0.125m,
Name = "Chop Saw"
},
new
{
Id = 3,
IsActive = true,
IsDefault = false,
KerfInches = 0.0625m,
Name = "Cold Cut Saw"
},
new
{
Id = 4,
IsActive = true,
IsDefault = false,
KerfInches = 0.0625m,
Name = "Hacksaw"
});
});
modelBuilder.Entity("CutList.Web.Data.Entities.Material", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<DateTime>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("datetime2")
.HasDefaultValueSql("GETUTCDATE()");
b.Property<string>("Description")
.HasMaxLength(255)
.HasColumnType("nvarchar(255)");
b.Property<bool>("IsActive")
.HasColumnType("bit");
b.Property<string>("Shape")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("nvarchar(50)");
b.Property<string>("Size")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<DateTime?>("UpdatedAt")
.HasColumnType("datetime2");
b.HasKey("Id");
b.ToTable("Materials");
});
modelBuilder.Entity("CutList.Web.Data.Entities.MaterialStockLength", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<bool>("IsActive")
.HasColumnType("bit");
b.Property<decimal>("LengthInches")
.HasPrecision(10, 4)
.HasColumnType("decimal(10,4)");
b.Property<int>("MaterialId")
.HasColumnType("int");
b.Property<string>("Notes")
.HasMaxLength(255)
.HasColumnType("nvarchar(255)");
b.Property<int>("Quantity")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("MaterialId", "LengthInches")
.IsUnique();
b.ToTable("MaterialStockLengths");
});
modelBuilder.Entity("CutList.Web.Data.Entities.Project", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<DateTime>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("datetime2")
.HasDefaultValueSql("GETUTCDATE()");
b.Property<string>("Customer")
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<int?>("CuttingToolId")
.HasColumnType("int");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<string>("Notes")
.HasColumnType("nvarchar(max)");
b.Property<DateTime?>("UpdatedAt")
.HasColumnType("datetime2");
b.HasKey("Id");
b.HasIndex("CuttingToolId");
b.ToTable("Projects");
});
modelBuilder.Entity("CutList.Web.Data.Entities.ProjectPart", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<decimal>("LengthInches")
.HasPrecision(10, 4)
.HasColumnType("decimal(10,4)");
b.Property<int>("MaterialId")
.HasColumnType("int");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<int>("ProjectId")
.HasColumnType("int");
b.Property<int>("Quantity")
.HasColumnType("int");
b.Property<int>("SortOrder")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("MaterialId");
b.HasIndex("ProjectId");
b.ToTable("ProjectParts");
});
modelBuilder.Entity("CutList.Web.Data.Entities.Supplier", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<string>("ContactInfo")
.HasMaxLength(500)
.HasColumnType("nvarchar(500)");
b.Property<DateTime>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("datetime2")
.HasDefaultValueSql("GETUTCDATE()");
b.Property<bool>("IsActive")
.HasColumnType("bit");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<string>("Notes")
.HasColumnType("nvarchar(max)");
b.HasKey("Id");
b.ToTable("Suppliers");
});
modelBuilder.Entity("CutList.Web.Data.Entities.SupplierStock", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<bool>("IsActive")
.HasColumnType("bit");
b.Property<decimal>("LengthInches")
.HasPrecision(10, 4)
.HasColumnType("decimal(10,4)");
b.Property<int>("MaterialId")
.HasColumnType("int");
b.Property<string>("Notes")
.HasMaxLength(255)
.HasColumnType("nvarchar(255)");
b.Property<decimal?>("Price")
.HasPrecision(10, 2)
.HasColumnType("decimal(10,2)");
b.Property<int>("SupplierId")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("MaterialId");
b.HasIndex("SupplierId", "MaterialId", "LengthInches")
.IsUnique();
b.ToTable("SupplierStocks");
});
modelBuilder.Entity("CutList.Web.Data.Entities.MaterialStockLength", b =>
{
b.HasOne("CutList.Web.Data.Entities.Material", "Material")
.WithMany("StockLengths")
.HasForeignKey("MaterialId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Material");
});
modelBuilder.Entity("CutList.Web.Data.Entities.Project", b =>
{
b.HasOne("CutList.Web.Data.Entities.CuttingTool", "CuttingTool")
.WithMany("Projects")
.HasForeignKey("CuttingToolId")
.OnDelete(DeleteBehavior.SetNull);
b.Navigation("CuttingTool");
});
modelBuilder.Entity("CutList.Web.Data.Entities.ProjectPart", b =>
{
b.HasOne("CutList.Web.Data.Entities.Material", "Material")
.WithMany("ProjectParts")
.HasForeignKey("MaterialId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.HasOne("CutList.Web.Data.Entities.Project", "Project")
.WithMany("Parts")
.HasForeignKey("ProjectId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Material");
b.Navigation("Project");
});
modelBuilder.Entity("CutList.Web.Data.Entities.SupplierStock", b =>
{
b.HasOne("CutList.Web.Data.Entities.Material", "Material")
.WithMany("SupplierStocks")
.HasForeignKey("MaterialId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("CutList.Web.Data.Entities.Supplier", "Supplier")
.WithMany("Stocks")
.HasForeignKey("SupplierId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Material");
b.Navigation("Supplier");
});
modelBuilder.Entity("CutList.Web.Data.Entities.CuttingTool", b =>
{
b.Navigation("Projects");
});
modelBuilder.Entity("CutList.Web.Data.Entities.Material", b =>
{
b.Navigation("ProjectParts");
b.Navigation("StockLengths");
b.Navigation("SupplierStocks");
});
modelBuilder.Entity("CutList.Web.Data.Entities.Project", b =>
{
b.Navigation("Parts");
});
modelBuilder.Entity("CutList.Web.Data.Entities.Supplier", b =>
{
b.Navigation("Stocks");
});
#pragma warning restore 612, 618
}
}
}

41
CutList.Web/Program.cs Normal file
View File

@@ -0,0 +1,41 @@
using CutList.Web.Components;
using CutList.Web.Data;
using CutList.Web.Services;
using Microsoft.EntityFrameworkCore;
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddControllers();
builder.Services.AddRazorComponents()
.AddInteractiveServerComponents();
// Add Entity Framework
builder.Services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));
// Add application services
builder.Services.AddScoped<MaterialService>();
builder.Services.AddScoped<SupplierService>();
builder.Services.AddScoped<ProjectService>();
builder.Services.AddScoped<CutListPackingService>();
builder.Services.AddScoped<ReportService>();
var app = builder.Build();
// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error", createScopeForErrors: true);
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseAntiforgery();
app.MapControllers();
app.MapRazorComponents<App>()
.AddInteractiveServerRenderMode();
app.Run();

View File

@@ -0,0 +1,22 @@
{
"profiles": {
"http": {
"commandName": "Project",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"dotnetRunMessages": true,
"applicationUrl": "http://localhost:5009"
},
"https": {
"commandName": "Project",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"dotnetRunMessages": true,
"applicationUrl": "https://localhost:5001;http://localhost:5000"
}
}
}

View File

@@ -0,0 +1,260 @@
using CutList.Core;
using CutList.Core.Nesting;
using CutList.Web.Data;
using CutList.Web.Data.Entities;
using Microsoft.EntityFrameworkCore;
namespace CutList.Web.Services;
public class CutListPackingService
{
private readonly ApplicationDbContext _context;
public CutListPackingService(ApplicationDbContext context)
{
_context = context;
}
public async Task<MultiMaterialPackResult> PackAsync(IEnumerable<ProjectPart> parts, decimal kerfInches)
{
var result = new MultiMaterialPackResult();
// Group parts by material
var partsByMaterial = parts.GroupBy(p => p.MaterialId);
foreach (var group in partsByMaterial)
{
var materialId = group.Key;
var materialParts = group.ToList();
// Get the material
var material = await _context.Materials
.FirstOrDefaultAsync(m => m.Id == materialId);
if (material == null) continue;
// Get in-stock lengths for this material
var inStockLengths = await _context.MaterialStockLengths
.Where(s => s.MaterialId == materialId && s.IsActive && s.Quantity > 0)
.ToListAsync();
// Get supplier stock lengths for this material (for purchase)
var supplierLengths = await _context.SupplierStocks
.Where(s => s.MaterialId == materialId && s.IsActive)
.Select(s => s.LengthInches)
.Distinct()
.ToListAsync();
// Build stock bins: in-stock first (priority 1), then supplier stock (priority 2)
var stockBins = new List<StockBinSource>();
// In-stock bins with finite quantity
foreach (var stock in inStockLengths)
{
stockBins.Add(new StockBinSource
{
LengthInches = stock.LengthInches,
Quantity = stock.Quantity,
Priority = 1,
IsInStock = true
});
}
// Supplier stock bins with unlimited quantity
foreach (var length in supplierLengths)
{
// Only add if not already covered by in-stock
if (!stockBins.Any(b => b.LengthInches == length && b.IsInStock))
{
stockBins.Add(new StockBinSource
{
LengthInches = length,
Quantity = -1, // unlimited
Priority = 2,
IsInStock = false
});
}
}
if (stockBins.Count == 0)
{
// No stock available for this material - mark all parts as not placed
var materialResult = new MaterialPackResult
{
Material = material,
PackResult = new PackResult(),
InStockBins = new List<Bin>(),
ToBePurchasedBins = new List<Bin>()
};
// Add all parts as not used
foreach (var part in materialParts)
{
for (int i = 0; i < part.Quantity; i++)
{
materialResult.PackResult.AddItemNotUsed(new BinItem(part.Name, (double)part.LengthInches));
}
}
result.MaterialResults.Add(materialResult);
continue;
}
// Run the packing algorithm
var engine = new MultiBinEngine();
engine.Spacing = (double)kerfInches;
engine.Strategy = PackingStrategy.AdvancedFit;
var multiBins = stockBins
.Select(b => new MultiBin((double)b.LengthInches, b.Quantity, b.Priority))
.ToList();
engine.SetBins(multiBins);
var items = materialParts
.SelectMany(p => Enumerable.Range(0, p.Quantity)
.Select(_ => new BinItem(p.Name, (double)p.LengthInches)))
.ToList();
var packResult = engine.Pack(items);
// Separate bins into in-stock and to-be-purchased
var inStockBins = new List<Bin>();
var toBePurchasedBins = new List<Bin>();
// Track remaining in-stock quantities
var remainingStock = inStockLengths.ToDictionary(
s => s.LengthInches,
s => s.Quantity);
foreach (var bin in packResult.Bins)
{
var binLength = (decimal)bin.Length;
// Check if this can come from in-stock
if (remainingStock.TryGetValue(binLength, out var remaining) && remaining > 0)
{
inStockBins.Add(bin);
remainingStock[binLength] = remaining - 1;
}
else
{
toBePurchasedBins.Add(bin);
}
}
result.MaterialResults.Add(new MaterialPackResult
{
Material = material,
PackResult = packResult,
InStockBins = inStockBins,
ToBePurchasedBins = toBePurchasedBins
});
}
return result;
}
public MultiMaterialPackingSummary GetSummary(MultiMaterialPackResult result)
{
var summary = new MultiMaterialPackingSummary();
foreach (var materialResult in result.MaterialResults)
{
var materialSummary = new MaterialPackingSummary
{
Material = materialResult.Material
};
foreach (var bin in materialResult.InStockBins)
{
materialSummary.InStockBins++;
materialSummary.TotalMaterial += bin.Length;
materialSummary.TotalUsed += bin.UsedLength;
materialSummary.TotalWaste += bin.RemainingLength;
materialSummary.TotalPieces += bin.Items.Count;
}
foreach (var bin in materialResult.ToBePurchasedBins)
{
materialSummary.ToBePurchasedBins++;
materialSummary.TotalMaterial += bin.Length;
materialSummary.TotalUsed += bin.UsedLength;
materialSummary.TotalWaste += bin.RemainingLength;
materialSummary.TotalPieces += bin.Items.Count;
}
materialSummary.ItemsNotPlaced = materialResult.PackResult.ItemsNotUsed.Count;
if (materialSummary.TotalMaterial > 0)
{
materialSummary.Efficiency = materialSummary.TotalUsed / materialSummary.TotalMaterial * 100;
}
summary.MaterialSummaries.Add(materialSummary);
// Aggregate totals
summary.TotalInStockBins += materialSummary.InStockBins;
summary.TotalToBePurchasedBins += materialSummary.ToBePurchasedBins;
summary.TotalPieces += materialSummary.TotalPieces;
summary.TotalMaterial += materialSummary.TotalMaterial;
summary.TotalUsed += materialSummary.TotalUsed;
summary.TotalWaste += materialSummary.TotalWaste;
summary.TotalItemsNotPlaced += materialSummary.ItemsNotPlaced;
}
if (summary.TotalMaterial > 0)
{
summary.Efficiency = summary.TotalUsed / summary.TotalMaterial * 100;
}
return summary;
}
}
public class StockBinSource
{
public decimal LengthInches { get; set; }
public int Quantity { get; set; }
public int Priority { get; set; }
public bool IsInStock { get; set; }
}
public class MultiMaterialPackResult
{
public List<MaterialPackResult> MaterialResults { get; set; } = new();
}
public class MaterialPackResult
{
public Material Material { get; set; } = null!;
public PackResult PackResult { get; set; } = null!;
public List<Bin> InStockBins { get; set; } = new();
public List<Bin> ToBePurchasedBins { get; set; } = new();
}
public class MultiMaterialPackingSummary
{
public List<MaterialPackingSummary> MaterialSummaries { get; set; } = new();
public int TotalInStockBins { get; set; }
public int TotalToBePurchasedBins { 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 TotalItemsNotPlaced { get; set; }
}
public class MaterialPackingSummary
{
public Material Material { get; set; } = null!;
public int InStockBins { get; set; }
public int ToBePurchasedBins { 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; }
}

View File

@@ -0,0 +1,122 @@
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();
}
// Stock Length methods
public async Task<List<MaterialStockLength>> GetStockLengthsAsync(int materialId)
{
return await _context.MaterialStockLengths
.Where(s => s.MaterialId == materialId && s.IsActive)
.OrderBy(s => s.LengthInches)
.ToListAsync();
}
public async Task<MaterialStockLength> AddStockLengthAsync(MaterialStockLength stockLength)
{
_context.MaterialStockLengths.Add(stockLength);
await _context.SaveChangesAsync();
return stockLength;
}
public async Task UpdateStockLengthAsync(MaterialStockLength stockLength)
{
_context.MaterialStockLengths.Update(stockLength);
await _context.SaveChangesAsync();
}
public async Task DeleteStockLengthAsync(int id)
{
var stockLength = await _context.MaterialStockLengths.FindAsync(id);
if (stockLength != null)
{
_context.MaterialStockLengths.Remove(stockLength);
await _context.SaveChangesAsync();
}
}
public async Task<bool> StockLengthExistsAsync(int materialId, decimal lengthInches, int? excludeId = null)
{
var query = _context.MaterialStockLengths
.Where(s => s.MaterialId == materialId && s.LengthInches == lengthInches && s.IsActive);
if (excludeId.HasValue)
{
query = query.Where(s => s.Id != excludeId.Value);
}
return await query.AnyAsync();
}
}

View File

@@ -0,0 +1,213 @@
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.CuttingTool)
.Include(p => p.Parts)
.ThenInclude(pt => pt.Material)
.OrderByDescending(p => p.UpdatedAt ?? p.CreatedAt)
.ToListAsync();
}
public async Task<Project?> GetByIdAsync(int id)
{
return await _context.Projects
.Include(p => p.CuttingTool)
.Include(p => p.Parts.OrderBy(pt => pt.SortOrder))
.ThenInclude(pt => pt.Material)
.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)",
Customer = original.Customer,
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,
MaterialId = part.MaterialId,
Name = part.Name,
LengthInches = part.LengthInches,
Quantity = part.Quantity,
SortOrder = part.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();
}
}
}
// 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();
}
}
}

View File

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

View File

@@ -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();
}
}

View File

@@ -0,0 +1,8 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}

View File

@@ -0,0 +1,13 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning",
"Microsoft.EntityFrameworkCore.Database.Command": "Warning"
}
},
"AllowedHosts": "*",
"ConnectionStrings": {
"DefaultConnection": "Server=(localdb)\\mssqllocaldb;Database=CutListDb;Trusted_Connection=True;MultipleActiveResultSets=true"
}
}

View File

@@ -0,0 +1,183 @@
html, body {
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
}
h1:focus {
outline: none;
}
a, .btn-link {
color: #006bb7;
}
.btn-primary {
color: #fff;
background-color: #1b6ec2;
border-color: #1861ac;
}
.btn:focus, .btn:active:focus, .btn-link.nav-link:focus, .form-control:focus, .form-check-input:focus {
box-shadow: 0 0 0 0.1rem white, 0 0 0 0.25rem #258cfb;
}
.nav-link {
padding-left: 10px;
}
.content {
padding-top: 1.1rem;
}
.valid.modified:not([type=checkbox]) {
outline: 1px solid #26b050;
}
.invalid {
outline: 1px solid red;
}
.validation-message {
color: red;
}
#blazor-error-ui {
background: lightyellow;
bottom: 0;
box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2);
display: none;
left: 0;
padding: 0.6rem 1.25rem 0.7rem 1.25rem;
position: fixed;
width: 100%;
z-index: 1000;
}
#blazor-error-ui .dismiss {
cursor: pointer;
position: absolute;
right: 0.75rem;
top: 0.5rem;
}
.blazor-error-boundary {
background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjswNiA1Mi4yNjg4TDI5MS4xMDUgODUuMjEzOEMyOTIuMTA0IDg2LjYwMDUgMjkxLjgwNyA4OC41NDA4IDI5MC40MjEgODkuNTM5N0wyNjguMTc2IDk5Ljk0MzlDMjY2Ljc5IDEwMC45NDMgMjY0Ljg0OSAxMDAuNjQ1IDI2My44NDkgOTkuMjU5NEwyMzkuNTcyIDY2LjMwNjVDMjM4LjU3MiA2NC45MiAyMzguODY5IDYyLjk3OTcgMjQwLjI1NiA2MS45ODA0TDI2MC44MDYgNTEuNjM5OUMyNjEuNjAyIDUxLjE4MzggMjYyLjU1IDUxLjAwMzYgMjYzLjUwNiA1MVoiIGZpbGw9IiNGRjgwODAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjxwYXRoIGQ9Ik0yNjMuMzQzIDY1LjM5OEMyNjQuMjY4IDY1LjM5OCAyNjUuMDA4IDY2LjEzOCAyNjUuMDA4IDY3LjA2M0MyNjUuMDA4IDY3Ljk4OCAyNjQuMjY4IDY4LjcyOCAyNjMuMzQzIDY4LjcyOEMyNjIuNDE4IDY4LjcyOCAyNjEuNjc4IDY3Ljk4OCAyNjEuNjc4IDY3LjA2M0MyNjEuNjc4IDY2LjEzOCAyNjIuNDE4IDY1LjM5OCAyNjMuMzQzIDY1LjM5OFoiIGZpbGw9IiNGRkZGRkYiLz48cGF0aCBkPSJNMjYzLjM0MyA3MS41OTEyQzI2My45NSA3MS41OTEyIDI2NC40NDQgNzIuMDg1NSAyNjQuNDQ0IDcyLjY5MjVMMjY0LjQ0NCA4NS4zMTg1QzI2NC40NDQgODUuOTI1NiAyNjMuOTUgODYuNDE5OCAyNjMuMzQzIDg2LjQxOTgiLz48L2c+PC9zdmc+) no-repeat 1rem/1.8rem, #b32121;
padding: 1rem 1rem 1rem 3.7rem;
color: white;
}
.blazor-error-boundary::after {
content: "An error has occurred."
}
.page {
position: relative;
display: flex;
flex-direction: column;
}
main {
flex: 1;
}
.sidebar {
background-image: linear-gradient(180deg, rgb(83 83 83) 0%, #2f2f2f 70%);
}
.top-row {
background-color: #f7f7f7;
border-bottom: 1px solid #d6d5d5;
justify-content: flex-end;
height: 3.5rem;
display: flex;
align-items: center;
}
.top-row ::deep a, .top-row ::deep .btn-link {
white-space: nowrap;
margin-left: 1.5rem;
text-decoration: none;
}
.top-row ::deep a:hover, .top-row ::deep .btn-link:hover {
text-decoration: underline;
}
.top-row ::deep a:first-child {
overflow: hidden;
text-overflow: ellipsis;
}
@media (max-width: 640.98px) {
.top-row {
justify-content: space-between;
}
.top-row ::deep a, .top-row ::deep .btn-link {
margin-left: 0;
}
}
@media (min-width: 641px) {
.page {
flex-direction: row;
}
.sidebar {
width: 250px;
height: 100vh;
position: sticky;
top: 0;
}
.top-row {
position: sticky;
top: 0;
z-index: 1;
}
.top-row.auth ::deep a:first-child {
flex: 1;
text-align: right;
width: 0;
}
.top-row, article {
padding-left: 2rem !important;
padding-right: 1.5rem !important;
}
}
/* Custom styles for length input */
.length-input {
position: relative;
}
/* Table improvements */
.table th {
white-space: nowrap;
}
/* Card improvements */
.card-header {
background-color: #f8f9fa;
}
/* Form improvements */
.form-label {
font-weight: 500;
margin-bottom: 0.25rem;
}
.form-text {
font-size: 0.8rem;
}
/* Better mobile responsiveness for tables */
@media (max-width: 768px) {
.table-responsive-stack td,
.table-responsive-stack th {
display: block;
width: 100%;
text-align: left;
}
}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,276 @@
/* CutList Report Styles - Print Friendly */
.cut-list-report {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
max-width: 800px;
margin: 0 auto;
padding: 20px;
}
.report-header {
margin-bottom: 2rem;
border-bottom: 2px solid #333;
padding-bottom: 1rem;
}
.report-header h1 {
font-size: 2rem;
margin-bottom: 1rem;
color: #333;
}
.meta-info {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 0.5rem;
}
.meta-row {
font-size: 0.95rem;
}
.meta-row span:first-child {
font-weight: 600;
min-width: 120px;
display: inline-block;
color: #555;
}
.bin-section {
border: 1px solid #ccc;
border-radius: 4px;
margin: 1rem 0;
padding: 1rem;
page-break-inside: avoid;
break-inside: avoid;
}
.bin-section h2 {
font-size: 1.1rem;
margin: 0 0 0.75rem 0;
padding-bottom: 0.5rem;
border-bottom: 1px solid #eee;
color: #333;
}
.cuts-table {
width: 100%;
border-collapse: collapse;
margin-bottom: 0.75rem;
}
.cuts-table th,
.cuts-table td {
padding: 0.5rem;
text-align: left;
border-bottom: 1px solid #eee;
}
.cuts-table th {
background: #f5f5f5;
font-weight: 600;
font-size: 0.85rem;
text-transform: uppercase;
color: #666;
}
.cuts-table tbody tr:hover {
background-color: #f9f9f9;
}
.drop {
font-size: 0.9rem;
color: #666;
font-style: italic;
}
.summary {
background: #f0f0f0;
padding: 1.5rem;
margin-top: 2rem;
border-radius: 4px;
page-break-inside: avoid;
}
.summary h2 {
font-size: 1.2rem;
margin: 0 0 1rem 0;
color: #333;
}
.summary-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 0.5rem 2rem;
}
.summary-row {
display: flex;
justify-content: space-between;
}
.summary-row span {
color: #555;
}
.summary-row strong {
color: #333;
}
.notes-section {
margin-top: 2rem;
padding: 1rem;
background: #fff;
border: 1px solid #ddd;
border-radius: 4px;
}
.notes-section h3 {
font-size: 1rem;
margin: 0 0 0.5rem 0;
color: #555;
}
.notes-section p {
margin: 0;
white-space: pre-wrap;
}
/* Print styles - Compact layout to save paper */
@media print {
body {
-webkit-print-color-adjust: exact;
print-color-adjust: exact;
font-size: 10pt;
}
.sidebar,
.top-row,
.page > main > .top-row,
.btn,
button,
nav,
.navbar {
display: none !important;
}
.page {
display: block !important;
}
.page > main {
margin-left: 0 !important;
padding: 0 !important;
}
.content {
padding: 0 !important;
}
.cut-list-report {
max-width: 100%;
padding: 0;
margin: 0;
font-size: 9pt;
}
.report-header {
margin-bottom: 0.5rem;
padding-bottom: 0.25rem;
border-bottom-width: 1px;
}
.report-header h1 {
font-size: 14pt;
margin-bottom: 0.25rem;
}
.meta-info {
gap: 0.1rem;
font-size: 8pt;
}
.meta-row span:first-child {
min-width: 80px;
}
.bin-section {
margin: 0.3rem 0;
padding: 0.3rem 0.5rem;
border-radius: 0;
break-inside: avoid;
page-break-inside: avoid;
}
.bin-section h2 {
font-size: 10pt;
margin: 0 0 0.2rem 0;
padding-bottom: 0.15rem;
}
.cuts-table {
margin-bottom: 0.2rem;
}
.cuts-table th,
.cuts-table td {
padding: 0.15rem 0.3rem;
font-size: 9pt;
}
.cuts-table th {
font-size: 8pt;
}
.drop {
font-size: 8pt;
margin-top: 0.1rem;
}
.summary {
padding: 0.5rem;
margin-top: 0.5rem;
break-inside: avoid;
page-break-inside: avoid;
}
.summary h2 {
font-size: 11pt;
margin: 0 0 0.3rem 0;
}
.summary-grid {
gap: 0.1rem 1rem;
font-size: 9pt;
}
.notes-section {
margin-top: 0.5rem;
padding: 0.3rem 0.5rem;
}
.notes-section h3 {
font-size: 9pt;
margin-bottom: 0.2rem;
}
.notes-section p {
font-size: 8pt;
}
.alert {
display: none !important;
}
.card {
display: none !important;
}
h1:not(.report-header h1) {
display: none !important;
}
.text-muted:not(.cut-list-report .text-muted) {
display: none !important;
}
}

View File

@@ -9,6 +9,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CutList.Core", "CutList.Cor
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CutList.Mcp", "CutList.Mcp\CutList.Mcp.csproj", "{3B53377F-E012-42BA-82C8-322815D661B3}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CutList.Web", "CutList.Web\CutList.Web.csproj", "{E3B33DE6-803C-4557-BF40-D8A5DB154144}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -55,6 +57,18 @@ Global
{3B53377F-E012-42BA-82C8-322815D661B3}.Release|x64.Build.0 = Release|Any CPU
{3B53377F-E012-42BA-82C8-322815D661B3}.Release|x86.ActiveCfg = Release|Any CPU
{3B53377F-E012-42BA-82C8-322815D661B3}.Release|x86.Build.0 = Release|Any CPU
{E3B33DE6-803C-4557-BF40-D8A5DB154144}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{E3B33DE6-803C-4557-BF40-D8A5DB154144}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E3B33DE6-803C-4557-BF40-D8A5DB154144}.Debug|x64.ActiveCfg = Debug|Any CPU
{E3B33DE6-803C-4557-BF40-D8A5DB154144}.Debug|x64.Build.0 = Debug|Any CPU
{E3B33DE6-803C-4557-BF40-D8A5DB154144}.Debug|x86.ActiveCfg = Debug|Any CPU
{E3B33DE6-803C-4557-BF40-D8A5DB154144}.Debug|x86.Build.0 = Debug|Any CPU
{E3B33DE6-803C-4557-BF40-D8A5DB154144}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E3B33DE6-803C-4557-BF40-D8A5DB154144}.Release|Any CPU.Build.0 = Release|Any CPU
{E3B33DE6-803C-4557-BF40-D8A5DB154144}.Release|x64.ActiveCfg = Release|Any CPU
{E3B33DE6-803C-4557-BF40-D8A5DB154144}.Release|x64.Build.0 = Release|Any CPU
{E3B33DE6-803C-4557-BF40-D8A5DB154144}.Release|x86.ActiveCfg = Release|Any CPU
{E3B33DE6-803C-4557-BF40-D8A5DB154144}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE

View File

@@ -43,6 +43,7 @@
newDocumentButton = new ToolStripButton();
openFileButton = new ToolStripButton();
saveButton = new ToolStripButton();
saveAsButton = new ToolStripButton();
toolStripSeparator1 = new ToolStripSeparator();
runButton = new ToolStripButton();
loadExampleDataButton = new ToolStripButton();
@@ -50,6 +51,8 @@
cutWidthTextBox = new TextBox();
cutMethodLabel = new Label();
cutWidthLabel = new Label();
materialShapeComboBox = new ComboBox();
materialShapeLabel = new Label();
tabControl1 = new TabControl();
tabPage2 = new TabPage();
tabPage1 = new TabPage();
@@ -59,6 +62,8 @@
TotalLengthString = new DataGridViewTextBoxColumn();
priorityDataGridViewTextBoxColumn = new DataGridViewTextBoxColumn();
binInputItemBindingSource = new BindingSource(components);
toolStripSeparator2 = new ToolStripSeparator();
toolStripSeparator3 = new ToolStripSeparator();
((System.ComponentModel.ISupportInitialize)dataGridView1).BeginInit();
((System.ComponentModel.ISupportInitialize)itemBindingSource).BeginInit();
toolStrip1.SuspendLayout();
@@ -137,63 +142,55 @@
//
// toolStrip1
//
toolStrip1.Items.AddRange(new ToolStripItem[] { newDocumentButton, openFileButton, saveButton, toolStripSeparator1, runButton, loadExampleDataButton });
toolStrip1.Items.AddRange(new ToolStripItem[] { newDocumentButton, toolStripSeparator3, openFileButton, toolStripSeparator2, saveButton, saveAsButton, toolStripSeparator1, runButton, loadExampleDataButton });
toolStrip1.Location = new Point(0, 0);
toolStrip1.Name = "toolStrip1";
toolStrip1.Size = new Size(844, 39);
toolStrip1.Size = new Size(844, 25);
toolStrip1.TabIndex = 0;
toolStrip1.Text = "toolStrip1";
//
// newDocumentButton
//
newDocumentButton.DisplayStyle = ToolStripItemDisplayStyle.Image;
newDocumentButton.Image = Properties.Resources.gnome_document_new;
newDocumentButton.ImageScaling = ToolStripItemImageScaling.None;
newDocumentButton.ImageTransparentColor = Color.Magenta;
newDocumentButton.Name = "newDocumentButton";
newDocumentButton.Padding = new Padding(5, 0, 5, 0);
newDocumentButton.Size = new Size(46, 36);
newDocumentButton.Size = new Size(51, 22);
newDocumentButton.Text = "New";
newDocumentButton.Click += newDocumentButton_Click;
//
// openFileButton
//
openFileButton.DisplayStyle = ToolStripItemDisplayStyle.Image;
openFileButton.Image = Properties.Resources.Open_Folder_32;
openFileButton.ImageScaling = ToolStripItemImageScaling.None;
openFileButton.ImageTransparentColor = Color.Magenta;
openFileButton.Name = "openFileButton";
openFileButton.Padding = new Padding(5, 0, 5, 0);
openFileButton.Size = new Size(46, 36);
openFileButton.Size = new Size(56, 22);
openFileButton.Text = "Open";
openFileButton.Click += openFileButton_Click;
//
// saveButton
//
saveButton.DisplayStyle = ToolStripItemDisplayStyle.Image;
saveButton.Image = Properties.Resources.Save_32;
saveButton.ImageScaling = ToolStripItemImageScaling.None;
saveButton.ImageTransparentColor = Color.Magenta;
saveButton.Name = "saveButton";
saveButton.Padding = new Padding(5, 0, 5, 0);
saveButton.Size = new Size(46, 36);
saveButton.Size = new Size(51, 22);
saveButton.Text = "Save";
saveButton.Click += saveButton_Click;
//
// saveAsButton
//
saveAsButton.DisplayStyle = ToolStripItemDisplayStyle.Text;
saveAsButton.Name = "saveAsButton";
saveAsButton.Size = new Size(51, 22);
saveAsButton.Text = "Save As";
saveAsButton.Click += saveAsButton_Click;
//
// toolStripSeparator1
//
toolStripSeparator1.Name = "toolStripSeparator1";
toolStripSeparator1.Size = new Size(6, 39);
toolStripSeparator1.Size = new Size(6, 25);
//
// runButton
//
runButton.DisplayStyle = ToolStripItemDisplayStyle.Image;
runButton.Image = Properties.Resources.Circled_Play_32;
runButton.ImageScaling = ToolStripItemImageScaling.None;
runButton.ImageTransparentColor = Color.Magenta;
runButton.Name = "runButton";
runButton.Padding = new Padding(5, 0, 5, 0);
runButton.Size = new Size(46, 36);
runButton.Size = new Size(48, 22);
runButton.Text = "Run";
runButton.Click += runButton_Click;
//
@@ -202,12 +199,8 @@
loadExampleDataButton.Alignment = ToolStripItemAlignment.Right;
loadExampleDataButton.DisplayStyle = ToolStripItemDisplayStyle.Text;
loadExampleDataButton.ForeColor = Color.DimGray;
loadExampleDataButton.Image = Properties.Resources.Circled_Play_32;
loadExampleDataButton.ImageScaling = ToolStripItemImageScaling.None;
loadExampleDataButton.ImageTransparentColor = Color.Magenta;
loadExampleDataButton.Name = "loadExampleDataButton";
loadExampleDataButton.Padding = new Padding(5, 0, 5, 0);
loadExampleDataButton.Size = new Size(122, 36);
loadExampleDataButton.Size = new Size(111, 22);
loadExampleDataButton.Text = "Load Example Data";
loadExampleDataButton.Click += loadExampleDataButton_Click;
//
@@ -250,9 +243,29 @@
cutWidthLabel.Size = new Size(67, 17);
cutWidthLabel.TabIndex = 8;
cutWidthLabel.Text = "Cut width";
//
//
// materialShapeLabel
//
materialShapeLabel.AutoSize = true;
materialShapeLabel.Font = new Font("Segoe UI Semibold", 9.75F, FontStyle.Bold, GraphicsUnit.Point, 0);
materialShapeLabel.ForeColor = Color.Blue;
materialShapeLabel.Location = new Point(310, 25);
materialShapeLabel.Name = "materialShapeLabel";
materialShapeLabel.Size = new Size(56, 17);
materialShapeLabel.TabIndex = 13;
materialShapeLabel.Text = "Material";
//
// materialShapeComboBox
//
materialShapeComboBox.DropDownStyle = ComboBoxStyle.DropDownList;
materialShapeComboBox.FormattingEnabled = true;
materialShapeComboBox.Location = new Point(372, 22);
materialShapeComboBox.Name = "materialShapeComboBox";
materialShapeComboBox.Size = new Size(184, 25);
materialShapeComboBox.TabIndex = 14;
//
// tabControl1
//
//
tabControl1.Anchor = AnchorStyles.Top | AnchorStyles.Bottom | AnchorStyles.Left | AnchorStyles.Right;
tabControl1.Controls.Add(tabPage2);
tabControl1.Controls.Add(tabPage1);
@@ -273,18 +286,20 @@
tabPage2.TabIndex = 1;
tabPage2.Text = "ITEMS TO NEST";
tabPage2.UseVisualStyleBackColor = true;
//
//
// tabPage1
//
//
tabPage1.Controls.Add(dataGridView2);
tabPage1.Controls.Add(materialShapeComboBox);
tabPage1.Controls.Add(materialShapeLabel);
tabPage1.Controls.Add(cutWidthTextBox);
tabPage1.Controls.Add(cutMethodComboBox);
tabPage1.Controls.Add(cutWidthLabel);
tabPage1.Controls.Add(cutMethodLabel);
tabPage1.Location = new Point(4, 26);
tabPage1.Location = new Point(4, 24);
tabPage1.Name = "tabPage1";
tabPage1.Padding = new Padding(3);
tabPage1.Size = new Size(812, 579);
tabPage1.Size = new Size(812, 581);
tabPage1.TabIndex = 0;
tabPage1.Text = "STOCK LENGTHS";
tabPage1.UseVisualStyleBackColor = true;
@@ -341,6 +356,16 @@
//
binInputItemBindingSource.DataSource = typeof(Models.BinInputItem);
//
// toolStripSeparator2
//
toolStripSeparator2.Name = "toolStripSeparator2";
toolStripSeparator2.Size = new Size(6, 25);
//
// toolStripSeparator3
//
toolStripSeparator3.Name = "toolStripSeparator3";
toolStripSeparator3.Size = new Size(6, 25);
//
// MainForm
//
AutoScaleMode = AutoScaleMode.None;
@@ -374,12 +399,15 @@
private System.Windows.Forms.ToolStrip toolStrip1;
private System.Windows.Forms.ToolStripButton openFileButton;
private System.Windows.Forms.ToolStripButton saveButton;
private System.Windows.Forms.ToolStripButton saveAsButton;
private System.Windows.Forms.ToolStripButton runButton;
private System.Windows.Forms.ToolStripSeparator toolStripSeparator1;
private System.Windows.Forms.ComboBox cutMethodComboBox;
private System.Windows.Forms.TextBox cutWidthTextBox;
private System.Windows.Forms.Label cutMethodLabel;
private System.Windows.Forms.Label cutWidthLabel;
private System.Windows.Forms.ComboBox materialShapeComboBox;
private System.Windows.Forms.Label materialShapeLabel;
private System.Windows.Forms.TabControl tabControl1;
private System.Windows.Forms.TabPage tabPage1;
private System.Windows.Forms.TabPage tabPage2;
@@ -395,6 +423,8 @@
private System.Windows.Forms.DataGridViewTextBoxColumn TotalLength;
private System.Windows.Forms.ToolStripButton newDocumentButton;
private System.Windows.Forms.ToolStripButton loadExampleDataButton;
private ToolStripSeparator toolStripSeparator3;
private ToolStripSeparator toolStripSeparator2;
}
}

View File

@@ -9,6 +9,7 @@ namespace CutList.Forms
public partial class MainForm : Form, IMainView
{
private static readonly Random random = new Random();
private const string BaseTitle = "Cut List";
private BindingList<PartInputItem> parts;
private BindingList<BinInputItem> bins;
@@ -32,9 +33,30 @@ namespace CutList.Forms
binInputItemBindingSource.DataSource = bins;
binInputItemBindingSource.ListChanged += BinInputItemBindingSource_ListChanged;
toolbox = new Toolbox();
cutMethodComboBox.DataSource = toolbox.Tools;
// Populate material shapes
materialShapeComboBox.Items.AddRange(new object[]
{
"Round Tube",
"Square Tube",
"Rectangular Tube",
"Angle",
"Channel",
"Flat Bar",
"Round Bar",
"Square Bar",
"I-Beam",
"Pipe",
"Other"
});
materialShapeComboBox.SelectedIndex = 0;
// Enable keyboard shortcuts
KeyPreview = true;
#if DEBUG
loadExampleDataButton.Visible = true;
#else
@@ -48,6 +70,7 @@ namespace CutList.Forms
public List<PartInputItem> Parts => parts.ToList();
public List<BinInputItem> StockBins => bins.ToList();
public Tool SelectedTool => cutMethodComboBox.SelectedItem as Tool;
public string? SelectedMaterialShape => materialShapeComboBox.SelectedItem?.ToString();
public void ShowError(string message)
{
@@ -122,9 +145,9 @@ namespace CutList.Forms
binInputItemBindingSource.DataSource = bins;
}
public void ShowResults(List<Bin> binResults, string fileName)
public void ShowResults(List<Bin> binResults, string fileName, string cutMethod, string? materialShape = null)
{
var form = new ResultsForm(fileName);
var form = new ResultsForm(fileName, cutMethod, materialShape);
form.Bins = binResults;
form.ShowDialog();
}
@@ -140,6 +163,13 @@ namespace CutList.Forms
LoadDocumentData(new List<PartInputItem>(), new List<BinInputItem>());
}
public void UpdateWindowTitle(string? fileName)
{
Text = string.IsNullOrEmpty(fileName)
? BaseTitle
: $"{fileName} - {BaseTitle}";
}
// Event handler delegates to presenter
private void Open()
{
@@ -164,6 +194,61 @@ namespace CutList.Forms
presenter.SaveDocument();
}
private void SaveAs()
{
FlushPendingEdits();
presenter.SaveDocumentAs();
}
protected override void OnKeyDown(KeyEventArgs e)
{
base.OnKeyDown(e);
if (e.Control && e.Shift && e.KeyCode == Keys.S)
{
SaveAs();
e.Handled = true;
}
else if (e.Control && e.KeyCode == Keys.S)
{
Save();
e.Handled = true;
}
else if (e.Control && e.KeyCode == Keys.O)
{
Open();
e.Handled = true;
}
else if (e.Control && e.KeyCode == Keys.N)
{
presenter.NewDocument();
e.Handled = true;
}
}
protected override bool ProcessCmdKey(ref Message msg, Keys keyData)
{
// Handle Enter key in items DataGridView to move to Length column
if (keyData == Keys.Enter && dataGridView1.CurrentCell != null &&
(dataGridView1.ContainsFocus || dataGridView1.IsCurrentCellInEditMode))
{
dataGridView1.EndEdit();
int currentRow = dataGridView1.CurrentCell.RowIndex;
int nextRow = currentRow + 1;
int lengthColumnIndex = lengthDataGridViewTextBoxColumn.Index;
if (nextRow < dataGridView1.RowCount)
{
dataGridView1.CurrentCell = dataGridView1[lengthColumnIndex, nextRow];
dataGridView1.BeginEdit(true);
return true; // Handled
}
}
return base.ProcessCmdKey(ref msg, keyData);
}
private void Run()
{
FlushPendingEdits();
@@ -224,6 +309,11 @@ namespace CutList.Forms
Save();
}
private void saveAsButton_Click(object sender, EventArgs e)
{
SaveAs();
}
private void runButton_Click(object sender, EventArgs e)
{
Run();
@@ -281,6 +371,7 @@ namespace CutList.Forms
dataGridView1.Refresh();
}
private void dataGridView1_DataError(object sender, DataGridViewDataErrorEventArgs e)
{
dataGridView1.Rows[e.RowIndex].ErrorText = e.Exception.InnerException?.Message;

View File

@@ -28,237 +28,233 @@
/// </summary>
private void InitializeComponent()
{
this.components = new System.ComponentModel.Container();
System.Windows.Forms.DataGridViewCellStyle dataGridViewCellStyle1 = new System.Windows.Forms.DataGridViewCellStyle();
this.dataGridView1 = new System.Windows.Forms.DataGridView();
this.countDataGridViewTextBoxColumn = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.spacingDataGridViewTextBoxColumn = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.lengthDataGridViewTextBoxColumn = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.usedLengthDataGridViewTextBoxColumn = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.remainingLengthDataGridViewTextBoxColumn = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.utilizationDataGridViewTextBoxColumn = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.binBindingSource = new System.Windows.Forms.BindingSource(this.components);
this.splitContainer1 = new System.Windows.Forms.SplitContainer();
this.splitContainer2 = new System.Windows.Forms.SplitContainer();
this.dataGridView2 = new System.Windows.Forms.DataGridView();
this.binLayoutView1 = new CutList.Controls.BinLayoutView();
this.label1 = new System.Windows.Forms.Label();
this.uIItemBindingSource = new System.Windows.Forms.BindingSource(this.components);
this.menuStrip1 = new System.Windows.Forms.MenuStrip();
this.saveToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
((System.ComponentModel.ISupportInitialize)(this.dataGridView1)).BeginInit();
((System.ComponentModel.ISupportInitialize)(this.binBindingSource)).BeginInit();
((System.ComponentModel.ISupportInitialize)(this.splitContainer1)).BeginInit();
this.splitContainer1.Panel1.SuspendLayout();
this.splitContainer1.Panel2.SuspendLayout();
this.splitContainer1.SuspendLayout();
((System.ComponentModel.ISupportInitialize)(this.splitContainer2)).BeginInit();
this.splitContainer2.Panel1.SuspendLayout();
this.splitContainer2.Panel2.SuspendLayout();
this.splitContainer2.SuspendLayout();
((System.ComponentModel.ISupportInitialize)(this.dataGridView2)).BeginInit();
((System.ComponentModel.ISupportInitialize)(this.uIItemBindingSource)).BeginInit();
this.menuStrip1.SuspendLayout();
this.SuspendLayout();
components = new System.ComponentModel.Container();
DataGridViewCellStyle dataGridViewCellStyle1 = new DataGridViewCellStyle();
dataGridView1 = new DataGridView();
countDataGridViewTextBoxColumn = new DataGridViewTextBoxColumn();
spacingDataGridViewTextBoxColumn = new DataGridViewTextBoxColumn();
lengthDataGridViewTextBoxColumn = new DataGridViewTextBoxColumn();
usedLengthDataGridViewTextBoxColumn = new DataGridViewTextBoxColumn();
remainingLengthDataGridViewTextBoxColumn = new DataGridViewTextBoxColumn();
utilizationDataGridViewTextBoxColumn = new DataGridViewTextBoxColumn();
binBindingSource = new BindingSource(components);
splitContainer1 = new SplitContainer();
splitContainer2 = new SplitContainer();
dataGridView2 = new DataGridView();
binLayoutView1 = new CutList.Controls.BinLayoutView();
label1 = new Label();
uIItemBindingSource = new BindingSource(components);
menuStrip1 = new MenuStrip();
saveToolStripMenuItem = new ToolStripMenuItem();
((System.ComponentModel.ISupportInitialize)dataGridView1).BeginInit();
((System.ComponentModel.ISupportInitialize)binBindingSource).BeginInit();
((System.ComponentModel.ISupportInitialize)splitContainer1).BeginInit();
splitContainer1.Panel1.SuspendLayout();
splitContainer1.Panel2.SuspendLayout();
splitContainer1.SuspendLayout();
((System.ComponentModel.ISupportInitialize)splitContainer2).BeginInit();
splitContainer2.Panel1.SuspendLayout();
splitContainer2.Panel2.SuspendLayout();
splitContainer2.SuspendLayout();
((System.ComponentModel.ISupportInitialize)dataGridView2).BeginInit();
((System.ComponentModel.ISupportInitialize)uIItemBindingSource).BeginInit();
menuStrip1.SuspendLayout();
SuspendLayout();
//
// dataGridView1
//
this.dataGridView1.AutoGenerateColumns = false;
this.dataGridView1.BackgroundColor = System.Drawing.Color.White;
this.dataGridView1.BorderStyle = System.Windows.Forms.BorderStyle.None;
this.dataGridView1.ColumnHeadersBorderStyle = System.Windows.Forms.DataGridViewHeaderBorderStyle.Single;
this.dataGridView1.ColumnHeadersHeight = 30;
this.dataGridView1.Columns.AddRange(new System.Windows.Forms.DataGridViewColumn[] {
this.countDataGridViewTextBoxColumn,
this.spacingDataGridViewTextBoxColumn,
this.lengthDataGridViewTextBoxColumn,
this.usedLengthDataGridViewTextBoxColumn,
this.remainingLengthDataGridViewTextBoxColumn,
this.utilizationDataGridViewTextBoxColumn});
this.dataGridView1.DataSource = this.binBindingSource;
this.dataGridView1.Dock = System.Windows.Forms.DockStyle.Fill;
this.dataGridView1.GridColor = System.Drawing.Color.FromArgb(((int)(((byte)(224)))), ((int)(((byte)(224)))), ((int)(((byte)(224)))));
this.dataGridView1.Location = new System.Drawing.Point(0, 0);
this.dataGridView1.Name = "dataGridView1";
this.dataGridView1.RowHeadersBorderStyle = System.Windows.Forms.DataGridViewHeaderBorderStyle.Single;
this.dataGridView1.RowTemplate.Height = 25;
this.dataGridView1.Size = new System.Drawing.Size(994, 253);
this.dataGridView1.TabIndex = 0;
this.dataGridView1.RowEnter += new System.Windows.Forms.DataGridViewCellEventHandler(this.dataGridView1_RowEnter);
//
dataGridView1.AutoGenerateColumns = false;
dataGridView1.BackgroundColor = Color.White;
dataGridView1.BorderStyle = BorderStyle.None;
dataGridView1.ColumnHeadersBorderStyle = DataGridViewHeaderBorderStyle.Single;
dataGridView1.ColumnHeadersHeight = 30;
dataGridView1.Columns.AddRange(new DataGridViewColumn[] { countDataGridViewTextBoxColumn, spacingDataGridViewTextBoxColumn, lengthDataGridViewTextBoxColumn, usedLengthDataGridViewTextBoxColumn, remainingLengthDataGridViewTextBoxColumn, utilizationDataGridViewTextBoxColumn });
dataGridView1.DataSource = binBindingSource;
dataGridView1.Dock = DockStyle.Fill;
dataGridView1.GridColor = Color.FromArgb(224, 224, 224);
dataGridView1.Location = new Point(0, 0);
dataGridView1.Name = "dataGridView1";
dataGridView1.RowHeadersBorderStyle = DataGridViewHeaderBorderStyle.Single;
dataGridView1.Size = new Size(959, 469);
dataGridView1.TabIndex = 0;
dataGridView1.RowEnter += dataGridView1_RowEnter;
//
// countDataGridViewTextBoxColumn
//
this.countDataGridViewTextBoxColumn.DataPropertyName = "Count";
this.countDataGridViewTextBoxColumn.HeaderText = "Count";
this.countDataGridViewTextBoxColumn.Name = "countDataGridViewTextBoxColumn";
this.countDataGridViewTextBoxColumn.Width = 60;
//
//
countDataGridViewTextBoxColumn.DataPropertyName = "Count";
countDataGridViewTextBoxColumn.HeaderText = "Count";
countDataGridViewTextBoxColumn.Name = "countDataGridViewTextBoxColumn";
countDataGridViewTextBoxColumn.ReadOnly = true;
countDataGridViewTextBoxColumn.Width = 60;
//
// spacingDataGridViewTextBoxColumn
//
this.spacingDataGridViewTextBoxColumn.DataPropertyName = "Spacing";
this.spacingDataGridViewTextBoxColumn.HeaderText = "Spacing";
this.spacingDataGridViewTextBoxColumn.Name = "spacingDataGridViewTextBoxColumn";
//
spacingDataGridViewTextBoxColumn.DataPropertyName = "Spacing";
spacingDataGridViewTextBoxColumn.HeaderText = "Spacing";
spacingDataGridViewTextBoxColumn.Name = "spacingDataGridViewTextBoxColumn";
spacingDataGridViewTextBoxColumn.ReadOnly = true;
//
// lengthDataGridViewTextBoxColumn
//
this.lengthDataGridViewTextBoxColumn.DataPropertyName = "Length";
this.lengthDataGridViewTextBoxColumn.HeaderText = "Length";
this.lengthDataGridViewTextBoxColumn.Name = "lengthDataGridViewTextBoxColumn";
lengthDataGridViewTextBoxColumn.DataPropertyName = "Length";
lengthDataGridViewTextBoxColumn.HeaderText = "Length";
lengthDataGridViewTextBoxColumn.Name = "lengthDataGridViewTextBoxColumn";
lengthDataGridViewTextBoxColumn.ReadOnly = true;
//
// usedLengthDataGridViewTextBoxColumn
//
this.usedLengthDataGridViewTextBoxColumn.DataPropertyName = "UsedLength";
this.usedLengthDataGridViewTextBoxColumn.HeaderText = "Used Length";
this.usedLengthDataGridViewTextBoxColumn.Name = "usedLengthDataGridViewTextBoxColumn";
this.usedLengthDataGridViewTextBoxColumn.ReadOnly = true;
usedLengthDataGridViewTextBoxColumn.DataPropertyName = "UsedLength";
usedLengthDataGridViewTextBoxColumn.HeaderText = "Used Length";
usedLengthDataGridViewTextBoxColumn.Name = "usedLengthDataGridViewTextBoxColumn";
usedLengthDataGridViewTextBoxColumn.ReadOnly = true;
//
// remainingLengthDataGridViewTextBoxColumn
//
this.remainingLengthDataGridViewTextBoxColumn.DataPropertyName = "RemainingLength";
this.remainingLengthDataGridViewTextBoxColumn.HeaderText = "Remaining Length";
this.remainingLengthDataGridViewTextBoxColumn.Name = "remainingLengthDataGridViewTextBoxColumn";
this.remainingLengthDataGridViewTextBoxColumn.ReadOnly = true;
this.remainingLengthDataGridViewTextBoxColumn.Width = 150;
remainingLengthDataGridViewTextBoxColumn.DataPropertyName = "RemainingLength";
remainingLengthDataGridViewTextBoxColumn.HeaderText = "Remaining Length";
remainingLengthDataGridViewTextBoxColumn.Name = "remainingLengthDataGridViewTextBoxColumn";
remainingLengthDataGridViewTextBoxColumn.ReadOnly = true;
remainingLengthDataGridViewTextBoxColumn.Width = 150;
//
// utilizationDataGridViewTextBoxColumn
//
this.utilizationDataGridViewTextBoxColumn.DataPropertyName = "Utilization";
utilizationDataGridViewTextBoxColumn.DataPropertyName = "Utilization";
dataGridViewCellStyle1.Format = "P2";
this.utilizationDataGridViewTextBoxColumn.DefaultCellStyle = dataGridViewCellStyle1;
this.utilizationDataGridViewTextBoxColumn.HeaderText = "Utilization";
this.utilizationDataGridViewTextBoxColumn.Name = "utilizationDataGridViewTextBoxColumn";
this.utilizationDataGridViewTextBoxColumn.ReadOnly = true;
utilizationDataGridViewTextBoxColumn.DefaultCellStyle = dataGridViewCellStyle1;
utilizationDataGridViewTextBoxColumn.HeaderText = "Utilization";
utilizationDataGridViewTextBoxColumn.Name = "utilizationDataGridViewTextBoxColumn";
utilizationDataGridViewTextBoxColumn.ReadOnly = true;
//
// binBindingSource
//
this.binBindingSource.DataSource = typeof(CutList.Core.BinGroup);
//
binBindingSource.DataSource = typeof(Core.BinGroup);
//
// splitContainer1
//
this.splitContainer1.Dock = System.Windows.Forms.DockStyle.Fill;
this.splitContainer1.FixedPanel = System.Windows.Forms.FixedPanel.Panel2;
this.splitContainer1.Location = new System.Drawing.Point(0, 24);
this.splitContainer1.Name = "splitContainer1";
this.splitContainer1.Orientation = System.Windows.Forms.Orientation.Horizontal;
splitContainer1.Dock = DockStyle.Fill;
splitContainer1.FixedPanel = FixedPanel.Panel2;
splitContainer1.Location = new Point(0, 24);
splitContainer1.Name = "splitContainer1";
splitContainer1.Orientation = Orientation.Horizontal;
//
// splitContainer1.Panel1
//
this.splitContainer1.Panel1.Controls.Add(this.dataGridView1);
splitContainer1.Panel1.Controls.Add(dataGridView1);
//
// splitContainer1.Panel2
//
this.splitContainer1.Panel2.Controls.Add(this.splitContainer2);
this.splitContainer1.Panel2.Controls.Add(this.label1);
this.splitContainer1.Size = new System.Drawing.Size(994, 507);
this.splitContainer1.SplitterDistance = 253;
this.splitContainer1.TabIndex = 2;
splitContainer1.Panel2.Controls.Add(splitContainer2);
splitContainer1.Panel2.Controls.Add(label1);
splitContainer1.Size = new Size(959, 723);
splitContainer1.SplitterDistance = 469;
splitContainer1.TabIndex = 2;
//
// splitContainer2
//
this.splitContainer2.Dock = System.Windows.Forms.DockStyle.Fill;
this.splitContainer2.Location = new System.Drawing.Point(0, 34);
this.splitContainer2.Name = "splitContainer2";
splitContainer2.Dock = DockStyle.Fill;
splitContainer2.Location = new Point(0, 34);
splitContainer2.Name = "splitContainer2";
//
// splitContainer2.Panel1
//
this.splitContainer2.Panel1.Controls.Add(this.dataGridView2);
splitContainer2.Panel1.Controls.Add(dataGridView2);
//
// splitContainer2.Panel2
//
this.splitContainer2.Panel2.Controls.Add(this.binLayoutView1);
this.splitContainer2.Size = new System.Drawing.Size(994, 216);
this.splitContainer2.SplitterDistance = 276;
this.splitContainer2.TabIndex = 1;
splitContainer2.Panel2.Controls.Add(binLayoutView1);
splitContainer2.Size = new Size(959, 216);
splitContainer2.SplitterDistance = 266;
splitContainer2.TabIndex = 1;
//
// dataGridView2
//
this.dataGridView2.BackgroundColor = System.Drawing.Color.White;
this.dataGridView2.BorderStyle = System.Windows.Forms.BorderStyle.None;
this.dataGridView2.ColumnHeadersBorderStyle = System.Windows.Forms.DataGridViewHeaderBorderStyle.Single;
this.dataGridView2.ColumnHeadersHeight = 30;
this.dataGridView2.Dock = System.Windows.Forms.DockStyle.Fill;
this.dataGridView2.GridColor = System.Drawing.Color.FromArgb(((int)(((byte)(224)))), ((int)(((byte)(224)))), ((int)(((byte)(224)))));
this.dataGridView2.Location = new System.Drawing.Point(0, 0);
this.dataGridView2.Name = "dataGridView2";
this.dataGridView2.RowHeadersBorderStyle = System.Windows.Forms.DataGridViewHeaderBorderStyle.Single;
this.dataGridView2.RowTemplate.Height = 25;
this.dataGridView2.Size = new System.Drawing.Size(276, 216);
this.dataGridView2.TabIndex = 1;
dataGridView2.BackgroundColor = Color.White;
dataGridView2.BorderStyle = BorderStyle.None;
dataGridView2.ColumnHeadersBorderStyle = DataGridViewHeaderBorderStyle.Single;
dataGridView2.ColumnHeadersHeight = 30;
dataGridView2.Dock = DockStyle.Fill;
dataGridView2.GridColor = Color.FromArgb(224, 224, 224);
dataGridView2.Location = new Point(0, 0);
dataGridView2.Name = "dataGridView2";
dataGridView2.RowHeadersBorderStyle = DataGridViewHeaderBorderStyle.Single;
dataGridView2.Size = new Size(266, 216);
dataGridView2.TabIndex = 1;
//
// binLayoutView1
//
this.binLayoutView1.BackColor = System.Drawing.Color.White;
this.binLayoutView1.Bin = null;
this.binLayoutView1.Dock = System.Windows.Forms.DockStyle.Fill;
this.binLayoutView1.Location = new System.Drawing.Point(0, 0);
this.binLayoutView1.Name = "binLayoutView1";
this.binLayoutView1.Size = new System.Drawing.Size(714, 216);
this.binLayoutView1.TabIndex = 1;
this.binLayoutView1.Text = "class11";
binLayoutView1.BackColor = Color.White;
binLayoutView1.Bin = null;
binLayoutView1.BinBackgroundColor = Color.Pink;
binLayoutView1.Dock = DockStyle.Fill;
binLayoutView1.ItemBackgroundColor = Color.White;
binLayoutView1.ItemBorderColor = Color.Blue;
binLayoutView1.Location = new Point(0, 0);
binLayoutView1.Name = "binLayoutView1";
binLayoutView1.Size = new Size(689, 216);
binLayoutView1.TabIndex = 1;
binLayoutView1.Text = "class11";
//
// label1
//
this.label1.BackColor = System.Drawing.Color.LightSlateGray;
this.label1.Dock = System.Windows.Forms.DockStyle.Top;
this.label1.Font = new System.Drawing.Font("Segoe UI", 14.25F, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point, ((byte)(0)));
this.label1.ForeColor = System.Drawing.Color.White;
this.label1.Location = new System.Drawing.Point(0, 0);
this.label1.Name = "label1";
this.label1.Size = new System.Drawing.Size(994, 34);
this.label1.TabIndex = 2;
this.label1.Text = "Items";
this.label1.TextAlign = System.Drawing.ContentAlignment.MiddleCenter;
label1.BackColor = Color.LightSlateGray;
label1.Dock = DockStyle.Top;
label1.Font = new Font("Segoe UI", 14.25F, FontStyle.Bold, GraphicsUnit.Point, 0);
label1.ForeColor = Color.White;
label1.Location = new Point(0, 0);
label1.Name = "label1";
label1.Size = new Size(959, 34);
label1.TabIndex = 2;
label1.Text = "Items";
label1.TextAlign = ContentAlignment.MiddleCenter;
//
// uIItemBindingSource
//
this.uIItemBindingSource.DataSource = typeof(CutList.Models.PartInputItem);
uIItemBindingSource.DataSource = typeof(Models.PartInputItem);
//
// menuStrip1
//
this.menuStrip1.Items.AddRange(new System.Windows.Forms.ToolStripItem[] {
this.saveToolStripMenuItem});
this.menuStrip1.Location = new System.Drawing.Point(0, 0);
this.menuStrip1.Name = "menuStrip1";
this.menuStrip1.Size = new System.Drawing.Size(994, 24);
this.menuStrip1.TabIndex = 5;
this.menuStrip1.Text = "menuStrip1";
menuStrip1.Items.AddRange(new ToolStripItem[] { saveToolStripMenuItem });
menuStrip1.Location = new Point(0, 0);
menuStrip1.Name = "menuStrip1";
menuStrip1.Size = new Size(959, 24);
menuStrip1.TabIndex = 5;
menuStrip1.Text = "menuStrip1";
//
// saveToolStripMenuItem
//
this.saveToolStripMenuItem.Name = "saveToolStripMenuItem";
this.saveToolStripMenuItem.Size = new System.Drawing.Size(43, 20);
this.saveToolStripMenuItem.Text = "Save";
this.saveToolStripMenuItem.Click += new System.EventHandler(this.saveToolStripMenuItem_Click);
saveToolStripMenuItem.Name = "saveToolStripMenuItem";
saveToolStripMenuItem.Size = new Size(43, 20);
saveToolStripMenuItem.Text = "Save";
saveToolStripMenuItem.Click += saveToolStripMenuItem_Click;
//
// ResultsForm
//
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.None;
this.ClientSize = new System.Drawing.Size(994, 531);
this.Controls.Add(this.splitContainer1);
this.Controls.Add(this.menuStrip1);
this.Font = new System.Drawing.Font("Segoe UI", 9.75F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(0)));
this.Name = "ResultsForm";
this.ShowIcon = false;
this.ShowInTaskbar = false;
this.StartPosition = System.Windows.Forms.FormStartPosition.CenterParent;
this.Text = "Results";
this.WindowState = System.Windows.Forms.FormWindowState.Maximized;
((System.ComponentModel.ISupportInitialize)(this.dataGridView1)).EndInit();
((System.ComponentModel.ISupportInitialize)(this.binBindingSource)).EndInit();
this.splitContainer1.Panel1.ResumeLayout(false);
this.splitContainer1.Panel2.ResumeLayout(false);
((System.ComponentModel.ISupportInitialize)(this.splitContainer1)).EndInit();
this.splitContainer1.ResumeLayout(false);
this.splitContainer2.Panel1.ResumeLayout(false);
this.splitContainer2.Panel2.ResumeLayout(false);
((System.ComponentModel.ISupportInitialize)(this.splitContainer2)).EndInit();
this.splitContainer2.ResumeLayout(false);
((System.ComponentModel.ISupportInitialize)(this.dataGridView2)).EndInit();
((System.ComponentModel.ISupportInitialize)(this.uIItemBindingSource)).EndInit();
this.menuStrip1.ResumeLayout(false);
this.menuStrip1.PerformLayout();
this.ResumeLayout(false);
this.PerformLayout();
AutoScaleMode = AutoScaleMode.None;
ClientSize = new Size(959, 747);
Controls.Add(splitContainer1);
Controls.Add(menuStrip1);
Font = new Font("Segoe UI", 9.75F, FontStyle.Regular, GraphicsUnit.Point, 0);
Name = "ResultsForm";
ShowIcon = false;
ShowInTaskbar = false;
StartPosition = FormStartPosition.CenterParent;
Text = "Results";
((System.ComponentModel.ISupportInitialize)dataGridView1).EndInit();
((System.ComponentModel.ISupportInitialize)binBindingSource).EndInit();
splitContainer1.Panel1.ResumeLayout(false);
splitContainer1.Panel2.ResumeLayout(false);
((System.ComponentModel.ISupportInitialize)splitContainer1).EndInit();
splitContainer1.ResumeLayout(false);
splitContainer2.Panel1.ResumeLayout(false);
splitContainer2.Panel2.ResumeLayout(false);
((System.ComponentModel.ISupportInitialize)splitContainer2).EndInit();
splitContainer2.ResumeLayout(false);
((System.ComponentModel.ISupportInitialize)dataGridView2).EndInit();
((System.ComponentModel.ISupportInitialize)uIItemBindingSource).EndInit();
menuStrip1.ResumeLayout(false);
menuStrip1.PerformLayout();
ResumeLayout(false);
PerformLayout();
}

View File

@@ -5,15 +5,19 @@ namespace CutList.Forms
public partial class ResultsForm : Form
{
private string filename;
private string cutMethod;
private string? materialShape;
private List<Bin> _originalBins = new List<Bin>();
public ResultsForm(string filename)
public ResultsForm(string filename, string cutMethod, string? materialShape = null)
{
InitializeComponent();
dataGridView1.DrawRowNumbers();
dataGridView2.DrawRowNumbers();
this.filename = filename;
this.cutMethod = cutMethod;
this.materialShape = materialShape;
}
private void dataGridView1_RowEnter(object sender, DataGridViewCellEventArgs e)
@@ -48,7 +52,11 @@ namespace CutList.Forms
public void Save(string filepath)
{
var writer = new BinFileSaver(_originalBins);
var writer = new BinFileSaver(_originalBins)
{
CutMethod = cutMethod,
MaterialShape = materialShape
};
writer.SaveBinsToFile(filepath);
}

View File

@@ -1,17 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
@@ -26,36 +26,36 @@
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->

View File

@@ -25,6 +25,11 @@ namespace CutList.Presenters
/// </summary>
Tool SelectedTool { get; }
/// <summary>
/// Gets the currently selected material shape from the view.
/// </summary>
string? SelectedMaterialShape { get; }
/// <summary>
/// Displays an error message to the user.
/// </summary>
@@ -77,7 +82,11 @@ namespace CutList.Presenters
/// <summary>
/// Shows the results form with the packing results.
/// </summary>
void ShowResults(List<Bin> bins, string fileName);
/// <param name="bins">The packed bins to display</param>
/// <param name="fileName">Default filename for saving</param>
/// <param name="cutMethod">The cutting method/tool name</param>
/// <param name="materialShape">The material shape (optional)</param>
void ShowResults(List<Bin> bins, string fileName, string cutMethod, string? materialShape = null);
/// <summary>
/// Updates the enabled state of the run button.
@@ -88,5 +97,11 @@ namespace CutList.Presenters
/// Clears all data in the view.
/// </summary>
void ClearData();
/// <summary>
/// Updates the window title to reflect the current document state.
/// </summary>
/// <param name="fileName">The file name to display, or null for a new document</param>
void UpdateWindowTitle(string? fileName);
}
}

View File

@@ -13,6 +13,7 @@ namespace CutList.Presenters
private readonly CutListService _cutListService;
private readonly DocumentService _documentService;
private Document _currentDocument;
private int _documentCounter = 0;
public MainFormPresenter(IMainView view, CutListService cutListService, DocumentService documentService)
{
@@ -40,11 +41,13 @@ namespace CutList.Presenters
_currentDocument = loadResult.Value;
_view.LoadDocumentData(_currentDocument.PartsToNest, _currentDocument.StockBins);
_view.UpdateWindowTitle(Path.GetFileName(filePath));
UpdateRunButtonState();
}
/// <summary>
/// Handles the "Save" operation to save the current document to file.
/// Handles the "Save" operation. If the document has a known path, saves directly.
/// Otherwise, prompts for a file location (same as Save As).
/// </summary>
public void SaveDocument()
{
@@ -57,19 +60,59 @@ namespace CutList.Presenters
return;
}
var defaultFileName = _currentDocument.LastFilePath == null
? "NewDocument.json"
// If we have a known path, save directly without prompting
if (!string.IsNullOrEmpty(_currentDocument.LastFilePath))
{
SaveToPath(_currentDocument.LastFilePath);
return;
}
// No known path - prompt for location (same as Save As)
SaveDocumentAs();
}
/// <summary>
/// Handles the "Save As" operation. Always prompts for a file location.
/// </summary>
public void SaveDocumentAs()
{
SyncDocumentFromView();
var validationResult = _documentService.Validate(_currentDocument);
if (validationResult.IsFailure)
{
_view.ShowWarning(validationResult.Error);
return;
}
var defaultFileName = string.IsNullOrEmpty(_currentDocument.LastFilePath)
? GenerateDefaultFileName()
: Path.GetFileName(_currentDocument.LastFilePath);
if (!_view.PromptSaveFile("Json File|*.json", defaultFileName, out string filePath))
return;
SaveToPath(filePath);
}
private void SaveToPath(string filePath)
{
var saveResult = _documentService.Save(_currentDocument, filePath);
if (saveResult.IsFailure)
{
_view.ShowError(saveResult.Error);
return;
}
_currentDocument.LastFilePath = filePath;
_view.UpdateWindowTitle(Path.GetFileName(filePath));
}
private string GenerateDefaultFileName()
{
_documentCounter++;
return $"CutList_{_documentCounter}.json";
}
/// <summary>
@@ -90,7 +133,9 @@ namespace CutList.Presenters
}
var fileName = GetResultsSaveName();
_view.ShowResults(packResult.Value.Bins.ToList(), fileName);
var cutMethod = cutTool?.Name ?? "Unknown";
var materialShape = _view.SelectedMaterialShape;
_view.ShowResults(packResult.Value.Bins.ToList(), fileName, cutMethod, materialShape);
}
/// <summary>
@@ -100,6 +145,7 @@ namespace CutList.Presenters
{
_currentDocument = new Document();
_view.ClearData();
_view.UpdateWindowTitle(null);
UpdateRunButtonState();
}
@@ -200,11 +246,14 @@ namespace CutList.Presenters
private string GetResultsSaveName()
{
var today = DateTime.Today;
var year = today.Year.ToString();
var month = today.Month.ToString().PadLeft(2, '0');
var day = today.Day.ToString().PadLeft(2, '0');
return $"Cut List {year}-{month}-{day}";
// Use document name if available, otherwise generate one
if (!string.IsNullOrEmpty(_currentDocument.LastFilePath))
{
var docName = Path.GetFileNameWithoutExtension(_currentDocument.LastFilePath);
return docName;
}
return $"CutList_{_documentCounter}";
}
}
}

View File

@@ -41,6 +41,7 @@ namespace CutList.Services
{
var json = File.ReadAllText(filePath);
var document = JsonConvert.DeserializeObject<Document>(json);
document.LastFilePath = filePath;
return Result<Document>.Success(document);
}
catch (Exception ex)