Compare commits
20 Commits
b19ecf3610
...
8e73d630d5
| Author | SHA1 | Date | |
|---|---|---|---|
| 8e73d630d5 | |||
| 079f5b1085 | |||
| 97fa90357b | |||
| bf6c4764ed | |||
| ed911a13ba | |||
| c99de55fe1 | |||
| 8b16cbd79f | |||
| cad5ab790a | |||
| f8020549fe | |||
| 66ed19a1ac | |||
| 051b866c6d | |||
| 3d80adbfff | |||
| b7b98d4338 | |||
| ced272d3e3 | |||
| 35b26e673e | |||
| cca569ae81 | |||
| fa36d82285 | |||
| b0c9470bb7 | |||
| 9868df162d | |||
| 6db8ab21f4 |
@@ -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, '=');
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
35
CutList.Web/Components/App.razor
Normal file
35
CutList.Web/Components/App.razor
Normal 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>
|
||||
13
CutList.Web/Components/Layout/MainLayout.razor
Normal file
13
CutList.Web/Components/Layout/MainLayout.razor
Normal 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>
|
||||
37
CutList.Web/Components/Layout/NavMenu.razor
Normal file
37
CutList.Web/Components/Layout/NavMenu.razor
Normal 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>
|
||||
111
CutList.Web/Components/Layout/NavMenu.razor.css
Normal file
111
CutList.Web/Components/Layout/NavMenu.razor.css
Normal 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;
|
||||
}
|
||||
}
|
||||
58
CutList.Web/Components/Pages/Home.razor
Normal file
58
CutList.Web/Components/Pages/Home.razor
Normal 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>
|
||||
331
CutList.Web/Components/Pages/Materials/Edit.razor
Normal file
331
CutList.Web/Components/Pages/Materials/Edit.razor
Normal 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" OD x 0.065 wall" />
|
||||
<ValidationMessage For="@(() => material.Size)" />
|
||||
<div class="form-text">Examples: "1" 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
84
CutList.Web/Components/Pages/Materials/Index.razor
Normal file
84
CutList.Web/Components/Pages/Materials/Index.razor
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
399
CutList.Web/Components/Pages/Projects/Edit.razor
Normal file
399
CutList.Web/Components/Pages/Projects/Edit.razor
Normal 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))!;
|
||||
}
|
||||
}
|
||||
94
CutList.Web/Components/Pages/Projects/Index.razor
Normal file
94
CutList.Web/Components/Pages/Projects/Index.razor
Normal 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}");
|
||||
}
|
||||
}
|
||||
257
CutList.Web/Components/Pages/Projects/Results.razor
Normal file
257
CutList.Web/Components/Pages/Projects/Results.razor
Normal 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);
|
||||
}
|
||||
}
|
||||
328
CutList.Web/Components/Pages/Suppliers/Edit.razor
Normal file
328
CutList.Web/Components/Pages/Suppliers/Edit.razor
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
91
CutList.Web/Components/Pages/Suppliers/Index.razor
Normal file
91
CutList.Web/Components/Pages/Suppliers/Index.razor
Normal 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) + "...";
|
||||
}
|
||||
}
|
||||
220
CutList.Web/Components/Pages/Tools/Index.razor
Normal file
220
CutList.Web/Components/Pages/Tools/Index.razor
Normal 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.####}\")";
|
||||
}
|
||||
}
|
||||
6
CutList.Web/Components/Routes.razor
Normal file
6
CutList.Web/Components/Routes.razor
Normal 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>
|
||||
66
CutList.Web/Components/Shared/ConfirmDialog.razor
Normal file
66
CutList.Web/Components/Shared/ConfirmDialog.razor
Normal 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();
|
||||
}
|
||||
}
|
||||
88
CutList.Web/Components/Shared/CutListReport.razor
Normal file
88
CutList.Web/Components/Shared/CutListReport.razor
Normal 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;
|
||||
}
|
||||
75
CutList.Web/Components/Shared/LengthInput.razor
Normal file
75
CutList.Web/Components/Shared/LengthInput.razor
Normal 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);
|
||||
}
|
||||
}
|
||||
14
CutList.Web/Components/_Imports.razor
Normal file
14
CutList.Web/Components/_Imports.razor
Normal 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
|
||||
170
CutList.Web/Controllers/MaterialsController.cs
Normal file
170
CutList.Web/Controllers/MaterialsController.cs
Normal 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();
|
||||
}
|
||||
94
CutList.Web/Controllers/SeedController.cs
Normal file
94
CutList.Web/Controllers/SeedController.cs
Normal 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
|
||||
});
|
||||
}
|
||||
}
|
||||
21
CutList.Web/CutList.Web.csproj
Normal file
21
CutList.Web/CutList.Web.csproj
Normal 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>
|
||||
128
CutList.Web/Data/ApplicationDbContext.cs
Normal file
128
CutList.Web/Data/ApplicationDbContext.cs
Normal 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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
12
CutList.Web/Data/Entities/CuttingTool.cs
Normal file
12
CutList.Web/Data/Entities/CuttingTool.cs
Normal 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>();
|
||||
}
|
||||
18
CutList.Web/Data/Entities/Material.cs
Normal file
18
CutList.Web/Data/Entities/Material.cs
Normal 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}";
|
||||
}
|
||||
13
CutList.Web/Data/Entities/MaterialStockLength.cs
Normal file
13
CutList.Web/Data/Entities/MaterialStockLength.cs
Normal 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!;
|
||||
}
|
||||
15
CutList.Web/Data/Entities/Project.cs
Normal file
15
CutList.Web/Data/Entities/Project.cs
Normal 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>();
|
||||
}
|
||||
15
CutList.Web/Data/Entities/ProjectPart.cs
Normal file
15
CutList.Web/Data/Entities/ProjectPart.cs
Normal 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!;
|
||||
}
|
||||
13
CutList.Web/Data/Entities/Supplier.cs
Normal file
13
CutList.Web/Data/Entities/Supplier.cs
Normal 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>();
|
||||
}
|
||||
15
CutList.Web/Data/Entities/SupplierStock.cs
Normal file
15
CutList.Web/Data/Entities/SupplierStock.cs
Normal 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!;
|
||||
}
|
||||
387
CutList.Web/Migrations/20260202024820_InitialCreate.Designer.cs
generated
Normal file
387
CutList.Web/Migrations/20260202024820_InitialCreate.Designer.cs
generated
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
241
CutList.Web/Migrations/20260202024820_InitialCreate.cs
Normal file
241
CutList.Web/Migrations/20260202024820_InitialCreate.cs
Normal 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");
|
||||
}
|
||||
}
|
||||
}
|
||||
430
CutList.Web/Migrations/20260202033321_AddMaterialStockLengths.Designer.cs
generated
Normal file
430
CutList.Web/Migrations/20260202033321_AddMaterialStockLengths.Designer.cs
generated
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
433
CutList.Web/Migrations/20260202041453_AddProjectCustomer.Designer.cs
generated
Normal file
433
CutList.Web/Migrations/20260202041453_AddProjectCustomer.Designer.cs
generated
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
28
CutList.Web/Migrations/20260202041453_AddProjectCustomer.cs
Normal file
28
CutList.Web/Migrations/20260202041453_AddProjectCustomer.cs
Normal 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");
|
||||
}
|
||||
}
|
||||
}
|
||||
394
CutList.Web/Migrations/20260202043251_MultiMaterialProjectParts.Designer.cs
generated
Normal file
394
CutList.Web/Migrations/20260202043251_MultiMaterialProjectParts.Designer.cs
generated
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
391
CutList.Web/Migrations/ApplicationDbContextModelSnapshot.cs
Normal file
391
CutList.Web/Migrations/ApplicationDbContextModelSnapshot.cs
Normal 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
41
CutList.Web/Program.cs
Normal 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();
|
||||
22
CutList.Web/Properties/launchSettings.json
Normal file
22
CutList.Web/Properties/launchSettings.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
260
CutList.Web/Services/CutListPackingService.cs
Normal file
260
CutList.Web/Services/CutListPackingService.cs
Normal 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; }
|
||||
}
|
||||
122
CutList.Web/Services/MaterialService.cs
Normal file
122
CutList.Web/Services/MaterialService.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
213
CutList.Web/Services/ProjectService.cs
Normal file
213
CutList.Web/Services/ProjectService.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
35
CutList.Web/Services/ReportService.cs
Normal file
35
CutList.Web/Services/ReportService.cs
Normal 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; }
|
||||
}
|
||||
126
CutList.Web/Services/SupplierService.cs
Normal file
126
CutList.Web/Services/SupplierService.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
8
CutList.Web/appsettings.Development.json
Normal file
8
CutList.Web/appsettings.Development.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
}
|
||||
}
|
||||
13
CutList.Web/appsettings.json
Normal file
13
CutList.Web/appsettings.json
Normal 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"
|
||||
}
|
||||
}
|
||||
183
CutList.Web/wwwroot/css/app.css
Normal file
183
CutList.Web/wwwroot/css/app.css
Normal 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;
|
||||
}
|
||||
}
|
||||
6
CutList.Web/wwwroot/css/bootstrap/bootstrap.min.css
vendored
Normal file
6
CutList.Web/wwwroot/css/bootstrap/bootstrap.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
276
CutList.Web/wwwroot/css/report.css
Normal file
276
CutList.Web/wwwroot/css/report.css
Normal 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;
|
||||
}
|
||||
}
|
||||
14
CutList.sln
14
CutList.sln
@@ -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
|
||||
|
||||
98
CutList/Forms/MainForm.Designer.cs
generated
98
CutList/Forms/MainForm.Designer.cs
generated
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
340
CutList/Forms/ResultsForm.Designer.cs
generated
340
CutList/Forms/ResultsForm.Designer.cs
generated
@@ -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();
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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.
|
||||
-->
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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}";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user