chore: Remove deprecated Project entities and pages
Remove files superseded by the Job model: - Project, ProjectPart entities (replaced by Job, JobPart, JobStock) - ProjectService (replaced by JobService) - Projects UI pages (replaced by Jobs pages) - MaterialStockLength entity (consolidated into StockItem) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,399 +0,0 @@
|
|||||||
@page "/projects/new"
|
|
||||||
@page "/projects/{Id:int}"
|
|
||||||
@inject ProjectService ProjectService
|
|
||||||
@inject MaterialService MaterialService
|
|
||||||
@inject NavigationManager Navigation
|
|
||||||
@using CutList.Core.Formatting
|
|
||||||
|
|
||||||
<PageTitle>@(IsNew ? "New Project" : project.Name)</PageTitle>
|
|
||||||
|
|
||||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
|
||||||
<h1>@(IsNew ? "New Project" : project.Name)</h1>
|
|
||||||
@if (!IsNew)
|
|
||||||
{
|
|
||||||
<a href="projects/@Id/results" class="btn btn-success">Run Optimization</a>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
@if (loading)
|
|
||||||
{
|
|
||||||
<p><em>Loading...</em></p>
|
|
||||||
}
|
|
||||||
else if (IsNew)
|
|
||||||
{
|
|
||||||
<!-- New Project: Simple form -->
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-lg-6">
|
|
||||||
@RenderDetailsForm()
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
<!-- Existing Project: Tabbed interface -->
|
|
||||||
<ul class="nav nav-tabs mb-3" role="tablist">
|
|
||||||
<li class="nav-item" role="presentation">
|
|
||||||
<button class="nav-link @(activeTab == Tab.Details ? "active" : "")"
|
|
||||||
@onclick="() => SetTab(Tab.Details)" type="button">
|
|
||||||
Details
|
|
||||||
</button>
|
|
||||||
</li>
|
|
||||||
<li class="nav-item" role="presentation">
|
|
||||||
<button class="nav-link @(activeTab == Tab.Parts ? "active" : "")"
|
|
||||||
@onclick="() => SetTab(Tab.Parts)" type="button">
|
|
||||||
Parts
|
|
||||||
@if (project.Parts.Count > 0)
|
|
||||||
{
|
|
||||||
<span class="badge bg-secondary ms-1">@project.Parts.Sum(p => p.Quantity)</span>
|
|
||||||
}
|
|
||||||
</button>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<div class="tab-content">
|
|
||||||
@if (activeTab == Tab.Details)
|
|
||||||
{
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-lg-6">
|
|
||||||
@RenderDetailsForm()
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
else if (activeTab == Tab.Parts)
|
|
||||||
{
|
|
||||||
@RenderPartsTab()
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
@code {
|
|
||||||
private enum Tab { Details, Parts }
|
|
||||||
|
|
||||||
[Parameter]
|
|
||||||
public int? Id { get; set; }
|
|
||||||
|
|
||||||
private Project project = new();
|
|
||||||
private List<Material> materials = new();
|
|
||||||
private List<CuttingTool> cuttingTools = new();
|
|
||||||
|
|
||||||
private bool loading = true;
|
|
||||||
private bool savingProject;
|
|
||||||
private string? projectErrorMessage;
|
|
||||||
private Tab activeTab = Tab.Details;
|
|
||||||
|
|
||||||
private void SetTab(Tab tab) => activeTab = tab;
|
|
||||||
|
|
||||||
// Parts form
|
|
||||||
private bool showPartForm;
|
|
||||||
private ProjectPart newPart = new();
|
|
||||||
private ProjectPart? editingPart;
|
|
||||||
private string? partErrorMessage;
|
|
||||||
private string selectedShape = string.Empty;
|
|
||||||
|
|
||||||
private IEnumerable<string> DistinctShapes => materials.Select(m => m.Shape).Distinct().OrderBy(s => s);
|
|
||||||
private IEnumerable<Material> FilteredMaterials => string.IsNullOrEmpty(selectedShape)
|
|
||||||
? Enumerable.Empty<Material>()
|
|
||||||
: materials.Where(m => m.Shape == selectedShape).OrderBy(m => m.Size);
|
|
||||||
|
|
||||||
private bool IsNew => !Id.HasValue;
|
|
||||||
|
|
||||||
protected override async Task OnInitializedAsync()
|
|
||||||
{
|
|
||||||
materials = await MaterialService.GetAllAsync();
|
|
||||||
cuttingTools = await ProjectService.GetCuttingToolsAsync();
|
|
||||||
|
|
||||||
if (Id.HasValue)
|
|
||||||
{
|
|
||||||
var existing = await ProjectService.GetByIdAsync(Id.Value);
|
|
||||||
if (existing == null)
|
|
||||||
{
|
|
||||||
Navigation.NavigateTo("projects");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
project = existing;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// Set default cutting tool for new projects
|
|
||||||
var defaultTool = await ProjectService.GetDefaultCuttingToolAsync();
|
|
||||||
if (defaultTool != null)
|
|
||||||
{
|
|
||||||
project.CuttingToolId = defaultTool.Id;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
loading = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private RenderFragment RenderDetailsForm() => __builder =>
|
|
||||||
{
|
|
||||||
<div class="card">
|
|
||||||
<div class="card-header">
|
|
||||||
<h5 class="mb-0">Project Details</h5>
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<EditForm Model="project" OnValidSubmit="SaveProjectAsync">
|
|
||||||
<div class="mb-3">
|
|
||||||
<label class="form-label">Project Name</label>
|
|
||||||
<InputText class="form-control" @bind-Value="project.Name" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mb-3">
|
|
||||||
<label class="form-label">Customer</label>
|
|
||||||
<InputText class="form-control" @bind-Value="project.Customer" placeholder="Customer name" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mb-3">
|
|
||||||
<label class="form-label">Cutting Tool</label>
|
|
||||||
<InputSelect class="form-select" @bind-Value="project.CuttingToolId">
|
|
||||||
<option value="">-- Select Tool --</option>
|
|
||||||
@foreach (var tool in cuttingTools)
|
|
||||||
{
|
|
||||||
<option value="@tool.Id">@tool.Name (@tool.KerfInches" kerf)</option>
|
|
||||||
}
|
|
||||||
</InputSelect>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mb-3">
|
|
||||||
<label class="form-label">Notes</label>
|
|
||||||
<InputTextArea class="form-control" @bind-Value="project.Notes" rows="3" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
@if (!string.IsNullOrEmpty(projectErrorMessage))
|
|
||||||
{
|
|
||||||
<div class="alert alert-danger">@projectErrorMessage</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
<div class="d-flex gap-2">
|
|
||||||
<button type="submit" class="btn btn-primary" disabled="@savingProject">
|
|
||||||
@if (savingProject)
|
|
||||||
{
|
|
||||||
<span class="spinner-border spinner-border-sm me-1"></span>
|
|
||||||
}
|
|
||||||
@(IsNew ? "Create Project" : "Save")
|
|
||||||
</button>
|
|
||||||
<a href="projects" class="btn btn-outline-secondary">Back</a>
|
|
||||||
</div>
|
|
||||||
</EditForm>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
};
|
|
||||||
|
|
||||||
private RenderFragment RenderPartsTab() => __builder =>
|
|
||||||
{
|
|
||||||
<div class="card">
|
|
||||||
<div class="card-header d-flex justify-content-between align-items-center">
|
|
||||||
<h5 class="mb-0">Parts to Cut</h5>
|
|
||||||
<button class="btn btn-primary" @onclick="ShowAddPartForm">Add Part</button>
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
@if (showPartForm)
|
|
||||||
{
|
|
||||||
<div class="border rounded p-3 mb-3 bg-light">
|
|
||||||
<h6>@(editingPart == null ? "Add Part" : "Edit Part")</h6>
|
|
||||||
<div class="row g-3">
|
|
||||||
<div class="col-md-2">
|
|
||||||
<label class="form-label">Shape</label>
|
|
||||||
<select class="form-select" @bind="selectedShape" @bind:after="OnShapeChanged">
|
|
||||||
<option value="">-- Select --</option>
|
|
||||||
@foreach (var shape in DistinctShapes)
|
|
||||||
{
|
|
||||||
<option value="@shape">@shape</option>
|
|
||||||
}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-2">
|
|
||||||
<label class="form-label">Size</label>
|
|
||||||
<select class="form-select" @bind="newPart.MaterialId" disabled="@string.IsNullOrEmpty(selectedShape)">
|
|
||||||
<option value="0">-- Select --</option>
|
|
||||||
@foreach (var material in FilteredMaterials)
|
|
||||||
{
|
|
||||||
<option value="@material.Id">@material.Size</option>
|
|
||||||
}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-3">
|
|
||||||
<label class="form-label">Length</label>
|
|
||||||
<LengthInput @bind-Value="newPart.LengthInches" />
|
|
||||||
</div>
|
|
||||||
<div class="col-md-2">
|
|
||||||
<label class="form-label">Qty</label>
|
|
||||||
<input type="number" class="form-control" @bind="newPart.Quantity" min="1" />
|
|
||||||
</div>
|
|
||||||
<div class="col-md-3">
|
|
||||||
<label class="form-label">Name <span class="text-muted fw-normal">(optional)</span></label>
|
|
||||||
<input type="text" class="form-control" @bind="newPart.Name" placeholder="Part name" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
@if (!string.IsNullOrEmpty(partErrorMessage))
|
|
||||||
{
|
|
||||||
<div class="alert alert-danger mt-3 mb-0">@partErrorMessage</div>
|
|
||||||
}
|
|
||||||
<div class="mt-3 d-flex gap-2">
|
|
||||||
<button class="btn btn-primary" @onclick="SavePartAsync">@(editingPart == null ? "Add Part" : "Save Changes")</button>
|
|
||||||
<button class="btn btn-outline-secondary" @onclick="CancelPartForm">Cancel</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
@if (project.Parts.Count == 0)
|
|
||||||
{
|
|
||||||
<div class="text-center py-4 text-muted">
|
|
||||||
<p class="mb-2">No parts added yet.</p>
|
|
||||||
<p class="small">Add the parts you need to cut, selecting the material for each.</p>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
<div class="table-responsive">
|
|
||||||
<table class="table table-hover mb-0">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Material</th>
|
|
||||||
<th>Length</th>
|
|
||||||
<th>Qty</th>
|
|
||||||
<th>Name</th>
|
|
||||||
<th style="width: 120px;">Actions</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
@foreach (var part in project.Parts)
|
|
||||||
{
|
|
||||||
<tr>
|
|
||||||
<td>@part.Material.DisplayName</td>
|
|
||||||
<td>@ArchUnits.FormatFromInches((double)part.LengthInches)</td>
|
|
||||||
<td>@part.Quantity</td>
|
|
||||||
<td>@(string.IsNullOrWhiteSpace(part.Name) ? "-" : part.Name)</td>
|
|
||||||
<td>
|
|
||||||
<button class="btn btn-sm btn-outline-primary me-1" @onclick="() => EditPart(part)">Edit</button>
|
|
||||||
<button class="btn btn-sm btn-outline-danger" @onclick="() => DeletePart(part)">Delete</button>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
<div class="mt-3 text-muted">
|
|
||||||
Total: @project.Parts.Sum(p => p.Quantity) pieces
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
};
|
|
||||||
|
|
||||||
private async Task SaveProjectAsync()
|
|
||||||
{
|
|
||||||
projectErrorMessage = null;
|
|
||||||
savingProject = true;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(project.Name))
|
|
||||||
{
|
|
||||||
projectErrorMessage = "Project name is required";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (IsNew)
|
|
||||||
{
|
|
||||||
var created = await ProjectService.CreateAsync(project);
|
|
||||||
Navigation.NavigateTo($"projects/{created.Id}");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
await ProjectService.UpdateAsync(project);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
savingProject = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parts methods
|
|
||||||
private void ShowAddPartForm()
|
|
||||||
{
|
|
||||||
editingPart = null;
|
|
||||||
newPart = new ProjectPart { ProjectId = Id!.Value, Quantity = 1 };
|
|
||||||
selectedShape = string.Empty;
|
|
||||||
showPartForm = true;
|
|
||||||
partErrorMessage = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnShapeChanged()
|
|
||||||
{
|
|
||||||
newPart.MaterialId = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void EditPart(ProjectPart part)
|
|
||||||
{
|
|
||||||
editingPart = part;
|
|
||||||
newPart = new ProjectPart
|
|
||||||
{
|
|
||||||
Id = part.Id,
|
|
||||||
ProjectId = part.ProjectId,
|
|
||||||
MaterialId = part.MaterialId,
|
|
||||||
Name = part.Name,
|
|
||||||
LengthInches = part.LengthInches,
|
|
||||||
Quantity = part.Quantity,
|
|
||||||
SortOrder = part.SortOrder
|
|
||||||
};
|
|
||||||
selectedShape = part.Material?.Shape ?? string.Empty;
|
|
||||||
showPartForm = true;
|
|
||||||
partErrorMessage = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void CancelPartForm()
|
|
||||||
{
|
|
||||||
showPartForm = false;
|
|
||||||
editingPart = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task SavePartAsync()
|
|
||||||
{
|
|
||||||
partErrorMessage = null;
|
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(selectedShape))
|
|
||||||
{
|
|
||||||
partErrorMessage = "Please select a shape";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (newPart.MaterialId == 0)
|
|
||||||
{
|
|
||||||
partErrorMessage = "Please select a size";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (newPart.LengthInches <= 0)
|
|
||||||
{
|
|
||||||
partErrorMessage = "Length must be greater than zero";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (newPart.Quantity < 1)
|
|
||||||
{
|
|
||||||
partErrorMessage = "Quantity must be at least 1";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (editingPart == null)
|
|
||||||
{
|
|
||||||
await ProjectService.AddPartAsync(newPart);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
await ProjectService.UpdatePartAsync(newPart);
|
|
||||||
}
|
|
||||||
|
|
||||||
project = (await ProjectService.GetByIdAsync(Id!.Value))!;
|
|
||||||
showPartForm = false;
|
|
||||||
editingPart = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task DeletePart(ProjectPart part)
|
|
||||||
{
|
|
||||||
await ProjectService.DeletePartAsync(part.Id);
|
|
||||||
project = (await ProjectService.GetByIdAsync(Id!.Value))!;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,94 +0,0 @@
|
|||||||
@page "/projects"
|
|
||||||
@inject ProjectService ProjectService
|
|
||||||
@inject NavigationManager Navigation
|
|
||||||
|
|
||||||
<PageTitle>Projects</PageTitle>
|
|
||||||
|
|
||||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
|
||||||
<h1>Projects</h1>
|
|
||||||
<a href="projects/new" class="btn btn-primary">New Project</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
@if (loading)
|
|
||||||
{
|
|
||||||
<p><em>Loading...</em></p>
|
|
||||||
}
|
|
||||||
else if (projects.Count == 0)
|
|
||||||
{
|
|
||||||
<div class="alert alert-info">
|
|
||||||
No projects found. <a href="projects/new">Create your first project</a>.
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
<table class="table table-striped table-hover">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Name</th>
|
|
||||||
<th>Customer</th>
|
|
||||||
<th>Cutting Tool</th>
|
|
||||||
<th>Last Modified</th>
|
|
||||||
<th style="width: 200px;">Actions</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
@foreach (var project in projects)
|
|
||||||
{
|
|
||||||
<tr>
|
|
||||||
<td><a href="projects/@project.Id">@project.Name</a></td>
|
|
||||||
<td>@(project.Customer ?? "-")</td>
|
|
||||||
<td>@(project.CuttingTool?.Name ?? "-")</td>
|
|
||||||
<td>@((project.UpdatedAt ?? project.CreatedAt).ToLocalTime().ToString("g"))</td>
|
|
||||||
<td>
|
|
||||||
<a href="projects/@project.Id" class="btn btn-sm btn-outline-primary">Edit</a>
|
|
||||||
<a href="projects/@project.Id/results" class="btn btn-sm btn-success">Optimize</a>
|
|
||||||
<button class="btn btn-sm btn-outline-secondary" @onclick="() => DuplicateProject(project)">Copy</button>
|
|
||||||
<button class="btn btn-sm btn-outline-danger" @onclick="() => ConfirmDelete(project)">Delete</button>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
}
|
|
||||||
|
|
||||||
<ConfirmDialog @ref="deleteDialog"
|
|
||||||
Title="Delete Project"
|
|
||||||
Message="@deleteMessage"
|
|
||||||
ConfirmText="Delete"
|
|
||||||
OnConfirm="DeleteConfirmed" />
|
|
||||||
|
|
||||||
@code {
|
|
||||||
private List<Project> projects = new();
|
|
||||||
private bool loading = true;
|
|
||||||
private ConfirmDialog deleteDialog = null!;
|
|
||||||
private Project? projectToDelete;
|
|
||||||
private string deleteMessage = "";
|
|
||||||
|
|
||||||
protected override async Task OnInitializedAsync()
|
|
||||||
{
|
|
||||||
projects = await ProjectService.GetAllAsync();
|
|
||||||
loading = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void ConfirmDelete(Project project)
|
|
||||||
{
|
|
||||||
projectToDelete = project;
|
|
||||||
deleteMessage = $"Are you sure you want to delete \"{project.Name}\"? This will also delete all parts and stock bins.";
|
|
||||||
deleteDialog.Show();
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task DeleteConfirmed()
|
|
||||||
{
|
|
||||||
if (projectToDelete != null)
|
|
||||||
{
|
|
||||||
await ProjectService.DeleteAsync(projectToDelete.Id);
|
|
||||||
projects = await ProjectService.GetAllAsync();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task DuplicateProject(Project project)
|
|
||||||
{
|
|
||||||
var duplicate = await ProjectService.DuplicateAsync(project.Id);
|
|
||||||
Navigation.NavigateTo($"projects/{duplicate.Id}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,257 +0,0 @@
|
|||||||
@page "/projects/{Id:int}/results"
|
|
||||||
@inject ProjectService ProjectService
|
|
||||||
@inject CutListPackingService PackingService
|
|
||||||
@inject NavigationManager Navigation
|
|
||||||
@inject IJSRuntime JS
|
|
||||||
@using CutList.Core
|
|
||||||
@using CutList.Core.Nesting
|
|
||||||
@using CutList.Core.Formatting
|
|
||||||
|
|
||||||
<PageTitle>Results - @(project?.Name ?? "Project")</PageTitle>
|
|
||||||
|
|
||||||
@if (loading)
|
|
||||||
{
|
|
||||||
<p><em>Loading...</em></p>
|
|
||||||
}
|
|
||||||
else if (project == null)
|
|
||||||
{
|
|
||||||
<div class="alert alert-danger">Project not found.</div>
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
|
||||||
<div>
|
|
||||||
<h1>@project.Name</h1>
|
|
||||||
@if (!string.IsNullOrWhiteSpace(project.Customer))
|
|
||||||
{
|
|
||||||
<p class="text-muted mb-0">Customer: @project.Customer</p>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<a href="projects/@Id" class="btn btn-outline-secondary me-2">Edit Project</a>
|
|
||||||
<button class="btn btn-primary" @onclick="PrintReport">Print Report</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
@if (!CanOptimize)
|
|
||||||
{
|
|
||||||
<div class="alert alert-warning">
|
|
||||||
<h4>Cannot Optimize</h4>
|
|
||||||
<ul class="mb-0">
|
|
||||||
@if (project.Parts.Count == 0)
|
|
||||||
{
|
|
||||||
<li>No parts defined. <a href="projects/@Id">Add parts to the project</a>.</li>
|
|
||||||
}
|
|
||||||
@if (project.CuttingToolId == null)
|
|
||||||
{
|
|
||||||
<li>No cutting tool selected. <a href="projects/@Id">Select a cutting tool</a>.</li>
|
|
||||||
}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
else if (packResult != null)
|
|
||||||
{
|
|
||||||
@if (summary!.TotalItemsNotPlaced > 0)
|
|
||||||
{
|
|
||||||
<div class="alert alert-warning">
|
|
||||||
<h5>Items Not Placed</h5>
|
|
||||||
<p>Some items could not be placed. This usually means no stock lengths are configured for the material, or parts are too long.</p>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
<!-- Overall Summary Cards -->
|
|
||||||
<div class="row mb-4">
|
|
||||||
<div class="col-md-3 col-6 mb-3">
|
|
||||||
<div class="card text-center">
|
|
||||||
<div class="card-body">
|
|
||||||
<h2 class="card-title mb-0">@(summary.TotalInStockBins + summary.TotalToBePurchasedBins)</h2>
|
|
||||||
<p class="card-text text-muted">Total Stock Bars</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-3 col-6 mb-3">
|
|
||||||
<div class="card text-center">
|
|
||||||
<div class="card-body">
|
|
||||||
<h2 class="card-title mb-0">@summary.TotalPieces</h2>
|
|
||||||
<p class="card-text text-muted">Total Pieces</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-3 col-6 mb-3">
|
|
||||||
<div class="card text-center">
|
|
||||||
<div class="card-body">
|
|
||||||
<h2 class="card-title mb-0">@ArchUnits.FormatFromInches(summary.TotalWaste)</h2>
|
|
||||||
<p class="card-text text-muted">Total Waste</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-3 col-6 mb-3">
|
|
||||||
<div class="card text-center">
|
|
||||||
<div class="card-body">
|
|
||||||
<h2 class="card-title mb-0">@summary.Efficiency.ToString("F1")%</h2>
|
|
||||||
<p class="card-text text-muted">Efficiency</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Stock Summary -->
|
|
||||||
<div class="row mb-4">
|
|
||||||
<div class="col-md-6 mb-3">
|
|
||||||
<div class="card border-success">
|
|
||||||
<div class="card-header bg-success text-white">
|
|
||||||
<h5 class="mb-0">In Stock</h5>
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<h3>@summary.TotalInStockBins bars</h3>
|
|
||||||
<p class="text-muted mb-0">Ready to cut from existing inventory</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-6 mb-3">
|
|
||||||
<div class="card border-warning">
|
|
||||||
<div class="card-header bg-warning">
|
|
||||||
<h5 class="mb-0">To Be Purchased</h5>
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<h3>@summary.TotalToBePurchasedBins bars</h3>
|
|
||||||
<p class="text-muted mb-0">Need to order from supplier</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Results by Material -->
|
|
||||||
@foreach (var materialResult in packResult.MaterialResults)
|
|
||||||
{
|
|
||||||
var materialSummary = summary.MaterialSummaries.First(s => s.Material.Id == materialResult.Material.Id);
|
|
||||||
|
|
||||||
<div class="card mb-4">
|
|
||||||
<div class="card-header">
|
|
||||||
<h4 class="mb-0">@materialResult.Material.DisplayName</h4>
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<!-- Material Summary -->
|
|
||||||
<div class="row mb-3">
|
|
||||||
<div class="col-md-2 col-4">
|
|
||||||
<strong>@(materialSummary.InStockBins + materialSummary.ToBePurchasedBins)</strong> bars
|
|
||||||
</div>
|
|
||||||
<div class="col-md-2 col-4">
|
|
||||||
<strong>@materialSummary.TotalPieces</strong> pieces
|
|
||||||
</div>
|
|
||||||
<div class="col-md-2 col-4">
|
|
||||||
<strong>@materialSummary.Efficiency.ToString("F1")%</strong> efficiency
|
|
||||||
</div>
|
|
||||||
<div class="col-md-3 col-6">
|
|
||||||
<span class="text-success">@materialSummary.InStockBins in stock</span>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-3 col-6">
|
|
||||||
<span class="text-warning">@materialSummary.ToBePurchasedBins to purchase</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
@if (materialResult.PackResult.ItemsNotUsed.Count > 0)
|
|
||||||
{
|
|
||||||
<div class="alert alert-danger">
|
|
||||||
<strong>@materialResult.PackResult.ItemsNotUsed.Count items not placed</strong> -
|
|
||||||
No stock lengths available or parts too long.
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
@if (materialResult.InStockBins.Count > 0)
|
|
||||||
{
|
|
||||||
<h5 class="text-success mt-3">In Stock (@materialResult.InStockBins.Count bars)</h5>
|
|
||||||
@RenderBinList(materialResult.InStockBins)
|
|
||||||
}
|
|
||||||
|
|
||||||
@if (materialResult.ToBePurchasedBins.Count > 0)
|
|
||||||
{
|
|
||||||
<h5 class="text-warning mt-3">To Be Purchased (@materialResult.ToBePurchasedBins.Count bars)</h5>
|
|
||||||
@RenderBinList(materialResult.ToBePurchasedBins)
|
|
||||||
|
|
||||||
<!-- Purchase Summary -->
|
|
||||||
<div class="mt-3 p-3 bg-light rounded">
|
|
||||||
<strong>Order Summary:</strong>
|
|
||||||
<ul class="mb-0 mt-2">
|
|
||||||
@foreach (var group in materialResult.ToBePurchasedBins.GroupBy(b => b.Length).OrderByDescending(g => g.Key))
|
|
||||||
{
|
|
||||||
<li>@group.Count() x @ArchUnits.FormatFromInches(group.Key)</li>
|
|
||||||
}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@code {
|
|
||||||
[Parameter]
|
|
||||||
public int Id { get; set; }
|
|
||||||
|
|
||||||
private Project? project;
|
|
||||||
private MultiMaterialPackResult? packResult;
|
|
||||||
private MultiMaterialPackingSummary? summary;
|
|
||||||
private bool loading = true;
|
|
||||||
|
|
||||||
private bool CanOptimize => project != null &&
|
|
||||||
project.Parts.Count > 0 &&
|
|
||||||
project.CuttingToolId != null;
|
|
||||||
|
|
||||||
protected override async Task OnInitializedAsync()
|
|
||||||
{
|
|
||||||
project = await ProjectService.GetByIdAsync(Id);
|
|
||||||
|
|
||||||
if (project != null && CanOptimize)
|
|
||||||
{
|
|
||||||
var kerf = project.CuttingTool?.KerfInches ?? 0.125m;
|
|
||||||
packResult = await PackingService.PackAsync(project.Parts, kerf);
|
|
||||||
summary = PackingService.GetSummary(packResult);
|
|
||||||
}
|
|
||||||
|
|
||||||
loading = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private RenderFragment RenderBinList(List<Bin> bins) => __builder =>
|
|
||||||
{
|
|
||||||
<div class="table-responsive">
|
|
||||||
<table class="table table-sm table-striped">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th style="width: 80px;">#</th>
|
|
||||||
<th>Stock Length</th>
|
|
||||||
<th>Cuts</th>
|
|
||||||
<th>Waste</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
@{ var binNumber = 1; }
|
|
||||||
@foreach (var bin in bins)
|
|
||||||
{
|
|
||||||
<tr>
|
|
||||||
<td>@binNumber</td>
|
|
||||||
<td>@ArchUnits.FormatFromInches(bin.Length)</td>
|
|
||||||
<td>
|
|
||||||
@foreach (var item in bin.Items)
|
|
||||||
{
|
|
||||||
<span class="badge bg-primary me-1">
|
|
||||||
@(string.IsNullOrWhiteSpace(item.Name) ? ArchUnits.FormatFromInches(item.Length) : $"{item.Name} ({ArchUnits.FormatFromInches(item.Length)})")
|
|
||||||
</span>
|
|
||||||
}
|
|
||||||
</td>
|
|
||||||
<td>@ArchUnits.FormatFromInches(bin.RemainingLength)</td>
|
|
||||||
</tr>
|
|
||||||
binNumber++;
|
|
||||||
}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
};
|
|
||||||
|
|
||||||
private async Task PrintReport()
|
|
||||||
{
|
|
||||||
var filename = $"CutList - {project!.Name} - {DateTime.Now:yyyy-MM-dd}";
|
|
||||||
await JS.InvokeVoidAsync("printWithTitle", filename);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
namespace CutList.Web.Data.Entities;
|
|
||||||
|
|
||||||
public class MaterialStockLength
|
|
||||||
{
|
|
||||||
public int Id { get; set; }
|
|
||||||
public int MaterialId { get; set; }
|
|
||||||
public decimal LengthInches { get; set; }
|
|
||||||
public int Quantity { get; set; } = 0;
|
|
||||||
public string? Notes { get; set; }
|
|
||||||
public bool IsActive { get; set; } = true;
|
|
||||||
|
|
||||||
public Material Material { get; set; } = null!;
|
|
||||||
}
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
namespace CutList.Web.Data.Entities;
|
|
||||||
|
|
||||||
public class Project
|
|
||||||
{
|
|
||||||
public int Id { get; set; }
|
|
||||||
public string Name { get; set; } = string.Empty;
|
|
||||||
public string? Customer { get; set; }
|
|
||||||
public int? CuttingToolId { get; set; }
|
|
||||||
public string? Notes { get; set; }
|
|
||||||
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
|
||||||
public DateTime? UpdatedAt { get; set; }
|
|
||||||
|
|
||||||
public CuttingTool? CuttingTool { get; set; }
|
|
||||||
public ICollection<ProjectPart> Parts { get; set; } = new List<ProjectPart>();
|
|
||||||
}
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
namespace CutList.Web.Data.Entities;
|
|
||||||
|
|
||||||
public class ProjectPart
|
|
||||||
{
|
|
||||||
public int Id { get; set; }
|
|
||||||
public int ProjectId { get; set; }
|
|
||||||
public int MaterialId { get; set; }
|
|
||||||
public string Name { get; set; } = string.Empty;
|
|
||||||
public decimal LengthInches { get; set; }
|
|
||||||
public int Quantity { get; set; } = 1;
|
|
||||||
public int SortOrder { get; set; }
|
|
||||||
|
|
||||||
public Project Project { get; set; } = null!;
|
|
||||||
public Material Material { get; set; } = null!;
|
|
||||||
}
|
|
||||||
@@ -1,213 +0,0 @@
|
|||||||
using CutList.Web.Data;
|
|
||||||
using CutList.Web.Data.Entities;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
|
|
||||||
namespace CutList.Web.Services;
|
|
||||||
|
|
||||||
public class ProjectService
|
|
||||||
{
|
|
||||||
private readonly ApplicationDbContext _context;
|
|
||||||
|
|
||||||
public ProjectService(ApplicationDbContext context)
|
|
||||||
{
|
|
||||||
_context = context;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<List<Project>> GetAllAsync()
|
|
||||||
{
|
|
||||||
return await _context.Projects
|
|
||||||
.Include(p => p.CuttingTool)
|
|
||||||
.Include(p => p.Parts)
|
|
||||||
.ThenInclude(pt => pt.Material)
|
|
||||||
.OrderByDescending(p => p.UpdatedAt ?? p.CreatedAt)
|
|
||||||
.ToListAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<Project?> GetByIdAsync(int id)
|
|
||||||
{
|
|
||||||
return await _context.Projects
|
|
||||||
.Include(p => p.CuttingTool)
|
|
||||||
.Include(p => p.Parts.OrderBy(pt => pt.SortOrder))
|
|
||||||
.ThenInclude(pt => pt.Material)
|
|
||||||
.FirstOrDefaultAsync(p => p.Id == id);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<Project> CreateAsync(Project project)
|
|
||||||
{
|
|
||||||
project.CreatedAt = DateTime.UtcNow;
|
|
||||||
_context.Projects.Add(project);
|
|
||||||
await _context.SaveChangesAsync();
|
|
||||||
return project;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task UpdateAsync(Project project)
|
|
||||||
{
|
|
||||||
project.UpdatedAt = DateTime.UtcNow;
|
|
||||||
_context.Projects.Update(project);
|
|
||||||
await _context.SaveChangesAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task DeleteAsync(int id)
|
|
||||||
{
|
|
||||||
var project = await _context.Projects.FindAsync(id);
|
|
||||||
if (project != null)
|
|
||||||
{
|
|
||||||
_context.Projects.Remove(project);
|
|
||||||
await _context.SaveChangesAsync();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<Project> DuplicateAsync(int id)
|
|
||||||
{
|
|
||||||
var original = await GetByIdAsync(id);
|
|
||||||
if (original == null)
|
|
||||||
{
|
|
||||||
throw new ArgumentException("Project not found", nameof(id));
|
|
||||||
}
|
|
||||||
|
|
||||||
var duplicate = new Project
|
|
||||||
{
|
|
||||||
Name = $"{original.Name} (Copy)",
|
|
||||||
Customer = original.Customer,
|
|
||||||
CuttingToolId = original.CuttingToolId,
|
|
||||||
Notes = original.Notes,
|
|
||||||
CreatedAt = DateTime.UtcNow
|
|
||||||
};
|
|
||||||
|
|
||||||
_context.Projects.Add(duplicate);
|
|
||||||
await _context.SaveChangesAsync();
|
|
||||||
|
|
||||||
// Copy parts
|
|
||||||
foreach (var part in original.Parts)
|
|
||||||
{
|
|
||||||
_context.ProjectParts.Add(new ProjectPart
|
|
||||||
{
|
|
||||||
ProjectId = duplicate.Id,
|
|
||||||
MaterialId = part.MaterialId,
|
|
||||||
Name = part.Name,
|
|
||||||
LengthInches = part.LengthInches,
|
|
||||||
Quantity = part.Quantity,
|
|
||||||
SortOrder = part.SortOrder
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
await _context.SaveChangesAsync();
|
|
||||||
return duplicate;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parts management
|
|
||||||
public async Task<ProjectPart> AddPartAsync(ProjectPart part)
|
|
||||||
{
|
|
||||||
var maxOrder = await _context.ProjectParts
|
|
||||||
.Where(p => p.ProjectId == part.ProjectId)
|
|
||||||
.MaxAsync(p => (int?)p.SortOrder) ?? -1;
|
|
||||||
part.SortOrder = maxOrder + 1;
|
|
||||||
|
|
||||||
_context.ProjectParts.Add(part);
|
|
||||||
await _context.SaveChangesAsync();
|
|
||||||
|
|
||||||
// Update project timestamp
|
|
||||||
var project = await _context.Projects.FindAsync(part.ProjectId);
|
|
||||||
if (project != null)
|
|
||||||
{
|
|
||||||
project.UpdatedAt = DateTime.UtcNow;
|
|
||||||
await _context.SaveChangesAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
return part;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task UpdatePartAsync(ProjectPart part)
|
|
||||||
{
|
|
||||||
_context.ProjectParts.Update(part);
|
|
||||||
await _context.SaveChangesAsync();
|
|
||||||
|
|
||||||
var project = await _context.Projects.FindAsync(part.ProjectId);
|
|
||||||
if (project != null)
|
|
||||||
{
|
|
||||||
project.UpdatedAt = DateTime.UtcNow;
|
|
||||||
await _context.SaveChangesAsync();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task DeletePartAsync(int id)
|
|
||||||
{
|
|
||||||
var part = await _context.ProjectParts.FindAsync(id);
|
|
||||||
if (part != null)
|
|
||||||
{
|
|
||||||
var projectId = part.ProjectId;
|
|
||||||
_context.ProjectParts.Remove(part);
|
|
||||||
await _context.SaveChangesAsync();
|
|
||||||
|
|
||||||
var project = await _context.Projects.FindAsync(projectId);
|
|
||||||
if (project != null)
|
|
||||||
{
|
|
||||||
project.UpdatedAt = DateTime.UtcNow;
|
|
||||||
await _context.SaveChangesAsync();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cutting tools
|
|
||||||
public async Task<List<CuttingTool>> GetCuttingToolsAsync(bool includeInactive = false)
|
|
||||||
{
|
|
||||||
var query = _context.CuttingTools.AsQueryable();
|
|
||||||
if (!includeInactive)
|
|
||||||
{
|
|
||||||
query = query.Where(t => t.IsActive);
|
|
||||||
}
|
|
||||||
return await query.OrderBy(t => t.Name).ToListAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<CuttingTool?> GetCuttingToolByIdAsync(int id)
|
|
||||||
{
|
|
||||||
return await _context.CuttingTools.FindAsync(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<CuttingTool?> GetDefaultCuttingToolAsync()
|
|
||||||
{
|
|
||||||
return await _context.CuttingTools.FirstOrDefaultAsync(t => t.IsDefault && t.IsActive);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<CuttingTool> CreateCuttingToolAsync(CuttingTool tool)
|
|
||||||
{
|
|
||||||
if (tool.IsDefault)
|
|
||||||
{
|
|
||||||
// Clear other defaults
|
|
||||||
var others = await _context.CuttingTools.Where(t => t.IsDefault).ToListAsync();
|
|
||||||
foreach (var other in others)
|
|
||||||
{
|
|
||||||
other.IsDefault = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_context.CuttingTools.Add(tool);
|
|
||||||
await _context.SaveChangesAsync();
|
|
||||||
return tool;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task UpdateCuttingToolAsync(CuttingTool tool)
|
|
||||||
{
|
|
||||||
if (tool.IsDefault)
|
|
||||||
{
|
|
||||||
var others = await _context.CuttingTools.Where(t => t.IsDefault && t.Id != tool.Id).ToListAsync();
|
|
||||||
foreach (var other in others)
|
|
||||||
{
|
|
||||||
other.IsDefault = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_context.CuttingTools.Update(tool);
|
|
||||||
await _context.SaveChangesAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task DeleteCuttingToolAsync(int id)
|
|
||||||
{
|
|
||||||
var tool = await _context.CuttingTools.FindAsync(id);
|
|
||||||
if (tool != null)
|
|
||||||
{
|
|
||||||
tool.IsActive = false;
|
|
||||||
await _context.SaveChangesAsync();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user