Compare commits
14 Commits
5cc088ea6b
...
21d50e7c20
| Author | SHA1 | Date | |
|---|---|---|---|
| 21d50e7c20 | |||
| f723661696 | |||
| c795c129e5 | |||
| 30071469bc | |||
| c9a2583f26 | |||
| 0e5b63c557 | |||
| 6388e003d3 | |||
| c5da5dda98 | |||
| 21cddb22c7 | |||
| 3b036308c8 | |||
| 4f6d986dc9 | |||
| 254066c989 | |||
| ce14dd50cb | |||
| dfc767320a |
Binary file not shown.
Binary file not shown.
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"runtimeOptions": {
|
||||||
|
"tfm": "net8.0",
|
||||||
|
"frameworks": [
|
||||||
|
{
|
||||||
|
"name": "Microsoft.NETCore.App",
|
||||||
|
"version": "8.0.0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Microsoft.AspNetCore.App",
|
||||||
|
"version": "8.0.0"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"configProperties": {
|
||||||
|
"System.Reflection.Metadata.MetadataUpdater.IsSupported": false,
|
||||||
|
"System.Reflection.NullabilityInfoContext.IsSupported": true,
|
||||||
|
"System.Runtime.Serialization.EnableUnsafeBinaryFormatterSerialization": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"runtimeOptions": {
|
||||||
|
"tfm": "net8.0",
|
||||||
|
"frameworks": [
|
||||||
|
{
|
||||||
|
"name": "Microsoft.NETCore.App",
|
||||||
|
"version": "8.0.0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Microsoft.AspNetCore.App",
|
||||||
|
"version": "8.0.0"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"configProperties": {
|
||||||
|
"System.GC.Server": true,
|
||||||
|
"System.Reflection.Metadata.MetadataUpdater.IsSupported": false,
|
||||||
|
"System.Reflection.NullabilityInfoContext.IsSupported": true,
|
||||||
|
"System.Runtime.Serialization.EnableUnsafeBinaryFormatterSerialization": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"Logging": {
|
||||||
|
"LogLevel": {
|
||||||
|
"Default": "Information",
|
||||||
|
"Microsoft.AspNetCore": "Warning"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,28 @@
|
|||||||
|
{
|
||||||
|
"permissions": {
|
||||||
|
"allow": [
|
||||||
|
"Skill(roslyn-bridge)",
|
||||||
|
"Bash(dotnet build:*)",
|
||||||
|
"SlashCommand(/rb)",
|
||||||
|
"mcp__roslyn-bridge__get_projects",
|
||||||
|
"Bash(dotnet tool install:*)",
|
||||||
|
"Bash(dotnet ilspy:*)",
|
||||||
|
"Bash(dotnet add package:*)",
|
||||||
|
"Bash(git -C /c/Users/AJ/Desktop/Projects/CutList add CutList.Core/BinComparer.cs CutList.Core/BinItem.cs CutList.Core/MultiBin.cs CutList/Tool.cs)",
|
||||||
|
"Bash(git -C /c/Users/AJ/Desktop/Projects/CutList commit --amend --no-edit)",
|
||||||
|
"mcp__roslyn-bridge__get_code_smells",
|
||||||
|
"mcp__roslyn-bridge__get_duplicates",
|
||||||
|
"mcp__roslyn-bridge__get_code_smell_summary",
|
||||||
|
"mcp__cutlist__create_cutlist",
|
||||||
|
"Bash(dotnet run:*)",
|
||||||
|
"mcp__roslyn-bridge__get_files",
|
||||||
|
"mcp__roslyn-bridge__refresh_workspace",
|
||||||
|
"mcp__roslyn-bridge__get_diagnostics",
|
||||||
|
"Bash(dotnet ef database update:*)",
|
||||||
|
"mcp__roslyn-bridge__search_symbol",
|
||||||
|
"Bash(dotnet ef migrations add:*)"
|
||||||
|
],
|
||||||
|
"deny": [],
|
||||||
|
"ask": []
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -28,7 +28,17 @@ namespace CutList.Core.Formatting
|
|||||||
var match2 = regex.Match(input);
|
var match2 = regex.Match(input);
|
||||||
|
|
||||||
if (!match2.Success)
|
if (!match2.Success)
|
||||||
|
{
|
||||||
|
// If no unit symbols, try to parse as plain inches (e.g., "0.5" or "1/2" converted to "0.5")
|
||||||
|
if (!input.Contains("'") && !input.Contains("\""))
|
||||||
|
{
|
||||||
|
if (double.TryParse(input.Trim(), out var plainInches))
|
||||||
|
{
|
||||||
|
return Math.Round(plainInches, 8);
|
||||||
|
}
|
||||||
|
}
|
||||||
throw new Exception("Input is not in a valid format.");
|
throw new Exception("Input is not in a valid format.");
|
||||||
|
}
|
||||||
|
|
||||||
var feet = match2.Groups["Feet"];
|
var feet = match2.Groups["Feet"];
|
||||||
var inches = match2.Groups["Inches"];
|
var inches = match2.Groups["Inches"];
|
||||||
|
|||||||
@@ -39,6 +39,12 @@ namespace CutList.Core.Formatting
|
|||||||
return wholeNumber.ToString();
|
return wholeNumber.ToString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If whole number is 0, just show the fraction
|
||||||
|
if (wholeNumber == 0)
|
||||||
|
{
|
||||||
|
return $"{numerator}/{denominator}";
|
||||||
|
}
|
||||||
|
|
||||||
return $"{wholeNumber}-{numerator}/{denominator}";
|
return $"{wholeNumber}-{numerator}/{denominator}";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\CutList.Core\CutList.Core.csproj" />
|
<ProjectReference Include="..\CutList.Core\CutList.Core.csproj" />
|
||||||
|
<ProjectReference Include="..\CutList.Web\CutList.Web.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,9 +1,15 @@
|
|||||||
|
using CutList.Web.Data;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.Hosting;
|
using Microsoft.Extensions.Hosting;
|
||||||
using ModelContextProtocol.Server;
|
using ModelContextProtocol.Server;
|
||||||
|
|
||||||
var builder = Host.CreateApplicationBuilder(args);
|
var builder = Host.CreateApplicationBuilder(args);
|
||||||
|
|
||||||
|
// Add DbContext for inventory tools
|
||||||
|
builder.Services.AddDbContext<ApplicationDbContext>(options =>
|
||||||
|
options.UseSqlServer("Server=(localdb)\\mssqllocaldb;Database=CutListDb;Trusted_Connection=True;MultipleActiveResultSets=true"));
|
||||||
|
|
||||||
builder.Services
|
builder.Services
|
||||||
.AddMcpServer()
|
.AddMcpServer()
|
||||||
.WithStdioServerTransport()
|
.WithStdioServerTransport()
|
||||||
|
|||||||
@@ -14,8 +14,8 @@
|
|||||||
</NavLink>
|
</NavLink>
|
||||||
</div>
|
</div>
|
||||||
<div class="nav-item px-3">
|
<div class="nav-item px-3">
|
||||||
<NavLink class="nav-link" href="projects">
|
<NavLink class="nav-link" href="jobs">
|
||||||
<span class="bi bi-list-check-nav-menu" aria-hidden="true"></span> Projects
|
<span class="bi bi-list-check-nav-menu" aria-hidden="true"></span> Jobs
|
||||||
</NavLink>
|
</NavLink>
|
||||||
</div>
|
</div>
|
||||||
<div class="nav-item px-3">
|
<div class="nav-item px-3">
|
||||||
|
|||||||
@@ -46,6 +46,10 @@
|
|||||||
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");
|
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-boxes-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-boxes' viewBox='0 0 16 16'%3E%3Cpath d='M7.752.066a.5.5 0 0 1 .496 0l3.75 2.143a.5.5 0 0 1 .252.434v3.995l3.498 2A.5.5 0 0 1 16 9.07v4.286a.5.5 0 0 1-.252.434l-3.75 2.143a.5.5 0 0 1-.496 0l-3.502-2-3.502 2.001a.5.5 0 0 1-.496 0l-3.75-2.143A.5.5 0 0 1 0 13.357V9.071a.5.5 0 0 1 .252-.434L3.75 6.638V2.643a.5.5 0 0 1 .252-.434zM4.25 7.504 1.508 9.071l2.742 1.567 2.742-1.567zM7.5 9.933l-2.75 1.571v3.134l2.75-1.571zm1 3.134 2.75 1.571v-3.134L8.5 9.933zm.508-3.996 2.742 1.567 2.742-1.567-2.742-1.567zm2.242-2.433V3.504L8.5 5.076V8.21zM7.5 8.21V5.076L4.75 3.504v3.134zM5.258 2.643 8 4.21l2.742-1.567L8 1.076zM15 9.933l-2.75 1.571v3.134L15 13.067zM3.75 14.638v-3.134L1 9.933v3.134z'/%3E%3C/svg%3E");
|
||||||
|
}
|
||||||
|
|
||||||
.bi-building-nav-menu {
|
.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");
|
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");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,9 +10,9 @@
|
|||||||
<div class="col-md-6 col-lg-3 mb-4">
|
<div class="col-md-6 col-lg-3 mb-4">
|
||||||
<div class="card h-100">
|
<div class="card h-100">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h5 class="card-title">Projects</h5>
|
<h5 class="card-title">Jobs</h5>
|
||||||
<p class="card-text">Create and manage cut list projects. Add parts and stock bins, then optimize to minimize waste.</p>
|
<p class="card-text">Create and manage cut list jobs. Add parts and stock bins, then optimize to minimize waste.</p>
|
||||||
<a href="projects" class="btn btn-primary">Go to Projects</a>
|
<a href="jobs" class="btn btn-primary">Go to Jobs</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -51,7 +51,7 @@
|
|||||||
<ol>
|
<ol>
|
||||||
<li><strong>Set up materials</strong> - Define the shapes and sizes of materials you work with</li>
|
<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>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>Create a job</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>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>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>
|
<li><strong>Print report</strong> - Generate a printable cut list to take to the shop</li>
|
||||||
|
|||||||
@@ -0,0 +1,822 @@
|
|||||||
|
@page "/jobs/new"
|
||||||
|
@page "/jobs/{Id:int}"
|
||||||
|
@inject JobService JobService
|
||||||
|
@inject MaterialService MaterialService
|
||||||
|
@inject StockItemService StockItemService
|
||||||
|
@inject NavigationManager Navigation
|
||||||
|
@using CutList.Core.Formatting
|
||||||
|
@using CutList.Web.Data.Entities
|
||||||
|
|
||||||
|
<PageTitle>@(IsNew ? "New Job" : job.DisplayName)</PageTitle>
|
||||||
|
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||||
|
<h1>@(IsNew ? "New Job" : job.DisplayName)</h1>
|
||||||
|
@if (!IsNew)
|
||||||
|
{
|
||||||
|
<a href="jobs/@Id/results" class="btn btn-success">Run Optimization</a>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if (loading)
|
||||||
|
{
|
||||||
|
<p><em>Loading...</em></p>
|
||||||
|
}
|
||||||
|
else if (IsNew)
|
||||||
|
{
|
||||||
|
<!-- New Job: Simple form -->
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-lg-6">
|
||||||
|
@RenderDetailsForm()
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<!-- Existing Job: 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 (job.Parts.Count > 0)
|
||||||
|
{
|
||||||
|
<span class="badge bg-secondary ms-1">@job.Parts.Sum(p => p.Quantity)</span>
|
||||||
|
}
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item" role="presentation">
|
||||||
|
<button class="nav-link @(activeTab == Tab.Stock ? "active" : "")"
|
||||||
|
@onclick="() => SetTab(Tab.Stock)" type="button">
|
||||||
|
Stock
|
||||||
|
@if (job.Stock.Count > 0)
|
||||||
|
{
|
||||||
|
<span class="badge bg-secondary ms-1">@job.Stock.Count</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()
|
||||||
|
}
|
||||||
|
else if (activeTab == Tab.Stock)
|
||||||
|
{
|
||||||
|
@RenderStockTab()
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
@* Part Modal Dialog *@
|
||||||
|
@if (showPartForm)
|
||||||
|
{
|
||||||
|
<div class="modal fade show d-block" tabindex="-1" style="background-color: rgba(0,0,0,0.5);">
|
||||||
|
<div class="modal-dialog modal-lg">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title">@(editingPart == null ? "Add Part" : "Edit Part")</h5>
|
||||||
|
<button type="button" class="btn-close" @onclick="CancelPartForm"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="row g-3">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<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.GetDisplayName()</option>
|
||||||
|
}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label">Size</label>
|
||||||
|
<select class="form-select" @bind="newPart.MaterialId" disabled="@(!selectedShape.HasValue)">
|
||||||
|
<option value="0">-- Select --</option>
|
||||||
|
@foreach (var material in FilteredMaterials)
|
||||||
|
{
|
||||||
|
<option value="@material.Id">@material.Size</option>
|
||||||
|
}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label">Length</label>
|
||||||
|
<LengthInput @bind-Value="newPart.LengthInches" />
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label">Quantity</label>
|
||||||
|
<input type="number" class="form-control" @bind="newPart.Quantity" min="1" />
|
||||||
|
</div>
|
||||||
|
<div class="col-12">
|
||||||
|
<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>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-outline-secondary" @onclick="CancelPartForm">Cancel</button>
|
||||||
|
<button type="button" class="btn btn-primary" @onclick="SavePartAsync">
|
||||||
|
@(editingPart == null ? "Add Part" : "Save Changes")
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
@code {
|
||||||
|
private enum Tab { Details, Parts, Stock }
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public int? Id { get; set; }
|
||||||
|
|
||||||
|
private Job job = new();
|
||||||
|
private List<Material> materials = new();
|
||||||
|
private List<CuttingTool> cuttingTools = new();
|
||||||
|
|
||||||
|
private bool loading = true;
|
||||||
|
private bool savingJob;
|
||||||
|
private string? jobErrorMessage;
|
||||||
|
private Tab activeTab = Tab.Details;
|
||||||
|
|
||||||
|
private void SetTab(Tab tab) => activeTab = tab;
|
||||||
|
|
||||||
|
// Parts form
|
||||||
|
private bool showPartForm;
|
||||||
|
private JobPart newPart = new();
|
||||||
|
private JobPart? editingPart;
|
||||||
|
private string? partErrorMessage;
|
||||||
|
private MaterialShape? selectedShape;
|
||||||
|
|
||||||
|
// Stock form
|
||||||
|
private bool showStockForm;
|
||||||
|
private bool showCustomStockForm;
|
||||||
|
private JobStock newStock = new();
|
||||||
|
private JobStock? editingStock;
|
||||||
|
private string? stockErrorMessage;
|
||||||
|
private MaterialShape? stockSelectedShape;
|
||||||
|
private int stockSelectedMaterialId;
|
||||||
|
private List<StockItem> availableStockItems = new();
|
||||||
|
|
||||||
|
private IEnumerable<MaterialShape> DistinctShapes => materials.Select(m => m.Shape).Distinct().OrderBy(s => s);
|
||||||
|
private IEnumerable<Material> FilteredMaterials => !selectedShape.HasValue
|
||||||
|
? Enumerable.Empty<Material>()
|
||||||
|
: materials.Where(m => m.Shape == selectedShape.Value).OrderBy(m => m.SortOrder).ThenBy(m => m.Size);
|
||||||
|
|
||||||
|
private bool IsNew => !Id.HasValue;
|
||||||
|
|
||||||
|
protected override async Task OnInitializedAsync()
|
||||||
|
{
|
||||||
|
materials = await MaterialService.GetAllAsync();
|
||||||
|
cuttingTools = await JobService.GetCuttingToolsAsync();
|
||||||
|
|
||||||
|
if (Id.HasValue)
|
||||||
|
{
|
||||||
|
var existing = await JobService.GetByIdAsync(Id.Value);
|
||||||
|
if (existing == null)
|
||||||
|
{
|
||||||
|
Navigation.NavigateTo("jobs");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
job = existing;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Set default cutting tool for new jobs
|
||||||
|
var defaultTool = await JobService.GetDefaultCuttingToolAsync();
|
||||||
|
if (defaultTool != null)
|
||||||
|
{
|
||||||
|
job.CuttingToolId = defaultTool.Id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
loading = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private RenderFragment RenderDetailsForm() => __builder =>
|
||||||
|
{
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5 class="mb-0">Job Details</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<EditForm Model="job" OnValidSubmit="SaveJobAsync">
|
||||||
|
@if (!IsNew)
|
||||||
|
{
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Job Number</label>
|
||||||
|
<input type="text" class="form-control" value="@job.JobNumber" readonly />
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Job Name <span class="text-muted fw-normal">(optional)</span></label>
|
||||||
|
<InputText class="form-control" @bind-Value="job.Name" placeholder="Descriptive name for this job" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Customer <span class="text-muted fw-normal">(optional)</span></label>
|
||||||
|
<InputText class="form-control" @bind-Value="job.Customer" placeholder="Customer name" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Cutting Tool</label>
|
||||||
|
<InputSelect class="form-select" @bind-Value="job.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="job.Notes" rows="3" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if (!string.IsNullOrEmpty(jobErrorMessage))
|
||||||
|
{
|
||||||
|
<div class="alert alert-danger">@jobErrorMessage</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<div class="d-flex gap-2">
|
||||||
|
<button type="submit" class="btn btn-primary" disabled="@savingJob">
|
||||||
|
@if (savingJob)
|
||||||
|
{
|
||||||
|
<span class="spinner-border spinner-border-sm me-1"></span>
|
||||||
|
}
|
||||||
|
@(IsNew ? "Create Job" : "Save")
|
||||||
|
</button>
|
||||||
|
<a href="jobs" 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 (job.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 job.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: @job.Parts.Sum(p => p.Quantity) pieces
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
};
|
||||||
|
|
||||||
|
private async Task SaveJobAsync()
|
||||||
|
{
|
||||||
|
jobErrorMessage = null;
|
||||||
|
savingJob = true;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (IsNew)
|
||||||
|
{
|
||||||
|
var created = await JobService.CreateAsync(job);
|
||||||
|
Navigation.NavigateTo($"jobs/{created.Id}");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
await JobService.UpdateAsync(job);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
savingJob = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parts methods
|
||||||
|
private void ShowAddPartForm()
|
||||||
|
{
|
||||||
|
editingPart = null;
|
||||||
|
newPart = new JobPart { JobId = Id!.Value, Quantity = 1 };
|
||||||
|
selectedShape = null;
|
||||||
|
showPartForm = true;
|
||||||
|
partErrorMessage = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnShapeChanged()
|
||||||
|
{
|
||||||
|
newPart.MaterialId = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void EditPart(JobPart part)
|
||||||
|
{
|
||||||
|
editingPart = part;
|
||||||
|
newPart = new JobPart
|
||||||
|
{
|
||||||
|
Id = part.Id,
|
||||||
|
JobId = part.JobId,
|
||||||
|
MaterialId = part.MaterialId,
|
||||||
|
Name = part.Name,
|
||||||
|
LengthInches = part.LengthInches,
|
||||||
|
Quantity = part.Quantity,
|
||||||
|
SortOrder = part.SortOrder
|
||||||
|
};
|
||||||
|
selectedShape = part.Material?.Shape;
|
||||||
|
showPartForm = true;
|
||||||
|
partErrorMessage = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CancelPartForm()
|
||||||
|
{
|
||||||
|
showPartForm = false;
|
||||||
|
editingPart = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task SavePartAsync()
|
||||||
|
{
|
||||||
|
partErrorMessage = null;
|
||||||
|
|
||||||
|
if (!selectedShape.HasValue)
|
||||||
|
{
|
||||||
|
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 JobService.AddPartAsync(newPart);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
await JobService.UpdatePartAsync(newPart);
|
||||||
|
}
|
||||||
|
|
||||||
|
job = (await JobService.GetByIdAsync(Id!.Value))!;
|
||||||
|
showPartForm = false;
|
||||||
|
editingPart = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task DeletePart(JobPart part)
|
||||||
|
{
|
||||||
|
await JobService.DeletePartAsync(part.Id);
|
||||||
|
job = (await JobService.GetByIdAsync(Id!.Value))!;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stock tab
|
||||||
|
private RenderFragment RenderStockTab() => __builder =>
|
||||||
|
{
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header d-flex justify-content-between align-items-center">
|
||||||
|
<h5 class="mb-0">Stock for This Job</h5>
|
||||||
|
<div class="btn-group">
|
||||||
|
<button class="btn btn-primary" @onclick="ShowAddStockFromInventory">Add from Inventory</button>
|
||||||
|
<button class="btn btn-outline-primary" @onclick="ShowAddCustomStock">Add Custom Length</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
@if (showStockForm)
|
||||||
|
{
|
||||||
|
@RenderStockFromInventoryForm()
|
||||||
|
}
|
||||||
|
else if (showCustomStockForm)
|
||||||
|
{
|
||||||
|
@RenderCustomStockForm()
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (job.Stock.Count == 0)
|
||||||
|
{
|
||||||
|
<div class="text-center py-4 text-muted">
|
||||||
|
<p class="mb-2">No stock configured for this job.</p>
|
||||||
|
<p class="small">Add stock from your inventory or define custom lengths.</p>
|
||||||
|
<p class="small">If no stock is selected, the optimizer will use all available stock for the materials in your parts list.</p>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
@RenderStockTable()
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
};
|
||||||
|
|
||||||
|
private RenderFragment RenderStockFromInventoryForm() => __builder =>
|
||||||
|
{
|
||||||
|
<div class="border rounded p-3 mb-3 bg-light">
|
||||||
|
<h6>@(editingStock == null ? "Add Stock from Inventory" : "Edit Stock Selection")</h6>
|
||||||
|
<div class="row g-3">
|
||||||
|
<div class="col-md-3">
|
||||||
|
<label class="form-label">Shape</label>
|
||||||
|
<select class="form-select" @bind="stockSelectedShape" @bind:after="OnStockShapeChanged">
|
||||||
|
<option value="">-- Select --</option>
|
||||||
|
@foreach (var shape in DistinctShapes)
|
||||||
|
{
|
||||||
|
<option value="@shape">@shape.GetDisplayName()</option>
|
||||||
|
}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<label class="form-label">Size</label>
|
||||||
|
<select class="form-select" @bind="stockSelectedMaterialId" @bind:after="OnStockMaterialChanged"
|
||||||
|
disabled="@(!stockSelectedShape.HasValue)">
|
||||||
|
<option value="0">-- Select --</option>
|
||||||
|
@foreach (var material in materials.Where(m => stockSelectedShape.HasValue && m.Shape == stockSelectedShape.Value).OrderBy(m => m.SortOrder).ThenBy(m => m.Size))
|
||||||
|
{
|
||||||
|
<option value="@material.Id">@material.Size</option>
|
||||||
|
}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<label class="form-label">Stock Length</label>
|
||||||
|
<select class="form-select" @bind="newStock.StockItemId" disabled="@(stockSelectedMaterialId == 0)">
|
||||||
|
<option value="">-- Select --</option>
|
||||||
|
@foreach (var stock in availableStockItems)
|
||||||
|
{
|
||||||
|
<option value="@stock.Id">@ArchUnits.FormatFromInches((double)stock.LengthInches) (@stock.QuantityOnHand available)</option>
|
||||||
|
}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<label class="form-label">Qty to Use</label>
|
||||||
|
<input type="number" class="form-control" @bind="newStock.Quantity" min="1" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row g-3 mt-1">
|
||||||
|
<div class="col-md-3">
|
||||||
|
<label class="form-label">Priority</label>
|
||||||
|
<input type="number" class="form-control" @bind="newStock.Priority" min="1" />
|
||||||
|
<small class="text-muted">Lower = used first</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@if (!string.IsNullOrEmpty(stockErrorMessage))
|
||||||
|
{
|
||||||
|
<div class="alert alert-danger mt-3 mb-0">@stockErrorMessage</div>
|
||||||
|
}
|
||||||
|
<div class="mt-3 d-flex gap-2">
|
||||||
|
<button class="btn btn-primary" @onclick="SaveStockFromInventoryAsync">
|
||||||
|
@(editingStock == null ? "Add Stock" : "Save Changes")
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-outline-secondary" @onclick="CancelStockForm">Cancel</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
};
|
||||||
|
|
||||||
|
private RenderFragment RenderCustomStockForm() => __builder =>
|
||||||
|
{
|
||||||
|
<div class="border rounded p-3 mb-3 bg-light">
|
||||||
|
<h6>@(editingStock == null ? "Add Custom Stock Length" : "Edit Custom Stock")</h6>
|
||||||
|
<div class="row g-3">
|
||||||
|
<div class="col-md-3">
|
||||||
|
<label class="form-label">Shape</label>
|
||||||
|
<select class="form-select" @bind="stockSelectedShape" @bind:after="OnStockShapeChanged">
|
||||||
|
<option value="">-- Select --</option>
|
||||||
|
@foreach (var shape in DistinctShapes)
|
||||||
|
{
|
||||||
|
<option value="@shape">@shape.GetDisplayName()</option>
|
||||||
|
}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<label class="form-label">Size</label>
|
||||||
|
<select class="form-select" @bind="newStock.MaterialId" disabled="@(!stockSelectedShape.HasValue)">
|
||||||
|
<option value="0">-- Select --</option>
|
||||||
|
@foreach (var material in materials.Where(m => stockSelectedShape.HasValue && m.Shape == stockSelectedShape.Value).OrderBy(m => m.SortOrder).ThenBy(m => m.Size))
|
||||||
|
{
|
||||||
|
<option value="@material.Id">@material.Size</option>
|
||||||
|
}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<label class="form-label">Length</label>
|
||||||
|
<LengthInput @bind-Value="newStock.LengthInches" />
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<label class="form-label">Quantity</label>
|
||||||
|
<input type="number" class="form-control" @bind="newStock.Quantity" min="1" />
|
||||||
|
<small class="text-muted">Use -1 for unlimited</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row g-3 mt-1">
|
||||||
|
<div class="col-md-3">
|
||||||
|
<label class="form-label">Priority</label>
|
||||||
|
<input type="number" class="form-control" @bind="newStock.Priority" min="1" />
|
||||||
|
<small class="text-muted">Lower = used first</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@if (!string.IsNullOrEmpty(stockErrorMessage))
|
||||||
|
{
|
||||||
|
<div class="alert alert-danger mt-3 mb-0">@stockErrorMessage</div>
|
||||||
|
}
|
||||||
|
<div class="mt-3 d-flex gap-2">
|
||||||
|
<button class="btn btn-primary" @onclick="SaveCustomStockAsync">
|
||||||
|
@(editingStock == null ? "Add Stock" : "Save Changes")
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-outline-secondary" @onclick="CancelStockForm">Cancel</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
};
|
||||||
|
|
||||||
|
private RenderFragment RenderStockTable() => __builder =>
|
||||||
|
{
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-hover mb-0">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Material</th>
|
||||||
|
<th>Length</th>
|
||||||
|
<th>Qty</th>
|
||||||
|
<th>Priority</th>
|
||||||
|
<th>Source</th>
|
||||||
|
<th style="width: 120px;">Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
@foreach (var stock in job.Stock.OrderBy(s => s.Material?.Shape).ThenBy(s => s.Material?.Size).ThenBy(s => s.Priority))
|
||||||
|
{
|
||||||
|
<tr>
|
||||||
|
<td>@stock.Material?.DisplayName</td>
|
||||||
|
<td>@ArchUnits.FormatFromInches((double)stock.LengthInches)</td>
|
||||||
|
<td>@(stock.Quantity == -1 ? "Unlimited" : stock.Quantity.ToString())</td>
|
||||||
|
<td>@stock.Priority</td>
|
||||||
|
<td>
|
||||||
|
@if (stock.IsCustomLength)
|
||||||
|
{
|
||||||
|
<span class="badge bg-info">Custom</span>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<span class="badge bg-secondary">Inventory</span>
|
||||||
|
}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<button class="btn btn-sm btn-outline-primary me-1" @onclick="() => EditStock(stock)">Edit</button>
|
||||||
|
<button class="btn btn-sm btn-outline-danger" @onclick="() => DeleteStock(stock)">Delete</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
};
|
||||||
|
|
||||||
|
private void ShowAddStockFromInventory()
|
||||||
|
{
|
||||||
|
editingStock = null;
|
||||||
|
newStock = new JobStock { JobId = Id!.Value, Quantity = 1, Priority = 10 };
|
||||||
|
stockSelectedShape = null;
|
||||||
|
stockSelectedMaterialId = 0;
|
||||||
|
availableStockItems.Clear();
|
||||||
|
showStockForm = true;
|
||||||
|
showCustomStockForm = false;
|
||||||
|
stockErrorMessage = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ShowAddCustomStock()
|
||||||
|
{
|
||||||
|
editingStock = null;
|
||||||
|
newStock = new JobStock { JobId = Id!.Value, Quantity = -1, Priority = 10, IsCustomLength = true };
|
||||||
|
stockSelectedShape = null;
|
||||||
|
showStockForm = false;
|
||||||
|
showCustomStockForm = true;
|
||||||
|
stockErrorMessage = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CancelStockForm()
|
||||||
|
{
|
||||||
|
showStockForm = false;
|
||||||
|
showCustomStockForm = false;
|
||||||
|
editingStock = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task OnStockShapeChanged()
|
||||||
|
{
|
||||||
|
stockSelectedMaterialId = 0;
|
||||||
|
newStock.MaterialId = 0;
|
||||||
|
newStock.StockItemId = null;
|
||||||
|
availableStockItems.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task OnStockMaterialChanged()
|
||||||
|
{
|
||||||
|
newStock.MaterialId = stockSelectedMaterialId;
|
||||||
|
newStock.StockItemId = null;
|
||||||
|
if (stockSelectedMaterialId > 0)
|
||||||
|
{
|
||||||
|
availableStockItems = await JobService.GetAvailableStockForMaterialAsync(stockSelectedMaterialId);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
availableStockItems.Clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void EditStock(JobStock stock)
|
||||||
|
{
|
||||||
|
editingStock = stock;
|
||||||
|
newStock = new JobStock
|
||||||
|
{
|
||||||
|
Id = stock.Id,
|
||||||
|
JobId = stock.JobId,
|
||||||
|
MaterialId = stock.MaterialId,
|
||||||
|
StockItemId = stock.StockItemId,
|
||||||
|
LengthInches = stock.LengthInches,
|
||||||
|
Quantity = stock.Quantity,
|
||||||
|
IsCustomLength = stock.IsCustomLength,
|
||||||
|
Priority = stock.Priority,
|
||||||
|
SortOrder = stock.SortOrder
|
||||||
|
};
|
||||||
|
stockSelectedShape = stock.Material?.Shape;
|
||||||
|
stockSelectedMaterialId = stock.MaterialId;
|
||||||
|
stockErrorMessage = null;
|
||||||
|
|
||||||
|
if (stock.IsCustomLength)
|
||||||
|
{
|
||||||
|
showStockForm = false;
|
||||||
|
showCustomStockForm = true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
showStockForm = true;
|
||||||
|
showCustomStockForm = false;
|
||||||
|
_ = OnStockMaterialChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task SaveStockFromInventoryAsync()
|
||||||
|
{
|
||||||
|
stockErrorMessage = null;
|
||||||
|
|
||||||
|
if (!stockSelectedShape.HasValue)
|
||||||
|
{
|
||||||
|
stockErrorMessage = "Please select a shape";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stockSelectedMaterialId == 0)
|
||||||
|
{
|
||||||
|
stockErrorMessage = "Please select a size";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!newStock.StockItemId.HasValue)
|
||||||
|
{
|
||||||
|
stockErrorMessage = "Please select a stock length";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newStock.Quantity < 1)
|
||||||
|
{
|
||||||
|
stockErrorMessage = "Quantity must be at least 1";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var selectedStock = availableStockItems.FirstOrDefault(s => s.Id == newStock.StockItemId);
|
||||||
|
if (selectedStock == null)
|
||||||
|
{
|
||||||
|
stockErrorMessage = "Selected stock not found";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
newStock.MaterialId = stockSelectedMaterialId;
|
||||||
|
newStock.LengthInches = selectedStock.LengthInches;
|
||||||
|
newStock.IsCustomLength = false;
|
||||||
|
|
||||||
|
if (editingStock == null)
|
||||||
|
{
|
||||||
|
await JobService.AddStockAsync(newStock);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
await JobService.UpdateStockAsync(newStock);
|
||||||
|
}
|
||||||
|
|
||||||
|
job = (await JobService.GetByIdAsync(Id!.Value))!;
|
||||||
|
showStockForm = false;
|
||||||
|
editingStock = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task SaveCustomStockAsync()
|
||||||
|
{
|
||||||
|
stockErrorMessage = null;
|
||||||
|
|
||||||
|
if (!stockSelectedShape.HasValue)
|
||||||
|
{
|
||||||
|
stockErrorMessage = "Please select a shape";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newStock.MaterialId == 0)
|
||||||
|
{
|
||||||
|
stockErrorMessage = "Please select a size";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newStock.LengthInches <= 0)
|
||||||
|
{
|
||||||
|
stockErrorMessage = "Length must be greater than zero";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newStock.Quantity < -1 || newStock.Quantity == 0)
|
||||||
|
{
|
||||||
|
stockErrorMessage = "Quantity must be at least 1 (or -1 for unlimited)";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
newStock.StockItemId = null;
|
||||||
|
newStock.IsCustomLength = true;
|
||||||
|
|
||||||
|
if (editingStock == null)
|
||||||
|
{
|
||||||
|
await JobService.AddStockAsync(newStock);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
await JobService.UpdateStockAsync(newStock);
|
||||||
|
}
|
||||||
|
|
||||||
|
job = (await JobService.GetByIdAsync(Id!.Value))!;
|
||||||
|
showCustomStockForm = false;
|
||||||
|
editingStock = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task DeleteStock(JobStock stock)
|
||||||
|
{
|
||||||
|
await JobService.DeleteStockAsync(stock.Id);
|
||||||
|
job = (await JobService.GetByIdAsync(Id!.Value))!;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,120 @@
|
|||||||
|
@page "/jobs"
|
||||||
|
@inject JobService JobService
|
||||||
|
@inject NavigationManager Navigation
|
||||||
|
|
||||||
|
<PageTitle>Jobs</PageTitle>
|
||||||
|
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||||
|
<h1>Jobs</h1>
|
||||||
|
<div class="d-flex gap-2">
|
||||||
|
<button class="btn btn-success" @onclick="QuickCreateJob" disabled="@creating">
|
||||||
|
@if (creating)
|
||||||
|
{
|
||||||
|
<span class="spinner-border spinner-border-sm me-1"></span>
|
||||||
|
}
|
||||||
|
Quick Create
|
||||||
|
</button>
|
||||||
|
<a href="jobs/new" class="btn btn-primary">New Job</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if (loading)
|
||||||
|
{
|
||||||
|
<p><em>Loading...</em></p>
|
||||||
|
}
|
||||||
|
else if (jobs.Count == 0)
|
||||||
|
{
|
||||||
|
<div class="alert alert-info">
|
||||||
|
No jobs found. <a href="jobs/new">Create your first job</a>.
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<table class="table table-striped table-hover">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Job #</th>
|
||||||
|
<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 job in jobs)
|
||||||
|
{
|
||||||
|
<tr>
|
||||||
|
<td><a href="jobs/@job.Id">@job.JobNumber</a></td>
|
||||||
|
<td>@(job.Name ?? "-")</td>
|
||||||
|
<td>@(job.Customer ?? "-")</td>
|
||||||
|
<td>@(job.CuttingTool?.Name ?? "-")</td>
|
||||||
|
<td>@((job.UpdatedAt ?? job.CreatedAt).ToLocalTime().ToString("g"))</td>
|
||||||
|
<td>
|
||||||
|
<a href="jobs/@job.Id" class="btn btn-sm btn-outline-primary">Edit</a>
|
||||||
|
<a href="jobs/@job.Id/results" class="btn btn-sm btn-success">Optimize</a>
|
||||||
|
<button class="btn btn-sm btn-outline-secondary" @onclick="() => DuplicateJob(job)">Copy</button>
|
||||||
|
<button class="btn btn-sm btn-outline-danger" @onclick="() => ConfirmDelete(job)">Delete</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
}
|
||||||
|
|
||||||
|
<ConfirmDialog @ref="deleteDialog"
|
||||||
|
Title="Delete Job"
|
||||||
|
Message="@deleteMessage"
|
||||||
|
ConfirmText="Delete"
|
||||||
|
OnConfirm="DeleteConfirmed" />
|
||||||
|
|
||||||
|
@code {
|
||||||
|
private List<Job> jobs = new();
|
||||||
|
private bool loading = true;
|
||||||
|
private bool creating = false;
|
||||||
|
private ConfirmDialog deleteDialog = null!;
|
||||||
|
private Job? jobToDelete;
|
||||||
|
private string deleteMessage = "";
|
||||||
|
|
||||||
|
protected override async Task OnInitializedAsync()
|
||||||
|
{
|
||||||
|
jobs = await JobService.GetAllAsync();
|
||||||
|
loading = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task QuickCreateJob()
|
||||||
|
{
|
||||||
|
creating = true;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var job = await JobService.QuickCreateAsync();
|
||||||
|
Navigation.NavigateTo($"jobs/{job.Id}");
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
creating = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ConfirmDelete(Job job)
|
||||||
|
{
|
||||||
|
jobToDelete = job;
|
||||||
|
deleteMessage = $"Are you sure you want to delete \"{job.DisplayName}\"? This will also delete all parts.";
|
||||||
|
deleteDialog.Show();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task DeleteConfirmed()
|
||||||
|
{
|
||||||
|
if (jobToDelete != null)
|
||||||
|
{
|
||||||
|
await JobService.DeleteAsync(jobToDelete.Id);
|
||||||
|
jobs = await JobService.GetAllAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task DuplicateJob(Job job)
|
||||||
|
{
|
||||||
|
var duplicate = await JobService.DuplicateAsync(job.Id);
|
||||||
|
Navigation.NavigateTo($"jobs/{duplicate.Id}");
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user