refactor: Rename Project to Job with enhanced model
Rename the Project concept to Job for clarity: - Add Job, JobPart, JobStock entities - JobStock supports both inventory stock and custom lengths - Add JobNumber field for job identification - Add priority-based stock allocation for cut optimization - Include Jobs UI pages (Index, Edit, Results) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
810
CutList.Web/Components/Pages/Jobs/Edit.razor
Normal file
810
CutList.Web/Components/Pages/Jobs/Edit.razor
Normal file
@@ -0,0 +1,810 @@
|
|||||||
|
@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>
|
||||||
|
}
|
||||||
|
|
||||||
|
@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.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 (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.GetDisplayName()</option>
|
||||||
|
}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-2">
|
||||||
|
<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-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 (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.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.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}");
|
||||||
|
}
|
||||||
|
}
|
||||||
258
CutList.Web/Components/Pages/Jobs/Results.razor
Normal file
258
CutList.Web/Components/Pages/Jobs/Results.razor
Normal file
@@ -0,0 +1,258 @@
|
|||||||
|
@page "/jobs/{Id:int}/results"
|
||||||
|
@inject JobService JobService
|
||||||
|
@inject CutListPackingService PackingService
|
||||||
|
@inject NavigationManager Navigation
|
||||||
|
@inject IJSRuntime JS
|
||||||
|
@using CutList.Core
|
||||||
|
@using CutList.Core.Nesting
|
||||||
|
@using CutList.Core.Formatting
|
||||||
|
|
||||||
|
<PageTitle>Results - @(job?.DisplayName ?? "Job")</PageTitle>
|
||||||
|
|
||||||
|
@if (loading)
|
||||||
|
{
|
||||||
|
<p><em>Loading...</em></p>
|
||||||
|
}
|
||||||
|
else if (job == null)
|
||||||
|
{
|
||||||
|
<div class="alert alert-danger">Job not found.</div>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||||
|
<div>
|
||||||
|
<h1>@job.DisplayName</h1>
|
||||||
|
@if (!string.IsNullOrWhiteSpace(job.Customer))
|
||||||
|
{
|
||||||
|
<p class="text-muted mb-0">Customer: @job.Customer</p>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<a href="jobs/@Id" class="btn btn-outline-secondary me-2">Edit Job</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 (job.Parts.Count == 0)
|
||||||
|
{
|
||||||
|
<li>No parts defined. <a href="jobs/@Id">Add parts to the job</a>.</li>
|
||||||
|
}
|
||||||
|
@if (job.CuttingToolId == null)
|
||||||
|
{
|
||||||
|
<li>No cutting tool selected. <a href="jobs/@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 Job? job;
|
||||||
|
private MultiMaterialPackResult? packResult;
|
||||||
|
private MultiMaterialPackingSummary? summary;
|
||||||
|
private bool loading = true;
|
||||||
|
|
||||||
|
private bool CanOptimize => job != null &&
|
||||||
|
job.Parts.Count > 0 &&
|
||||||
|
job.CuttingToolId != null;
|
||||||
|
|
||||||
|
protected override async Task OnInitializedAsync()
|
||||||
|
{
|
||||||
|
job = await JobService.GetByIdAsync(Id);
|
||||||
|
|
||||||
|
if (job != null && CanOptimize)
|
||||||
|
{
|
||||||
|
var kerf = job.CuttingTool?.KerfInches ?? 0.125m;
|
||||||
|
// Pass job stock if configured, otherwise packing service uses all available stock
|
||||||
|
packResult = await PackingService.PackAsync(job.Parts, kerf, job.Stock.Count > 0 ? job.Stock : null);
|
||||||
|
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 - {job!.Name} - {DateTime.Now:yyyy-MM-dd}";
|
||||||
|
await JS.InvokeVoidAsync("printWithTitle", filename);
|
||||||
|
}
|
||||||
|
}
|
||||||
19
CutList.Web/Data/Entities/Job.cs
Normal file
19
CutList.Web/Data/Entities/Job.cs
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
namespace CutList.Web.Data.Entities;
|
||||||
|
|
||||||
|
public class Job
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
public string JobNumber { get; set; } = string.Empty;
|
||||||
|
public string? Name { get; set; }
|
||||||
|
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<JobPart> Parts { get; set; } = new List<JobPart>();
|
||||||
|
public ICollection<JobStock> Stock { get; set; } = new List<JobStock>();
|
||||||
|
|
||||||
|
public string DisplayName => string.IsNullOrWhiteSpace(Name) ? JobNumber : $"{JobNumber} - {Name}";
|
||||||
|
}
|
||||||
15
CutList.Web/Data/Entities/JobPart.cs
Normal file
15
CutList.Web/Data/Entities/JobPart.cs
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
namespace CutList.Web.Data.Entities;
|
||||||
|
|
||||||
|
public class JobPart
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
public int JobId { 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 Job Job { get; set; } = null!;
|
||||||
|
public Material Material { get; set; } = null!;
|
||||||
|
}
|
||||||
46
CutList.Web/Data/Entities/JobStock.cs
Normal file
46
CutList.Web/Data/Entities/JobStock.cs
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
namespace CutList.Web.Data.Entities;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents stock allocated to a specific job.
|
||||||
|
/// Can reference an existing StockItem with quantity override,
|
||||||
|
/// or define a custom length just for this job.
|
||||||
|
/// </summary>
|
||||||
|
public class JobStock
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
public int JobId { get; set; }
|
||||||
|
public int MaterialId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// If set, references an existing stock item. Null for custom job-specific lengths.
|
||||||
|
/// </summary>
|
||||||
|
public int? StockItemId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Length in inches. For stock items, copied from StockItem.LengthInches.
|
||||||
|
/// For custom lengths, user-specified.
|
||||||
|
/// </summary>
|
||||||
|
public decimal LengthInches { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Quantity to use for this job. Can be less than or equal to available stock.
|
||||||
|
/// For custom lengths, represents unlimited available.
|
||||||
|
/// </summary>
|
||||||
|
public int Quantity { get; set; } = 1;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// True if this is a custom length just for this job (not from inventory).
|
||||||
|
/// </summary>
|
||||||
|
public bool IsCustomLength { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Priority for bin packing. Lower values are used first.
|
||||||
|
/// </summary>
|
||||||
|
public int Priority { get; set; } = 10;
|
||||||
|
|
||||||
|
public int SortOrder { get; set; }
|
||||||
|
|
||||||
|
public Job Job { get; set; } = null!;
|
||||||
|
public Material Material { get; set; } = null!;
|
||||||
|
public StockItem? StockItem { get; set; }
|
||||||
|
}
|
||||||
452
CutList.Web/Migrations/20260204214947_RenameProjectToJob.Designer.cs
generated
Normal file
452
CutList.Web/Migrations/20260204214947_RenameProjectToJob.Designer.cs
generated
Normal file
@@ -0,0 +1,452 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using CutList.Web.Data;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Metadata;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace CutList.Web.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(ApplicationDbContext))]
|
||||||
|
[Migration("20260204214947_RenameProjectToJob")]
|
||||||
|
partial class RenameProjectToJob
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder
|
||||||
|
.HasAnnotation("ProductVersion", "8.0.11")
|
||||||
|
.HasAnnotation("Relational:MaxIdentifierLength", 128);
|
||||||
|
|
||||||
|
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
|
||||||
|
|
||||||
|
modelBuilder.Entity("CutList.Web.Data.Entities.CuttingTool", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<bool>("IsActive")
|
||||||
|
.HasColumnType("bit");
|
||||||
|
|
||||||
|
b.Property<bool>("IsDefault")
|
||||||
|
.HasColumnType("bit");
|
||||||
|
|
||||||
|
b.Property<decimal>("KerfInches")
|
||||||
|
.HasPrecision(6, 4)
|
||||||
|
.HasColumnType("decimal(6,4)");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(50)
|
||||||
|
.HasColumnType("nvarchar(50)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("CuttingTools");
|
||||||
|
|
||||||
|
b.HasData(
|
||||||
|
new
|
||||||
|
{
|
||||||
|
Id = 1,
|
||||||
|
IsActive = true,
|
||||||
|
IsDefault = true,
|
||||||
|
KerfInches = 0.0625m,
|
||||||
|
Name = "Bandsaw"
|
||||||
|
},
|
||||||
|
new
|
||||||
|
{
|
||||||
|
Id = 2,
|
||||||
|
IsActive = true,
|
||||||
|
IsDefault = false,
|
||||||
|
KerfInches = 0.125m,
|
||||||
|
Name = "Chop Saw"
|
||||||
|
},
|
||||||
|
new
|
||||||
|
{
|
||||||
|
Id = 3,
|
||||||
|
IsActive = true,
|
||||||
|
IsDefault = false,
|
||||||
|
KerfInches = 0.0625m,
|
||||||
|
Name = "Cold Cut Saw"
|
||||||
|
},
|
||||||
|
new
|
||||||
|
{
|
||||||
|
Id = 4,
|
||||||
|
IsActive = true,
|
||||||
|
IsDefault = false,
|
||||||
|
KerfInches = 0.0625m,
|
||||||
|
Name = "Hacksaw"
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("CutList.Web.Data.Entities.Job", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("datetime2")
|
||||||
|
.HasDefaultValueSql("GETUTCDATE()");
|
||||||
|
|
||||||
|
b.Property<string>("Customer")
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("nvarchar(100)");
|
||||||
|
|
||||||
|
b.Property<int?>("CuttingToolId")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("nvarchar(100)");
|
||||||
|
|
||||||
|
b.Property<string>("Notes")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("UpdatedAt")
|
||||||
|
.HasColumnType("datetime2");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("CuttingToolId");
|
||||||
|
|
||||||
|
b.ToTable("Jobs");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("CutList.Web.Data.Entities.JobPart", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<int>("JobId")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<decimal>("LengthInches")
|
||||||
|
.HasPrecision(10, 4)
|
||||||
|
.HasColumnType("decimal(10,4)");
|
||||||
|
|
||||||
|
b.Property<int>("MaterialId")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("nvarchar(100)");
|
||||||
|
|
||||||
|
b.Property<int>("Quantity")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<int>("SortOrder")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("JobId");
|
||||||
|
|
||||||
|
b.HasIndex("MaterialId");
|
||||||
|
|
||||||
|
b.ToTable("JobParts");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("CutList.Web.Data.Entities.Material", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("datetime2")
|
||||||
|
.HasDefaultValueSql("GETUTCDATE()");
|
||||||
|
|
||||||
|
b.Property<string>("Description")
|
||||||
|
.HasMaxLength(255)
|
||||||
|
.HasColumnType("nvarchar(255)");
|
||||||
|
|
||||||
|
b.Property<bool>("IsActive")
|
||||||
|
.HasColumnType("bit");
|
||||||
|
|
||||||
|
b.Property<string>("Shape")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(50)
|
||||||
|
.HasColumnType("nvarchar(50)");
|
||||||
|
|
||||||
|
b.Property<string>("Size")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("nvarchar(100)");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("UpdatedAt")
|
||||||
|
.HasColumnType("datetime2");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("Materials");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("CutList.Web.Data.Entities.MaterialStockLength", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<bool>("IsActive")
|
||||||
|
.HasColumnType("bit");
|
||||||
|
|
||||||
|
b.Property<decimal>("LengthInches")
|
||||||
|
.HasPrecision(10, 4)
|
||||||
|
.HasColumnType("decimal(10,4)");
|
||||||
|
|
||||||
|
b.Property<int>("MaterialId")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<string>("Notes")
|
||||||
|
.HasMaxLength(255)
|
||||||
|
.HasColumnType("nvarchar(255)");
|
||||||
|
|
||||||
|
b.Property<int>("Quantity")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("MaterialId", "LengthInches")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("MaterialStockLengths");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("CutList.Web.Data.Entities.StockItem", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("datetime2")
|
||||||
|
.HasDefaultValueSql("GETUTCDATE()");
|
||||||
|
|
||||||
|
b.Property<bool>("IsActive")
|
||||||
|
.HasColumnType("bit");
|
||||||
|
|
||||||
|
b.Property<decimal>("LengthInches")
|
||||||
|
.HasPrecision(10, 4)
|
||||||
|
.HasColumnType("decimal(10,4)");
|
||||||
|
|
||||||
|
b.Property<int>("MaterialId")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("nvarchar(100)");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("UpdatedAt")
|
||||||
|
.HasColumnType("datetime2");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("MaterialId", "LengthInches")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("StockItems");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("CutList.Web.Data.Entities.Supplier", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<string>("ContactInfo")
|
||||||
|
.HasMaxLength(500)
|
||||||
|
.HasColumnType("nvarchar(500)");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("datetime2")
|
||||||
|
.HasDefaultValueSql("GETUTCDATE()");
|
||||||
|
|
||||||
|
b.Property<bool>("IsActive")
|
||||||
|
.HasColumnType("bit");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("nvarchar(100)");
|
||||||
|
|
||||||
|
b.Property<string>("Notes")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("Suppliers");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("CutList.Web.Data.Entities.SupplierOffering", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<bool>("IsActive")
|
||||||
|
.HasColumnType("bit");
|
||||||
|
|
||||||
|
b.Property<string>("Notes")
|
||||||
|
.HasMaxLength(255)
|
||||||
|
.HasColumnType("nvarchar(255)");
|
||||||
|
|
||||||
|
b.Property<string>("PartNumber")
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("nvarchar(100)");
|
||||||
|
|
||||||
|
b.Property<decimal?>("Price")
|
||||||
|
.HasPrecision(10, 2)
|
||||||
|
.HasColumnType("decimal(10,2)");
|
||||||
|
|
||||||
|
b.Property<int>("StockItemId")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<string>("SupplierDescription")
|
||||||
|
.HasMaxLength(255)
|
||||||
|
.HasColumnType("nvarchar(255)");
|
||||||
|
|
||||||
|
b.Property<int>("SupplierId")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("StockItemId");
|
||||||
|
|
||||||
|
b.HasIndex("SupplierId", "StockItemId")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("SupplierOfferings");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("CutList.Web.Data.Entities.Job", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("CutList.Web.Data.Entities.CuttingTool", "CuttingTool")
|
||||||
|
.WithMany("Jobs")
|
||||||
|
.HasForeignKey("CuttingToolId")
|
||||||
|
.OnDelete(DeleteBehavior.SetNull);
|
||||||
|
|
||||||
|
b.Navigation("CuttingTool");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("CutList.Web.Data.Entities.JobPart", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("CutList.Web.Data.Entities.Job", "Job")
|
||||||
|
.WithMany("Parts")
|
||||||
|
.HasForeignKey("JobId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("CutList.Web.Data.Entities.Material", "Material")
|
||||||
|
.WithMany("JobParts")
|
||||||
|
.HasForeignKey("MaterialId")
|
||||||
|
.OnDelete(DeleteBehavior.Restrict)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Job");
|
||||||
|
|
||||||
|
b.Navigation("Material");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("CutList.Web.Data.Entities.MaterialStockLength", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("CutList.Web.Data.Entities.Material", "Material")
|
||||||
|
.WithMany("StockLengths")
|
||||||
|
.HasForeignKey("MaterialId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Material");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("CutList.Web.Data.Entities.StockItem", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("CutList.Web.Data.Entities.Material", "Material")
|
||||||
|
.WithMany("StockItems")
|
||||||
|
.HasForeignKey("MaterialId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Material");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("CutList.Web.Data.Entities.SupplierOffering", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("CutList.Web.Data.Entities.StockItem", "StockItem")
|
||||||
|
.WithMany("SupplierOfferings")
|
||||||
|
.HasForeignKey("StockItemId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("CutList.Web.Data.Entities.Supplier", "Supplier")
|
||||||
|
.WithMany("Offerings")
|
||||||
|
.HasForeignKey("SupplierId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("StockItem");
|
||||||
|
|
||||||
|
b.Navigation("Supplier");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("CutList.Web.Data.Entities.CuttingTool", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Jobs");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("CutList.Web.Data.Entities.Job", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Parts");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("CutList.Web.Data.Entities.Material", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("JobParts");
|
||||||
|
|
||||||
|
b.Navigation("StockItems");
|
||||||
|
|
||||||
|
b.Navigation("StockLengths");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("CutList.Web.Data.Entities.StockItem", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("SupplierOfferings");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("CutList.Web.Data.Entities.Supplier", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Offerings");
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
169
CutList.Web/Migrations/20260204214947_RenameProjectToJob.cs
Normal file
169
CutList.Web/Migrations/20260204214947_RenameProjectToJob.cs
Normal file
@@ -0,0 +1,169 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace CutList.Web.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class RenameProjectToJob : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "ProjectParts");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "Projects");
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "Jobs",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<int>(type: "int", nullable: false)
|
||||||
|
.Annotation("SqlServer:Identity", "1, 1"),
|
||||||
|
Name = table.Column<string>(type: "nvarchar(100)", maxLength: 100, nullable: false),
|
||||||
|
Customer = table.Column<string>(type: "nvarchar(100)", maxLength: 100, nullable: true),
|
||||||
|
CuttingToolId = table.Column<int>(type: "int", nullable: true),
|
||||||
|
Notes = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||||
|
CreatedAt = table.Column<DateTime>(type: "datetime2", nullable: false, defaultValueSql: "GETUTCDATE()"),
|
||||||
|
UpdatedAt = table.Column<DateTime>(type: "datetime2", nullable: true)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_Jobs", x => x.Id);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_Jobs_CuttingTools_CuttingToolId",
|
||||||
|
column: x => x.CuttingToolId,
|
||||||
|
principalTable: "CuttingTools",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.SetNull);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "JobParts",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<int>(type: "int", nullable: false)
|
||||||
|
.Annotation("SqlServer:Identity", "1, 1"),
|
||||||
|
JobId = table.Column<int>(type: "int", nullable: false),
|
||||||
|
MaterialId = table.Column<int>(type: "int", nullable: false),
|
||||||
|
Name = table.Column<string>(type: "nvarchar(100)", maxLength: 100, nullable: false),
|
||||||
|
LengthInches = table.Column<decimal>(type: "decimal(10,4)", precision: 10, scale: 4, nullable: false),
|
||||||
|
Quantity = table.Column<int>(type: "int", nullable: false),
|
||||||
|
SortOrder = table.Column<int>(type: "int", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_JobParts", x => x.Id);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_JobParts_Jobs_JobId",
|
||||||
|
column: x => x.JobId,
|
||||||
|
principalTable: "Jobs",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_JobParts_Materials_MaterialId",
|
||||||
|
column: x => x.MaterialId,
|
||||||
|
principalTable: "Materials",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Restrict);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_JobParts_JobId",
|
||||||
|
table: "JobParts",
|
||||||
|
column: "JobId");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_JobParts_MaterialId",
|
||||||
|
table: "JobParts",
|
||||||
|
column: "MaterialId");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_Jobs_CuttingToolId",
|
||||||
|
table: "Jobs",
|
||||||
|
column: "CuttingToolId");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "JobParts");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "Jobs");
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "Projects",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<int>(type: "int", nullable: false)
|
||||||
|
.Annotation("SqlServer:Identity", "1, 1"),
|
||||||
|
CuttingToolId = table.Column<int>(type: "int", nullable: true),
|
||||||
|
CreatedAt = table.Column<DateTime>(type: "datetime2", nullable: false, defaultValueSql: "GETUTCDATE()"),
|
||||||
|
Customer = table.Column<string>(type: "nvarchar(100)", maxLength: 100, nullable: true),
|
||||||
|
Name = table.Column<string>(type: "nvarchar(100)", maxLength: 100, nullable: false),
|
||||||
|
Notes = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||||
|
UpdatedAt = table.Column<DateTime>(type: "datetime2", nullable: true)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_Projects", x => x.Id);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_Projects_CuttingTools_CuttingToolId",
|
||||||
|
column: x => x.CuttingToolId,
|
||||||
|
principalTable: "CuttingTools",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.SetNull);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "ProjectParts",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<int>(type: "int", nullable: false)
|
||||||
|
.Annotation("SqlServer:Identity", "1, 1"),
|
||||||
|
MaterialId = table.Column<int>(type: "int", nullable: false),
|
||||||
|
ProjectId = table.Column<int>(type: "int", nullable: false),
|
||||||
|
LengthInches = table.Column<decimal>(type: "decimal(10,4)", precision: 10, scale: 4, nullable: false),
|
||||||
|
Name = table.Column<string>(type: "nvarchar(100)", maxLength: 100, nullable: false),
|
||||||
|
Quantity = table.Column<int>(type: "int", nullable: false),
|
||||||
|
SortOrder = table.Column<int>(type: "int", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_ProjectParts", x => x.Id);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_ProjectParts_Materials_MaterialId",
|
||||||
|
column: x => x.MaterialId,
|
||||||
|
principalTable: "Materials",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Restrict);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_ProjectParts_Projects_ProjectId",
|
||||||
|
column: x => x.ProjectId,
|
||||||
|
principalTable: "Projects",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_ProjectParts_MaterialId",
|
||||||
|
table: "ProjectParts",
|
||||||
|
column: "MaterialId");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_ProjectParts_ProjectId",
|
||||||
|
table: "ProjectParts",
|
||||||
|
column: "ProjectId");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_Projects_CuttingToolId",
|
||||||
|
table: "Projects",
|
||||||
|
column: "CuttingToolId");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
494
CutList.Web/Migrations/20260204222017_AddJobNumber.Designer.cs
generated
Normal file
494
CutList.Web/Migrations/20260204222017_AddJobNumber.Designer.cs
generated
Normal file
@@ -0,0 +1,494 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using CutList.Web.Data;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Metadata;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace CutList.Web.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(ApplicationDbContext))]
|
||||||
|
[Migration("20260204222017_AddJobNumber")]
|
||||||
|
partial class AddJobNumber
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder
|
||||||
|
.HasAnnotation("ProductVersion", "8.0.11")
|
||||||
|
.HasAnnotation("Relational:MaxIdentifierLength", 128);
|
||||||
|
|
||||||
|
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
|
||||||
|
|
||||||
|
modelBuilder.Entity("CutList.Web.Data.Entities.CuttingTool", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<bool>("IsActive")
|
||||||
|
.HasColumnType("bit");
|
||||||
|
|
||||||
|
b.Property<bool>("IsDefault")
|
||||||
|
.HasColumnType("bit");
|
||||||
|
|
||||||
|
b.Property<decimal>("KerfInches")
|
||||||
|
.HasPrecision(6, 4)
|
||||||
|
.HasColumnType("decimal(6,4)");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(50)
|
||||||
|
.HasColumnType("nvarchar(50)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("CuttingTools");
|
||||||
|
|
||||||
|
b.HasData(
|
||||||
|
new
|
||||||
|
{
|
||||||
|
Id = 1,
|
||||||
|
IsActive = true,
|
||||||
|
IsDefault = true,
|
||||||
|
KerfInches = 0.0625m,
|
||||||
|
Name = "Bandsaw"
|
||||||
|
},
|
||||||
|
new
|
||||||
|
{
|
||||||
|
Id = 2,
|
||||||
|
IsActive = true,
|
||||||
|
IsDefault = false,
|
||||||
|
KerfInches = 0.125m,
|
||||||
|
Name = "Chop Saw"
|
||||||
|
},
|
||||||
|
new
|
||||||
|
{
|
||||||
|
Id = 3,
|
||||||
|
IsActive = true,
|
||||||
|
IsDefault = false,
|
||||||
|
KerfInches = 0.0625m,
|
||||||
|
Name = "Cold Cut Saw"
|
||||||
|
},
|
||||||
|
new
|
||||||
|
{
|
||||||
|
Id = 4,
|
||||||
|
IsActive = true,
|
||||||
|
IsDefault = false,
|
||||||
|
KerfInches = 0.0625m,
|
||||||
|
Name = "Hacksaw"
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("CutList.Web.Data.Entities.Job", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("datetime2")
|
||||||
|
.HasDefaultValueSql("GETUTCDATE()");
|
||||||
|
|
||||||
|
b.Property<string>("Customer")
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("nvarchar(100)");
|
||||||
|
|
||||||
|
b.Property<int?>("CuttingToolId")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<string>("JobNumber")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(20)
|
||||||
|
.HasColumnType("nvarchar(20)");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("nvarchar(100)");
|
||||||
|
|
||||||
|
b.Property<string>("Notes")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("UpdatedAt")
|
||||||
|
.HasColumnType("datetime2");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("CuttingToolId");
|
||||||
|
|
||||||
|
b.HasIndex("JobNumber")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("Jobs");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("CutList.Web.Data.Entities.JobPart", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<int>("JobId")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<decimal>("LengthInches")
|
||||||
|
.HasPrecision(10, 4)
|
||||||
|
.HasColumnType("decimal(10,4)");
|
||||||
|
|
||||||
|
b.Property<int>("MaterialId")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("nvarchar(100)");
|
||||||
|
|
||||||
|
b.Property<int>("Quantity")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<int>("SortOrder")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("JobId");
|
||||||
|
|
||||||
|
b.HasIndex("MaterialId");
|
||||||
|
|
||||||
|
b.ToTable("JobParts");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("CutList.Web.Data.Entities.Material", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("datetime2")
|
||||||
|
.HasDefaultValueSql("GETUTCDATE()");
|
||||||
|
|
||||||
|
b.Property<string>("Description")
|
||||||
|
.HasMaxLength(255)
|
||||||
|
.HasColumnType("nvarchar(255)");
|
||||||
|
|
||||||
|
b.Property<bool>("IsActive")
|
||||||
|
.HasColumnType("bit");
|
||||||
|
|
||||||
|
b.Property<string>("Shape")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(50)
|
||||||
|
.HasColumnType("nvarchar(50)");
|
||||||
|
|
||||||
|
b.Property<string>("Size")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("nvarchar(100)");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("UpdatedAt")
|
||||||
|
.HasColumnType("datetime2");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("Materials");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("CutList.Web.Data.Entities.StockItem", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("datetime2")
|
||||||
|
.HasDefaultValueSql("GETUTCDATE()");
|
||||||
|
|
||||||
|
b.Property<bool>("IsActive")
|
||||||
|
.HasColumnType("bit");
|
||||||
|
|
||||||
|
b.Property<decimal>("LengthInches")
|
||||||
|
.HasPrecision(10, 4)
|
||||||
|
.HasColumnType("decimal(10,4)");
|
||||||
|
|
||||||
|
b.Property<int>("MaterialId")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("nvarchar(100)");
|
||||||
|
|
||||||
|
b.Property<string>("Notes")
|
||||||
|
.HasMaxLength(255)
|
||||||
|
.HasColumnType("nvarchar(255)");
|
||||||
|
|
||||||
|
b.Property<int>("QuantityOnHand")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("UpdatedAt")
|
||||||
|
.HasColumnType("datetime2");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("MaterialId", "LengthInches")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("StockItems");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("CutList.Web.Data.Entities.StockTransaction", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("datetime2")
|
||||||
|
.HasDefaultValueSql("GETUTCDATE()");
|
||||||
|
|
||||||
|
b.Property<int?>("JobId")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<string>("Notes")
|
||||||
|
.HasMaxLength(500)
|
||||||
|
.HasColumnType("nvarchar(500)");
|
||||||
|
|
||||||
|
b.Property<int>("Quantity")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<int>("StockItemId")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<int?>("SupplierId")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<int>("Type")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<decimal?>("UnitPrice")
|
||||||
|
.HasPrecision(10, 2)
|
||||||
|
.HasColumnType("decimal(10,2)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("JobId");
|
||||||
|
|
||||||
|
b.HasIndex("StockItemId");
|
||||||
|
|
||||||
|
b.HasIndex("SupplierId");
|
||||||
|
|
||||||
|
b.ToTable("StockTransactions");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("CutList.Web.Data.Entities.Supplier", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<string>("ContactInfo")
|
||||||
|
.HasMaxLength(500)
|
||||||
|
.HasColumnType("nvarchar(500)");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("datetime2")
|
||||||
|
.HasDefaultValueSql("GETUTCDATE()");
|
||||||
|
|
||||||
|
b.Property<bool>("IsActive")
|
||||||
|
.HasColumnType("bit");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("nvarchar(100)");
|
||||||
|
|
||||||
|
b.Property<string>("Notes")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("Suppliers");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("CutList.Web.Data.Entities.SupplierOffering", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<bool>("IsActive")
|
||||||
|
.HasColumnType("bit");
|
||||||
|
|
||||||
|
b.Property<string>("Notes")
|
||||||
|
.HasMaxLength(255)
|
||||||
|
.HasColumnType("nvarchar(255)");
|
||||||
|
|
||||||
|
b.Property<string>("PartNumber")
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("nvarchar(100)");
|
||||||
|
|
||||||
|
b.Property<decimal?>("Price")
|
||||||
|
.HasPrecision(10, 2)
|
||||||
|
.HasColumnType("decimal(10,2)");
|
||||||
|
|
||||||
|
b.Property<int>("StockItemId")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<string>("SupplierDescription")
|
||||||
|
.HasMaxLength(255)
|
||||||
|
.HasColumnType("nvarchar(255)");
|
||||||
|
|
||||||
|
b.Property<int>("SupplierId")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("StockItemId");
|
||||||
|
|
||||||
|
b.HasIndex("SupplierId", "StockItemId")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("SupplierOfferings");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("CutList.Web.Data.Entities.Job", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("CutList.Web.Data.Entities.CuttingTool", "CuttingTool")
|
||||||
|
.WithMany("Jobs")
|
||||||
|
.HasForeignKey("CuttingToolId")
|
||||||
|
.OnDelete(DeleteBehavior.SetNull);
|
||||||
|
|
||||||
|
b.Navigation("CuttingTool");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("CutList.Web.Data.Entities.JobPart", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("CutList.Web.Data.Entities.Job", "Job")
|
||||||
|
.WithMany("Parts")
|
||||||
|
.HasForeignKey("JobId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("CutList.Web.Data.Entities.Material", "Material")
|
||||||
|
.WithMany("JobParts")
|
||||||
|
.HasForeignKey("MaterialId")
|
||||||
|
.OnDelete(DeleteBehavior.Restrict)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Job");
|
||||||
|
|
||||||
|
b.Navigation("Material");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("CutList.Web.Data.Entities.StockItem", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("CutList.Web.Data.Entities.Material", "Material")
|
||||||
|
.WithMany("StockItems")
|
||||||
|
.HasForeignKey("MaterialId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Material");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("CutList.Web.Data.Entities.StockTransaction", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("CutList.Web.Data.Entities.Job", "Job")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("JobId")
|
||||||
|
.OnDelete(DeleteBehavior.SetNull);
|
||||||
|
|
||||||
|
b.HasOne("CutList.Web.Data.Entities.StockItem", "StockItem")
|
||||||
|
.WithMany("Transactions")
|
||||||
|
.HasForeignKey("StockItemId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("CutList.Web.Data.Entities.Supplier", "Supplier")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("SupplierId")
|
||||||
|
.OnDelete(DeleteBehavior.SetNull);
|
||||||
|
|
||||||
|
b.Navigation("Job");
|
||||||
|
|
||||||
|
b.Navigation("StockItem");
|
||||||
|
|
||||||
|
b.Navigation("Supplier");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("CutList.Web.Data.Entities.SupplierOffering", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("CutList.Web.Data.Entities.StockItem", "StockItem")
|
||||||
|
.WithMany("SupplierOfferings")
|
||||||
|
.HasForeignKey("StockItemId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("CutList.Web.Data.Entities.Supplier", "Supplier")
|
||||||
|
.WithMany("Offerings")
|
||||||
|
.HasForeignKey("SupplierId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("StockItem");
|
||||||
|
|
||||||
|
b.Navigation("Supplier");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("CutList.Web.Data.Entities.CuttingTool", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Jobs");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("CutList.Web.Data.Entities.Job", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Parts");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("CutList.Web.Data.Entities.Material", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("JobParts");
|
||||||
|
|
||||||
|
b.Navigation("StockItems");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("CutList.Web.Data.Entities.StockItem", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("SupplierOfferings");
|
||||||
|
|
||||||
|
b.Navigation("Transactions");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("CutList.Web.Data.Entities.Supplier", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Offerings");
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
74
CutList.Web/Migrations/20260204222017_AddJobNumber.cs
Normal file
74
CutList.Web/Migrations/20260204222017_AddJobNumber.cs
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace CutList.Web.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddJobNumber : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AlterColumn<string>(
|
||||||
|
name: "Name",
|
||||||
|
table: "Jobs",
|
||||||
|
type: "nvarchar(100)",
|
||||||
|
maxLength: 100,
|
||||||
|
nullable: true,
|
||||||
|
oldClrType: typeof(string),
|
||||||
|
oldType: "nvarchar(100)",
|
||||||
|
oldMaxLength: 100);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "JobNumber",
|
||||||
|
table: "Jobs",
|
||||||
|
type: "nvarchar(20)",
|
||||||
|
maxLength: 20,
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: "");
|
||||||
|
|
||||||
|
// Generate job numbers for existing jobs
|
||||||
|
migrationBuilder.Sql(@"
|
||||||
|
WITH NumberedJobs AS (
|
||||||
|
SELECT Id, ROW_NUMBER() OVER (ORDER BY Id) AS RowNum
|
||||||
|
FROM Jobs
|
||||||
|
)
|
||||||
|
UPDATE j
|
||||||
|
SET j.JobNumber = 'JOB-' + RIGHT('00000' + CAST(nj.RowNum AS VARCHAR(5)), 5)
|
||||||
|
FROM Jobs j
|
||||||
|
INNER JOIN NumberedJobs nj ON j.Id = nj.Id
|
||||||
|
");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_Jobs_JobNumber",
|
||||||
|
table: "Jobs",
|
||||||
|
column: "JobNumber",
|
||||||
|
unique: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropIndex(
|
||||||
|
name: "IX_Jobs_JobNumber",
|
||||||
|
table: "Jobs");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "JobNumber",
|
||||||
|
table: "Jobs");
|
||||||
|
|
||||||
|
migrationBuilder.AlterColumn<string>(
|
||||||
|
name: "Name",
|
||||||
|
table: "Jobs",
|
||||||
|
type: "nvarchar(100)",
|
||||||
|
maxLength: 100,
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: "",
|
||||||
|
oldClrType: typeof(string),
|
||||||
|
oldType: "nvarchar(100)",
|
||||||
|
oldMaxLength: 100,
|
||||||
|
oldNullable: true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
566
CutList.Web/Migrations/20260204223202_AddJobStock.Designer.cs
generated
Normal file
566
CutList.Web/Migrations/20260204223202_AddJobStock.Designer.cs
generated
Normal file
@@ -0,0 +1,566 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using CutList.Web.Data;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Metadata;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace CutList.Web.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(ApplicationDbContext))]
|
||||||
|
[Migration("20260204223202_AddJobStock")]
|
||||||
|
partial class AddJobStock
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder
|
||||||
|
.HasAnnotation("ProductVersion", "8.0.11")
|
||||||
|
.HasAnnotation("Relational:MaxIdentifierLength", 128);
|
||||||
|
|
||||||
|
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
|
||||||
|
|
||||||
|
modelBuilder.Entity("CutList.Web.Data.Entities.CuttingTool", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<bool>("IsActive")
|
||||||
|
.HasColumnType("bit");
|
||||||
|
|
||||||
|
b.Property<bool>("IsDefault")
|
||||||
|
.HasColumnType("bit");
|
||||||
|
|
||||||
|
b.Property<decimal>("KerfInches")
|
||||||
|
.HasPrecision(6, 4)
|
||||||
|
.HasColumnType("decimal(6,4)");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(50)
|
||||||
|
.HasColumnType("nvarchar(50)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("CuttingTools");
|
||||||
|
|
||||||
|
b.HasData(
|
||||||
|
new
|
||||||
|
{
|
||||||
|
Id = 1,
|
||||||
|
IsActive = true,
|
||||||
|
IsDefault = true,
|
||||||
|
KerfInches = 0.0625m,
|
||||||
|
Name = "Bandsaw"
|
||||||
|
},
|
||||||
|
new
|
||||||
|
{
|
||||||
|
Id = 2,
|
||||||
|
IsActive = true,
|
||||||
|
IsDefault = false,
|
||||||
|
KerfInches = 0.125m,
|
||||||
|
Name = "Chop Saw"
|
||||||
|
},
|
||||||
|
new
|
||||||
|
{
|
||||||
|
Id = 3,
|
||||||
|
IsActive = true,
|
||||||
|
IsDefault = false,
|
||||||
|
KerfInches = 0.0625m,
|
||||||
|
Name = "Cold Cut Saw"
|
||||||
|
},
|
||||||
|
new
|
||||||
|
{
|
||||||
|
Id = 4,
|
||||||
|
IsActive = true,
|
||||||
|
IsDefault = false,
|
||||||
|
KerfInches = 0.0625m,
|
||||||
|
Name = "Hacksaw"
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("CutList.Web.Data.Entities.Job", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("datetime2")
|
||||||
|
.HasDefaultValueSql("GETUTCDATE()");
|
||||||
|
|
||||||
|
b.Property<string>("Customer")
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("nvarchar(100)");
|
||||||
|
|
||||||
|
b.Property<int?>("CuttingToolId")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<string>("JobNumber")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(20)
|
||||||
|
.HasColumnType("nvarchar(20)");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("nvarchar(100)");
|
||||||
|
|
||||||
|
b.Property<string>("Notes")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("UpdatedAt")
|
||||||
|
.HasColumnType("datetime2");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("CuttingToolId");
|
||||||
|
|
||||||
|
b.HasIndex("JobNumber")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("Jobs");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("CutList.Web.Data.Entities.JobPart", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<int>("JobId")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<decimal>("LengthInches")
|
||||||
|
.HasPrecision(10, 4)
|
||||||
|
.HasColumnType("decimal(10,4)");
|
||||||
|
|
||||||
|
b.Property<int>("MaterialId")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("nvarchar(100)");
|
||||||
|
|
||||||
|
b.Property<int>("Quantity")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<int>("SortOrder")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("JobId");
|
||||||
|
|
||||||
|
b.HasIndex("MaterialId");
|
||||||
|
|
||||||
|
b.ToTable("JobParts");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("CutList.Web.Data.Entities.JobStock", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<bool>("IsCustomLength")
|
||||||
|
.HasColumnType("bit");
|
||||||
|
|
||||||
|
b.Property<int>("JobId")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<decimal>("LengthInches")
|
||||||
|
.HasPrecision(10, 4)
|
||||||
|
.HasColumnType("decimal(10,4)");
|
||||||
|
|
||||||
|
b.Property<int>("MaterialId")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<int>("Priority")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<int>("Quantity")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<int>("SortOrder")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<int?>("StockItemId")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("JobId");
|
||||||
|
|
||||||
|
b.HasIndex("MaterialId");
|
||||||
|
|
||||||
|
b.HasIndex("StockItemId");
|
||||||
|
|
||||||
|
b.ToTable("JobStocks");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("CutList.Web.Data.Entities.Material", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("datetime2")
|
||||||
|
.HasDefaultValueSql("GETUTCDATE()");
|
||||||
|
|
||||||
|
b.Property<string>("Description")
|
||||||
|
.HasMaxLength(255)
|
||||||
|
.HasColumnType("nvarchar(255)");
|
||||||
|
|
||||||
|
b.Property<bool>("IsActive")
|
||||||
|
.HasColumnType("bit");
|
||||||
|
|
||||||
|
b.Property<string>("Shape")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(50)
|
||||||
|
.HasColumnType("nvarchar(50)");
|
||||||
|
|
||||||
|
b.Property<string>("Size")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("nvarchar(100)");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("UpdatedAt")
|
||||||
|
.HasColumnType("datetime2");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("Materials");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("CutList.Web.Data.Entities.StockItem", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("datetime2")
|
||||||
|
.HasDefaultValueSql("GETUTCDATE()");
|
||||||
|
|
||||||
|
b.Property<bool>("IsActive")
|
||||||
|
.HasColumnType("bit");
|
||||||
|
|
||||||
|
b.Property<decimal>("LengthInches")
|
||||||
|
.HasPrecision(10, 4)
|
||||||
|
.HasColumnType("decimal(10,4)");
|
||||||
|
|
||||||
|
b.Property<int>("MaterialId")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("nvarchar(100)");
|
||||||
|
|
||||||
|
b.Property<string>("Notes")
|
||||||
|
.HasMaxLength(255)
|
||||||
|
.HasColumnType("nvarchar(255)");
|
||||||
|
|
||||||
|
b.Property<int>("QuantityOnHand")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("UpdatedAt")
|
||||||
|
.HasColumnType("datetime2");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("MaterialId", "LengthInches")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("StockItems");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("CutList.Web.Data.Entities.StockTransaction", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("datetime2")
|
||||||
|
.HasDefaultValueSql("GETUTCDATE()");
|
||||||
|
|
||||||
|
b.Property<int?>("JobId")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<string>("Notes")
|
||||||
|
.HasMaxLength(500)
|
||||||
|
.HasColumnType("nvarchar(500)");
|
||||||
|
|
||||||
|
b.Property<int>("Quantity")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<int>("StockItemId")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<int?>("SupplierId")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<int>("Type")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<decimal?>("UnitPrice")
|
||||||
|
.HasPrecision(10, 2)
|
||||||
|
.HasColumnType("decimal(10,2)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("JobId");
|
||||||
|
|
||||||
|
b.HasIndex("StockItemId");
|
||||||
|
|
||||||
|
b.HasIndex("SupplierId");
|
||||||
|
|
||||||
|
b.ToTable("StockTransactions");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("CutList.Web.Data.Entities.Supplier", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<string>("ContactInfo")
|
||||||
|
.HasMaxLength(500)
|
||||||
|
.HasColumnType("nvarchar(500)");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("datetime2")
|
||||||
|
.HasDefaultValueSql("GETUTCDATE()");
|
||||||
|
|
||||||
|
b.Property<bool>("IsActive")
|
||||||
|
.HasColumnType("bit");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("nvarchar(100)");
|
||||||
|
|
||||||
|
b.Property<string>("Notes")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("Suppliers");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("CutList.Web.Data.Entities.SupplierOffering", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<bool>("IsActive")
|
||||||
|
.HasColumnType("bit");
|
||||||
|
|
||||||
|
b.Property<string>("Notes")
|
||||||
|
.HasMaxLength(255)
|
||||||
|
.HasColumnType("nvarchar(255)");
|
||||||
|
|
||||||
|
b.Property<string>("PartNumber")
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("nvarchar(100)");
|
||||||
|
|
||||||
|
b.Property<decimal?>("Price")
|
||||||
|
.HasPrecision(10, 2)
|
||||||
|
.HasColumnType("decimal(10,2)");
|
||||||
|
|
||||||
|
b.Property<int>("StockItemId")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<string>("SupplierDescription")
|
||||||
|
.HasMaxLength(255)
|
||||||
|
.HasColumnType("nvarchar(255)");
|
||||||
|
|
||||||
|
b.Property<int>("SupplierId")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("StockItemId");
|
||||||
|
|
||||||
|
b.HasIndex("SupplierId", "StockItemId")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("SupplierOfferings");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("CutList.Web.Data.Entities.Job", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("CutList.Web.Data.Entities.CuttingTool", "CuttingTool")
|
||||||
|
.WithMany("Jobs")
|
||||||
|
.HasForeignKey("CuttingToolId")
|
||||||
|
.OnDelete(DeleteBehavior.SetNull);
|
||||||
|
|
||||||
|
b.Navigation("CuttingTool");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("CutList.Web.Data.Entities.JobPart", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("CutList.Web.Data.Entities.Job", "Job")
|
||||||
|
.WithMany("Parts")
|
||||||
|
.HasForeignKey("JobId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("CutList.Web.Data.Entities.Material", "Material")
|
||||||
|
.WithMany("JobParts")
|
||||||
|
.HasForeignKey("MaterialId")
|
||||||
|
.OnDelete(DeleteBehavior.Restrict)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Job");
|
||||||
|
|
||||||
|
b.Navigation("Material");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("CutList.Web.Data.Entities.JobStock", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("CutList.Web.Data.Entities.Job", "Job")
|
||||||
|
.WithMany("Stock")
|
||||||
|
.HasForeignKey("JobId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("CutList.Web.Data.Entities.Material", "Material")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("MaterialId")
|
||||||
|
.OnDelete(DeleteBehavior.Restrict)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("CutList.Web.Data.Entities.StockItem", "StockItem")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("StockItemId")
|
||||||
|
.OnDelete(DeleteBehavior.SetNull);
|
||||||
|
|
||||||
|
b.Navigation("Job");
|
||||||
|
|
||||||
|
b.Navigation("Material");
|
||||||
|
|
||||||
|
b.Navigation("StockItem");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("CutList.Web.Data.Entities.StockItem", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("CutList.Web.Data.Entities.Material", "Material")
|
||||||
|
.WithMany("StockItems")
|
||||||
|
.HasForeignKey("MaterialId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Material");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("CutList.Web.Data.Entities.StockTransaction", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("CutList.Web.Data.Entities.Job", "Job")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("JobId")
|
||||||
|
.OnDelete(DeleteBehavior.SetNull);
|
||||||
|
|
||||||
|
b.HasOne("CutList.Web.Data.Entities.StockItem", "StockItem")
|
||||||
|
.WithMany("Transactions")
|
||||||
|
.HasForeignKey("StockItemId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("CutList.Web.Data.Entities.Supplier", "Supplier")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("SupplierId")
|
||||||
|
.OnDelete(DeleteBehavior.SetNull);
|
||||||
|
|
||||||
|
b.Navigation("Job");
|
||||||
|
|
||||||
|
b.Navigation("StockItem");
|
||||||
|
|
||||||
|
b.Navigation("Supplier");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("CutList.Web.Data.Entities.SupplierOffering", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("CutList.Web.Data.Entities.StockItem", "StockItem")
|
||||||
|
.WithMany("SupplierOfferings")
|
||||||
|
.HasForeignKey("StockItemId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("CutList.Web.Data.Entities.Supplier", "Supplier")
|
||||||
|
.WithMany("Offerings")
|
||||||
|
.HasForeignKey("SupplierId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("StockItem");
|
||||||
|
|
||||||
|
b.Navigation("Supplier");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("CutList.Web.Data.Entities.CuttingTool", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Jobs");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("CutList.Web.Data.Entities.Job", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Parts");
|
||||||
|
|
||||||
|
b.Navigation("Stock");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("CutList.Web.Data.Entities.Material", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("JobParts");
|
||||||
|
|
||||||
|
b.Navigation("StockItems");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("CutList.Web.Data.Entities.StockItem", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("SupplierOfferings");
|
||||||
|
|
||||||
|
b.Navigation("Transactions");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("CutList.Web.Data.Entities.Supplier", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Offerings");
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
74
CutList.Web/Migrations/20260204223202_AddJobStock.cs
Normal file
74
CutList.Web/Migrations/20260204223202_AddJobStock.cs
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace CutList.Web.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddJobStock : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "JobStocks",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<int>(type: "int", nullable: false)
|
||||||
|
.Annotation("SqlServer:Identity", "1, 1"),
|
||||||
|
JobId = table.Column<int>(type: "int", nullable: false),
|
||||||
|
MaterialId = table.Column<int>(type: "int", nullable: false),
|
||||||
|
StockItemId = table.Column<int>(type: "int", nullable: true),
|
||||||
|
LengthInches = table.Column<decimal>(type: "decimal(10,4)", precision: 10, scale: 4, nullable: false),
|
||||||
|
Quantity = table.Column<int>(type: "int", nullable: false),
|
||||||
|
IsCustomLength = table.Column<bool>(type: "bit", nullable: false),
|
||||||
|
Priority = table.Column<int>(type: "int", nullable: false),
|
||||||
|
SortOrder = table.Column<int>(type: "int", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_JobStocks", x => x.Id);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_JobStocks_Jobs_JobId",
|
||||||
|
column: x => x.JobId,
|
||||||
|
principalTable: "Jobs",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_JobStocks_Materials_MaterialId",
|
||||||
|
column: x => x.MaterialId,
|
||||||
|
principalTable: "Materials",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Restrict);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_JobStocks_StockItems_StockItemId",
|
||||||
|
column: x => x.StockItemId,
|
||||||
|
principalTable: "StockItems",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.SetNull);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_JobStocks_JobId",
|
||||||
|
table: "JobStocks",
|
||||||
|
column: "JobId");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_JobStocks_MaterialId",
|
||||||
|
table: "JobStocks",
|
||||||
|
column: "MaterialId");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_JobStocks_StockItemId",
|
||||||
|
table: "JobStocks",
|
||||||
|
column: "StockItemId");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "JobStocks");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
320
CutList.Web/Services/JobService.cs
Normal file
320
CutList.Web/Services/JobService.cs
Normal file
@@ -0,0 +1,320 @@
|
|||||||
|
using CutList.Web.Data;
|
||||||
|
using CutList.Web.Data.Entities;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace CutList.Web.Services;
|
||||||
|
|
||||||
|
public class JobService
|
||||||
|
{
|
||||||
|
private readonly ApplicationDbContext _context;
|
||||||
|
|
||||||
|
public JobService(ApplicationDbContext context)
|
||||||
|
{
|
||||||
|
_context = context;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<List<Job>> GetAllAsync()
|
||||||
|
{
|
||||||
|
return await _context.Jobs
|
||||||
|
.Include(p => p.CuttingTool)
|
||||||
|
.Include(p => p.Parts)
|
||||||
|
.ThenInclude(pt => pt.Material)
|
||||||
|
.OrderByDescending(p => p.UpdatedAt ?? p.CreatedAt)
|
||||||
|
.ToListAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Job?> GetByIdAsync(int id)
|
||||||
|
{
|
||||||
|
return await _context.Jobs
|
||||||
|
.Include(p => p.CuttingTool)
|
||||||
|
.Include(p => p.Parts.OrderBy(pt => pt.SortOrder))
|
||||||
|
.ThenInclude(pt => pt.Material)
|
||||||
|
.Include(p => p.Stock.OrderBy(s => s.SortOrder))
|
||||||
|
.ThenInclude(s => s.Material)
|
||||||
|
.Include(p => p.Stock)
|
||||||
|
.ThenInclude(s => s.StockItem)
|
||||||
|
.FirstOrDefaultAsync(p => p.Id == id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Job> CreateAsync(Job? job = null)
|
||||||
|
{
|
||||||
|
job ??= new Job();
|
||||||
|
job.JobNumber = await GenerateJobNumberAsync();
|
||||||
|
job.CreatedAt = DateTime.UtcNow;
|
||||||
|
_context.Jobs.Add(job);
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
return job;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<string> GenerateJobNumberAsync()
|
||||||
|
{
|
||||||
|
var maxNumber = await _context.Jobs
|
||||||
|
.Where(j => j.JobNumber.StartsWith("JOB-"))
|
||||||
|
.Select(j => j.JobNumber)
|
||||||
|
.MaxAsync() as string;
|
||||||
|
|
||||||
|
if (maxNumber == null)
|
||||||
|
return "JOB-00001";
|
||||||
|
|
||||||
|
var numPart = maxNumber.Substring(4);
|
||||||
|
if (int.TryParse(numPart, out var num))
|
||||||
|
return $"JOB-{num + 1:D5}";
|
||||||
|
|
||||||
|
return $"JOB-{DateTime.UtcNow:yyyyMMddHHmmss}";
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Job> QuickCreateAsync(string? customer = null)
|
||||||
|
{
|
||||||
|
var job = new Job { Customer = customer };
|
||||||
|
return await CreateAsync(job);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task UpdateAsync(Job job)
|
||||||
|
{
|
||||||
|
job.UpdatedAt = DateTime.UtcNow;
|
||||||
|
_context.Jobs.Update(job);
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task DeleteAsync(int id)
|
||||||
|
{
|
||||||
|
var job = await _context.Jobs.FindAsync(id);
|
||||||
|
if (job != null)
|
||||||
|
{
|
||||||
|
_context.Jobs.Remove(job);
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Job> DuplicateAsync(int id)
|
||||||
|
{
|
||||||
|
var original = await GetByIdAsync(id);
|
||||||
|
if (original == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentException("Job not found", nameof(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
var duplicate = new Job
|
||||||
|
{
|
||||||
|
JobNumber = await GenerateJobNumberAsync(),
|
||||||
|
Name = string.IsNullOrWhiteSpace(original.Name) ? null : $"{original.Name} (Copy)",
|
||||||
|
Customer = original.Customer,
|
||||||
|
CuttingToolId = original.CuttingToolId,
|
||||||
|
Notes = original.Notes,
|
||||||
|
CreatedAt = DateTime.UtcNow
|
||||||
|
};
|
||||||
|
|
||||||
|
_context.Jobs.Add(duplicate);
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
|
||||||
|
// Copy parts
|
||||||
|
foreach (var part in original.Parts)
|
||||||
|
{
|
||||||
|
_context.JobParts.Add(new JobPart
|
||||||
|
{
|
||||||
|
JobId = duplicate.Id,
|
||||||
|
MaterialId = part.MaterialId,
|
||||||
|
Name = part.Name,
|
||||||
|
LengthInches = part.LengthInches,
|
||||||
|
Quantity = part.Quantity,
|
||||||
|
SortOrder = part.SortOrder
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy stock selections
|
||||||
|
foreach (var stock in original.Stock)
|
||||||
|
{
|
||||||
|
_context.JobStocks.Add(new JobStock
|
||||||
|
{
|
||||||
|
JobId = duplicate.Id,
|
||||||
|
MaterialId = stock.MaterialId,
|
||||||
|
StockItemId = stock.StockItemId,
|
||||||
|
LengthInches = stock.LengthInches,
|
||||||
|
Quantity = stock.Quantity,
|
||||||
|
IsCustomLength = stock.IsCustomLength,
|
||||||
|
Priority = stock.Priority,
|
||||||
|
SortOrder = stock.SortOrder
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
return duplicate;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parts management
|
||||||
|
public async Task<JobPart> AddPartAsync(JobPart part)
|
||||||
|
{
|
||||||
|
var maxOrder = await _context.JobParts
|
||||||
|
.Where(p => p.JobId == part.JobId)
|
||||||
|
.MaxAsync(p => (int?)p.SortOrder) ?? -1;
|
||||||
|
part.SortOrder = maxOrder + 1;
|
||||||
|
|
||||||
|
_context.JobParts.Add(part);
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
|
||||||
|
// Update job timestamp
|
||||||
|
var job = await _context.Jobs.FindAsync(part.JobId);
|
||||||
|
if (job != null)
|
||||||
|
{
|
||||||
|
job.UpdatedAt = DateTime.UtcNow;
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
return part;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task UpdatePartAsync(JobPart part)
|
||||||
|
{
|
||||||
|
_context.JobParts.Update(part);
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
|
||||||
|
var job = await _context.Jobs.FindAsync(part.JobId);
|
||||||
|
if (job != null)
|
||||||
|
{
|
||||||
|
job.UpdatedAt = DateTime.UtcNow;
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task DeletePartAsync(int id)
|
||||||
|
{
|
||||||
|
var part = await _context.JobParts.FindAsync(id);
|
||||||
|
if (part != null)
|
||||||
|
{
|
||||||
|
var jobId = part.JobId;
|
||||||
|
_context.JobParts.Remove(part);
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
|
||||||
|
var job = await _context.Jobs.FindAsync(jobId);
|
||||||
|
if (job != null)
|
||||||
|
{
|
||||||
|
job.UpdatedAt = DateTime.UtcNow;
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stock management
|
||||||
|
public async Task<JobStock> AddStockAsync(JobStock stock)
|
||||||
|
{
|
||||||
|
var maxOrder = await _context.JobStocks
|
||||||
|
.Where(s => s.JobId == stock.JobId)
|
||||||
|
.MaxAsync(s => (int?)s.SortOrder) ?? -1;
|
||||||
|
stock.SortOrder = maxOrder + 1;
|
||||||
|
|
||||||
|
_context.JobStocks.Add(stock);
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
|
||||||
|
var job = await _context.Jobs.FindAsync(stock.JobId);
|
||||||
|
if (job != null)
|
||||||
|
{
|
||||||
|
job.UpdatedAt = DateTime.UtcNow;
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
return stock;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task UpdateStockAsync(JobStock stock)
|
||||||
|
{
|
||||||
|
_context.JobStocks.Update(stock);
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
|
||||||
|
var job = await _context.Jobs.FindAsync(stock.JobId);
|
||||||
|
if (job != null)
|
||||||
|
{
|
||||||
|
job.UpdatedAt = DateTime.UtcNow;
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task DeleteStockAsync(int id)
|
||||||
|
{
|
||||||
|
var stock = await _context.JobStocks.FindAsync(id);
|
||||||
|
if (stock != null)
|
||||||
|
{
|
||||||
|
var jobId = stock.JobId;
|
||||||
|
_context.JobStocks.Remove(stock);
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
|
||||||
|
var job = await _context.Jobs.FindAsync(jobId);
|
||||||
|
if (job != null)
|
||||||
|
{
|
||||||
|
job.UpdatedAt = DateTime.UtcNow;
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<List<StockItem>> GetAvailableStockForMaterialAsync(int materialId)
|
||||||
|
{
|
||||||
|
return await _context.StockItems
|
||||||
|
.Include(s => s.Material)
|
||||||
|
.Where(s => s.MaterialId == materialId && s.IsActive)
|
||||||
|
.OrderBy(s => s.LengthInches)
|
||||||
|
.ToListAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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