Compare commits
19 Commits
8e73d630d5
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 21d50e7c20 | |||
| f723661696 | |||
| c795c129e5 | |||
| 30071469bc | |||
| c9a2583f26 | |||
| 0e5b63c557 | |||
| 6388e003d3 | |||
| c5da5dda98 | |||
| 21cddb22c7 | |||
| 3b036308c8 | |||
| 4f6d986dc9 | |||
| 254066c989 | |||
| ce14dd50cb | |||
| dfc767320a | |||
| 5cc088ea6b | |||
| 6797d1e4fd | |||
| c4fc88f7d2 | |||
| 9929d82768 | |||
| 0ded77ce8b |
BIN
.claude/mcp/Azure.Core.dll
Normal file
BIN
.claude/mcp/Azure.Core.dll
Normal file
Binary file not shown.
BIN
.claude/mcp/Azure.Identity.dll
Normal file
BIN
.claude/mcp/Azure.Identity.dll
Normal file
Binary file not shown.
BIN
.claude/mcp/CutList.Core.dll
Normal file
BIN
.claude/mcp/CutList.Core.dll
Normal file
Binary file not shown.
1399
.claude/mcp/CutList.Mcp.deps.json
Normal file
1399
.claude/mcp/CutList.Mcp.deps.json
Normal file
File diff suppressed because it is too large
Load Diff
BIN
.claude/mcp/CutList.Mcp.dll
Normal file
BIN
.claude/mcp/CutList.Mcp.dll
Normal file
Binary file not shown.
BIN
.claude/mcp/CutList.Mcp.exe
Normal file
BIN
.claude/mcp/CutList.Mcp.exe
Normal file
Binary file not shown.
20
.claude/mcp/CutList.Mcp.runtimeconfig.json
Normal file
20
.claude/mcp/CutList.Mcp.runtimeconfig.json
Normal file
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
BIN
.claude/mcp/CutList.Web.dll
Normal file
BIN
.claude/mcp/CutList.Web.dll
Normal file
Binary file not shown.
BIN
.claude/mcp/CutList.Web.exe
Normal file
BIN
.claude/mcp/CutList.Web.exe
Normal file
Binary file not shown.
21
.claude/mcp/CutList.Web.runtimeconfig.json
Normal file
21
.claude/mcp/CutList.Web.runtimeconfig.json
Normal file
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
BIN
.claude/mcp/Microsoft.Bcl.AsyncInterfaces.dll
Normal file
BIN
.claude/mcp/Microsoft.Bcl.AsyncInterfaces.dll
Normal file
Binary file not shown.
BIN
.claude/mcp/Microsoft.Data.SqlClient.dll
Normal file
BIN
.claude/mcp/Microsoft.Data.SqlClient.dll
Normal file
Binary file not shown.
BIN
.claude/mcp/Microsoft.EntityFrameworkCore.Abstractions.dll
Normal file
BIN
.claude/mcp/Microsoft.EntityFrameworkCore.Abstractions.dll
Normal file
Binary file not shown.
BIN
.claude/mcp/Microsoft.EntityFrameworkCore.Relational.dll
Normal file
BIN
.claude/mcp/Microsoft.EntityFrameworkCore.Relational.dll
Normal file
Binary file not shown.
BIN
.claude/mcp/Microsoft.EntityFrameworkCore.SqlServer.dll
Normal file
BIN
.claude/mcp/Microsoft.EntityFrameworkCore.SqlServer.dll
Normal file
Binary file not shown.
BIN
.claude/mcp/Microsoft.EntityFrameworkCore.dll
Normal file
BIN
.claude/mcp/Microsoft.EntityFrameworkCore.dll
Normal file
Binary file not shown.
BIN
.claude/mcp/Microsoft.Extensions.AI.Abstractions.dll
Normal file
BIN
.claude/mcp/Microsoft.Extensions.AI.Abstractions.dll
Normal file
Binary file not shown.
BIN
.claude/mcp/Microsoft.Extensions.Caching.Memory.dll
Normal file
BIN
.claude/mcp/Microsoft.Extensions.Caching.Memory.dll
Normal file
Binary file not shown.
BIN
.claude/mcp/Microsoft.Extensions.Configuration.Abstractions.dll
Normal file
BIN
.claude/mcp/Microsoft.Extensions.Configuration.Abstractions.dll
Normal file
Binary file not shown.
BIN
.claude/mcp/Microsoft.Extensions.Configuration.Binder.dll
Normal file
BIN
.claude/mcp/Microsoft.Extensions.Configuration.Binder.dll
Normal file
Binary file not shown.
BIN
.claude/mcp/Microsoft.Extensions.Configuration.CommandLine.dll
Normal file
BIN
.claude/mcp/Microsoft.Extensions.Configuration.CommandLine.dll
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
.claude/mcp/Microsoft.Extensions.Configuration.Json.dll
Normal file
BIN
.claude/mcp/Microsoft.Extensions.Configuration.Json.dll
Normal file
Binary file not shown.
BIN
.claude/mcp/Microsoft.Extensions.Configuration.UserSecrets.dll
Normal file
BIN
.claude/mcp/Microsoft.Extensions.Configuration.UserSecrets.dll
Normal file
Binary file not shown.
BIN
.claude/mcp/Microsoft.Extensions.Configuration.dll
Normal file
BIN
.claude/mcp/Microsoft.Extensions.Configuration.dll
Normal file
Binary file not shown.
Binary file not shown.
BIN
.claude/mcp/Microsoft.Extensions.DependencyInjection.dll
Normal file
BIN
.claude/mcp/Microsoft.Extensions.DependencyInjection.dll
Normal file
Binary file not shown.
BIN
.claude/mcp/Microsoft.Extensions.Diagnostics.Abstractions.dll
Normal file
BIN
.claude/mcp/Microsoft.Extensions.Diagnostics.Abstractions.dll
Normal file
Binary file not shown.
BIN
.claude/mcp/Microsoft.Extensions.Diagnostics.dll
Normal file
BIN
.claude/mcp/Microsoft.Extensions.Diagnostics.dll
Normal file
Binary file not shown.
BIN
.claude/mcp/Microsoft.Extensions.FileProviders.Abstractions.dll
Normal file
BIN
.claude/mcp/Microsoft.Extensions.FileProviders.Abstractions.dll
Normal file
Binary file not shown.
BIN
.claude/mcp/Microsoft.Extensions.FileProviders.Physical.dll
Normal file
BIN
.claude/mcp/Microsoft.Extensions.FileProviders.Physical.dll
Normal file
Binary file not shown.
BIN
.claude/mcp/Microsoft.Extensions.FileSystemGlobbing.dll
Normal file
BIN
.claude/mcp/Microsoft.Extensions.FileSystemGlobbing.dll
Normal file
Binary file not shown.
BIN
.claude/mcp/Microsoft.Extensions.Hosting.Abstractions.dll
Normal file
BIN
.claude/mcp/Microsoft.Extensions.Hosting.Abstractions.dll
Normal file
Binary file not shown.
BIN
.claude/mcp/Microsoft.Extensions.Hosting.dll
Normal file
BIN
.claude/mcp/Microsoft.Extensions.Hosting.dll
Normal file
Binary file not shown.
BIN
.claude/mcp/Microsoft.Extensions.Logging.Abstractions.dll
Normal file
BIN
.claude/mcp/Microsoft.Extensions.Logging.Abstractions.dll
Normal file
Binary file not shown.
BIN
.claude/mcp/Microsoft.Extensions.Logging.Configuration.dll
Normal file
BIN
.claude/mcp/Microsoft.Extensions.Logging.Configuration.dll
Normal file
Binary file not shown.
BIN
.claude/mcp/Microsoft.Extensions.Logging.Console.dll
Normal file
BIN
.claude/mcp/Microsoft.Extensions.Logging.Console.dll
Normal file
Binary file not shown.
BIN
.claude/mcp/Microsoft.Extensions.Logging.Debug.dll
Normal file
BIN
.claude/mcp/Microsoft.Extensions.Logging.Debug.dll
Normal file
Binary file not shown.
BIN
.claude/mcp/Microsoft.Extensions.Logging.EventLog.dll
Normal file
BIN
.claude/mcp/Microsoft.Extensions.Logging.EventLog.dll
Normal file
Binary file not shown.
BIN
.claude/mcp/Microsoft.Extensions.Logging.EventSource.dll
Normal file
BIN
.claude/mcp/Microsoft.Extensions.Logging.EventSource.dll
Normal file
Binary file not shown.
BIN
.claude/mcp/Microsoft.Extensions.Logging.dll
Normal file
BIN
.claude/mcp/Microsoft.Extensions.Logging.dll
Normal file
Binary file not shown.
Binary file not shown.
BIN
.claude/mcp/Microsoft.Extensions.Options.dll
Normal file
BIN
.claude/mcp/Microsoft.Extensions.Options.dll
Normal file
Binary file not shown.
BIN
.claude/mcp/Microsoft.Extensions.Primitives.dll
Normal file
BIN
.claude/mcp/Microsoft.Extensions.Primitives.dll
Normal file
Binary file not shown.
BIN
.claude/mcp/Microsoft.Identity.Client.Extensions.Msal.dll
Normal file
BIN
.claude/mcp/Microsoft.Identity.Client.Extensions.Msal.dll
Normal file
Binary file not shown.
BIN
.claude/mcp/Microsoft.Identity.Client.dll
Normal file
BIN
.claude/mcp/Microsoft.Identity.Client.dll
Normal file
Binary file not shown.
BIN
.claude/mcp/Microsoft.IdentityModel.Abstractions.dll
Normal file
BIN
.claude/mcp/Microsoft.IdentityModel.Abstractions.dll
Normal file
Binary file not shown.
BIN
.claude/mcp/Microsoft.IdentityModel.JsonWebTokens.dll
Normal file
BIN
.claude/mcp/Microsoft.IdentityModel.JsonWebTokens.dll
Normal file
Binary file not shown.
BIN
.claude/mcp/Microsoft.IdentityModel.Logging.dll
Normal file
BIN
.claude/mcp/Microsoft.IdentityModel.Logging.dll
Normal file
Binary file not shown.
BIN
.claude/mcp/Microsoft.IdentityModel.Protocols.OpenIdConnect.dll
Normal file
BIN
.claude/mcp/Microsoft.IdentityModel.Protocols.OpenIdConnect.dll
Normal file
Binary file not shown.
BIN
.claude/mcp/Microsoft.IdentityModel.Protocols.dll
Normal file
BIN
.claude/mcp/Microsoft.IdentityModel.Protocols.dll
Normal file
Binary file not shown.
BIN
.claude/mcp/Microsoft.IdentityModel.Tokens.dll
Normal file
BIN
.claude/mcp/Microsoft.IdentityModel.Tokens.dll
Normal file
Binary file not shown.
BIN
.claude/mcp/Microsoft.SqlServer.Server.dll
Normal file
BIN
.claude/mcp/Microsoft.SqlServer.Server.dll
Normal file
Binary file not shown.
BIN
.claude/mcp/Microsoft.Win32.SystemEvents.dll
Normal file
BIN
.claude/mcp/Microsoft.Win32.SystemEvents.dll
Normal file
Binary file not shown.
BIN
.claude/mcp/ModelContextProtocol.Core.dll
Normal file
BIN
.claude/mcp/ModelContextProtocol.Core.dll
Normal file
Binary file not shown.
BIN
.claude/mcp/ModelContextProtocol.dll
Normal file
BIN
.claude/mcp/ModelContextProtocol.dll
Normal file
Binary file not shown.
BIN
.claude/mcp/System.ClientModel.dll
Normal file
BIN
.claude/mcp/System.ClientModel.dll
Normal file
Binary file not shown.
BIN
.claude/mcp/System.Configuration.ConfigurationManager.dll
Normal file
BIN
.claude/mcp/System.Configuration.ConfigurationManager.dll
Normal file
Binary file not shown.
BIN
.claude/mcp/System.Diagnostics.DiagnosticSource.dll
Normal file
BIN
.claude/mcp/System.Diagnostics.DiagnosticSource.dll
Normal file
Binary file not shown.
BIN
.claude/mcp/System.Diagnostics.EventLog.dll
Normal file
BIN
.claude/mcp/System.Diagnostics.EventLog.dll
Normal file
Binary file not shown.
BIN
.claude/mcp/System.Drawing.Common.dll
Normal file
BIN
.claude/mcp/System.Drawing.Common.dll
Normal file
Binary file not shown.
BIN
.claude/mcp/System.IO.Pipelines.dll
Normal file
BIN
.claude/mcp/System.IO.Pipelines.dll
Normal file
Binary file not shown.
BIN
.claude/mcp/System.IdentityModel.Tokens.Jwt.dll
Normal file
BIN
.claude/mcp/System.IdentityModel.Tokens.Jwt.dll
Normal file
Binary file not shown.
BIN
.claude/mcp/System.Memory.Data.dll
Normal file
BIN
.claude/mcp/System.Memory.Data.dll
Normal file
Binary file not shown.
BIN
.claude/mcp/System.Net.ServerSentEvents.dll
Normal file
BIN
.claude/mcp/System.Net.ServerSentEvents.dll
Normal file
Binary file not shown.
BIN
.claude/mcp/System.Runtime.Caching.dll
Normal file
BIN
.claude/mcp/System.Runtime.Caching.dll
Normal file
Binary file not shown.
BIN
.claude/mcp/System.Security.Cryptography.ProtectedData.dll
Normal file
BIN
.claude/mcp/System.Security.Cryptography.ProtectedData.dll
Normal file
Binary file not shown.
BIN
.claude/mcp/System.Security.Permissions.dll
Normal file
BIN
.claude/mcp/System.Security.Permissions.dll
Normal file
Binary file not shown.
BIN
.claude/mcp/System.Text.Encodings.Web.dll
Normal file
BIN
.claude/mcp/System.Text.Encodings.Web.dll
Normal file
Binary file not shown.
BIN
.claude/mcp/System.Text.Json.dll
Normal file
BIN
.claude/mcp/System.Text.Json.dll
Normal file
Binary file not shown.
BIN
.claude/mcp/System.Windows.Extensions.dll
Normal file
BIN
.claude/mcp/System.Windows.Extensions.dll
Normal file
Binary file not shown.
8
.claude/mcp/appsettings.Development.json
Normal file
8
.claude/mcp/appsettings.Development.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
}
|
||||
}
|
||||
13
.claude/mcp/appsettings.json
Normal file
13
.claude/mcp/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"
|
||||
}
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
BIN
.claude/mcp/runtimes/unix/lib/net6.0/System.Drawing.Common.dll
Normal file
BIN
.claude/mcp/runtimes/unix/lib/net6.0/System.Drawing.Common.dll
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
.claude/mcp/runtimes/win/lib/net6.0/Microsoft.Data.SqlClient.dll
Normal file
BIN
.claude/mcp/runtimes/win/lib/net6.0/Microsoft.Data.SqlClient.dll
Normal file
Binary file not shown.
Binary file not shown.
BIN
.claude/mcp/runtimes/win/lib/net6.0/System.Drawing.Common.dll
Normal file
BIN
.claude/mcp/runtimes/win/lib/net6.0/System.Drawing.Common.dll
Normal file
Binary file not shown.
BIN
.claude/mcp/runtimes/win/lib/net6.0/System.Runtime.Caching.dll
Normal file
BIN
.claude/mcp/runtimes/win/lib/net6.0/System.Runtime.Caching.dll
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
28
.claude/settings.local.json
Normal file
28
.claude/settings.local.json
Normal file
@@ -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);
|
||||
|
||||
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.");
|
||||
}
|
||||
|
||||
var feet = match2.Groups["Feet"];
|
||||
var inches = match2.Groups["Inches"];
|
||||
|
||||
@@ -39,6 +39,12 @@ namespace CutList.Core.Formatting
|
||||
return wholeNumber.ToString();
|
||||
}
|
||||
|
||||
// If whole number is 0, just show the fraction
|
||||
if (wholeNumber == 0)
|
||||
{
|
||||
return $"{numerator}/{denominator}";
|
||||
}
|
||||
|
||||
return $"{wholeNumber}-{numerator}/{denominator}";
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\CutList.Core\CutList.Core.csproj" />
|
||||
<ProjectReference Include="..\CutList.Web\CutList.Web.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
1102
CutList.Mcp/InventoryTools.cs
Normal file
1102
CutList.Mcp/InventoryTools.cs
Normal file
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.Hosting;
|
||||
using ModelContextProtocol.Server;
|
||||
|
||||
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
|
||||
.AddMcpServer()
|
||||
.WithStdioServerTransport()
|
||||
|
||||
@@ -14,8 +14,8 @@
|
||||
</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 class="nav-link" href="jobs">
|
||||
<span class="bi bi-list-check-nav-menu" aria-hidden="true"></span> Jobs
|
||||
</NavLink>
|
||||
</div>
|
||||
<div class="nav-item px-3">
|
||||
@@ -23,6 +23,11 @@
|
||||
<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="stock">
|
||||
<span class="bi bi-boxes-nav-menu" aria-hidden="true"></span> Stock Items
|
||||
</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
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
.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 {
|
||||
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="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>
|
||||
<h5 class="card-title">Jobs</h5>
|
||||
<p class="card-text">Create and manage cut list jobs. Add parts and stock bins, then optimize to minimize waste.</p>
|
||||
<a href="jobs" class="btn btn-primary">Go to Jobs</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -51,7 +51,7 @@
|
||||
<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>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>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>
|
||||
|
||||
822
CutList.Web/Components/Pages/Jobs/Edit.razor
Normal file
822
CutList.Web/Components/Pages/Jobs/Edit.razor
Normal file
@@ -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))!;
|
||||
}
|
||||
}
|
||||
120
CutList.Web/Components/Pages/Jobs/Index.razor
Normal file
120
CutList.Web/Components/Pages/Jobs/Index.razor
Normal file
@@ -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