Compare commits
3 Commits
02e936febb
...
dac2833dd1
| Author | SHA1 | Date | |
|---|---|---|---|
| dac2833dd1 | |||
| a226a1f652 | |||
| 5000021193 |
@@ -118,14 +118,15 @@ else
|
|||||||
<div class="modal-dialog modal-lg">
|
<div class="modal-dialog modal-lg">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h5 class="modal-title">@(editingPart == null ? "Add Part" : "Edit Part")</h5>
|
<h5 class="modal-title">@(editingPart == null ? "Add Parts" : "Edit Part")</h5>
|
||||||
<button type="button" class="btn-close" @onclick="CancelPartForm"></button>
|
<button type="button" class="btn-close" @onclick="CancelPartForm"></button>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<div class="row g-3">
|
<div class="row g-3 mb-3">
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<label class="form-label">Shape</label>
|
<label class="form-label">Shape</label>
|
||||||
<select class="form-select" @bind="selectedShape" @bind:after="OnShapeChanged">
|
<select class="form-select" @bind="selectedShape" @bind:after="OnShapeChanged"
|
||||||
|
disabled="@(editingPart != null)">
|
||||||
<option value="">-- Select --</option>
|
<option value="">-- Select --</option>
|
||||||
@foreach (var shape in DistinctShapes)
|
@foreach (var shape in DistinctShapes)
|
||||||
{
|
{
|
||||||
@@ -135,7 +136,7 @@ else
|
|||||||
</div>
|
</div>
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<label class="form-label">Size</label>
|
<label class="form-label">Size</label>
|
||||||
<select class="form-select" @bind="newPart.MaterialId" disabled="@(!selectedShape.HasValue)">
|
<select class="form-select" @bind="partSelectedMaterialId" disabled="@(!selectedShape.HasValue || editingPart != null)">
|
||||||
<option value="0">-- Select --</option>
|
<option value="0">-- Select --</option>
|
||||||
@foreach (var material in FilteredMaterials)
|
@foreach (var material in FilteredMaterials)
|
||||||
{
|
{
|
||||||
@@ -143,19 +144,63 @@ else
|
|||||||
}
|
}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-6">
|
</div>
|
||||||
|
|
||||||
|
@if (editingPart != null)
|
||||||
|
{
|
||||||
|
@* Edit mode: single row *@
|
||||||
|
<div class="row g-3">
|
||||||
|
<div class="col-md-4">
|
||||||
<label class="form-label">Length</label>
|
<label class="form-label">Length</label>
|
||||||
<LengthInput @bind-Value="newPart.LengthInches" />
|
<LengthInput @bind-Value="newPart.LengthInches" />
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-6">
|
<div class="col-md-4">
|
||||||
<label class="form-label">Quantity</label>
|
<label class="form-label">Quantity</label>
|
||||||
<input type="number" class="form-control" @bind="newPart.Quantity" min="1" />
|
<input type="number" class="form-control" @bind="newPart.Quantity" min="1" />
|
||||||
</div>
|
</div>
|
||||||
<div class="col-12">
|
<div class="col-md-4">
|
||||||
<label class="form-label">Name <span class="text-muted fw-normal">(optional)</span></label>
|
<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" />
|
<input type="text" class="form-control" @bind="newPart.Name" placeholder="Part name" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
@* Add mode: multi-row table *@
|
||||||
|
<table class="table table-sm align-middle mb-2">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Length</th>
|
||||||
|
<th style="width: 100px;">Qty</th>
|
||||||
|
<th>Name <span class="text-muted fw-normal">(optional)</span></th>
|
||||||
|
<th style="width: 50px;"></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
@for (var i = 0; i < partRows.Count; i++)
|
||||||
|
{
|
||||||
|
var row = partRows[i];
|
||||||
|
<tr>
|
||||||
|
<td><LengthInput @bind-Value="row.LengthInches" /></td>
|
||||||
|
<td><input type="number" class="form-control form-control-sm" @bind="row.Quantity" @bind:event="oninput" min="1" /></td>
|
||||||
|
<td><input type="text" class="form-control form-control-sm" @bind="row.Name" @bind:event="oninput" placeholder="Part name" /></td>
|
||||||
|
<td>
|
||||||
|
@if (partRows.Count > 1)
|
||||||
|
{
|
||||||
|
<button type="button" class="btn btn-sm btn-outline-danger" @onclick="() => RemovePartRow(row)" title="Remove">
|
||||||
|
<i class="bi bi-x-lg"></i>
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<button type="button" class="btn btn-sm btn-outline-secondary" @onclick="AddPartRow">
|
||||||
|
<i class="bi bi-plus-lg me-1"></i>Add Row
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
|
||||||
@if (!string.IsNullOrEmpty(partErrorMessage))
|
@if (!string.IsNullOrEmpty(partErrorMessage))
|
||||||
{
|
{
|
||||||
<div class="alert alert-danger mt-3 mb-0">@partErrorMessage</div>
|
<div class="alert alert-danger mt-3 mb-0">@partErrorMessage</div>
|
||||||
@@ -164,7 +209,14 @@ else
|
|||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<button type="button" class="btn btn-outline-secondary" @onclick="CancelPartForm">Cancel</button>
|
<button type="button" class="btn btn-outline-secondary" @onclick="CancelPartForm">Cancel</button>
|
||||||
<button type="button" class="btn btn-primary" @onclick="SavePartAsync">
|
<button type="button" class="btn btn-primary" @onclick="SavePartAsync">
|
||||||
@(editingPart == null ? "Add Part" : "Save Changes")
|
@if (editingPart != null)
|
||||||
|
{
|
||||||
|
<text>Save Changes</text>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<text>Add @partRows.Count Part@(partRows.Count != 1 ? "s" : "")</text>
|
||||||
|
}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -291,6 +343,8 @@ else
|
|||||||
private JobPart? editingPart;
|
private JobPart? editingPart;
|
||||||
private string? partErrorMessage;
|
private string? partErrorMessage;
|
||||||
private MaterialShape? selectedShape;
|
private MaterialShape? selectedShape;
|
||||||
|
private int partSelectedMaterialId;
|
||||||
|
private List<PartRow> partRows = new();
|
||||||
|
|
||||||
// Stock form
|
// Stock form
|
||||||
private bool showStockForm;
|
private bool showStockForm;
|
||||||
@@ -541,15 +595,28 @@ else
|
|||||||
editingPart = null;
|
editingPart = null;
|
||||||
newPart = new JobPart { JobId = Id!.Value, Quantity = 1 };
|
newPart = new JobPart { JobId = Id!.Value, Quantity = 1 };
|
||||||
selectedShape = null;
|
selectedShape = null;
|
||||||
|
partSelectedMaterialId = 0;
|
||||||
|
partRows = new List<PartRow> { new PartRow() };
|
||||||
showPartForm = true;
|
showPartForm = true;
|
||||||
partErrorMessage = null;
|
partErrorMessage = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnShapeChanged()
|
private void OnShapeChanged()
|
||||||
{
|
{
|
||||||
|
partSelectedMaterialId = 0;
|
||||||
newPart.MaterialId = 0;
|
newPart.MaterialId = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void AddPartRow()
|
||||||
|
{
|
||||||
|
partRows.Add(new PartRow());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RemovePartRow(PartRow row)
|
||||||
|
{
|
||||||
|
partRows.Remove(row);
|
||||||
|
}
|
||||||
|
|
||||||
private void EditPart(JobPart part)
|
private void EditPart(JobPart part)
|
||||||
{
|
{
|
||||||
editingPart = part;
|
editingPart = part;
|
||||||
@@ -564,6 +631,8 @@ else
|
|||||||
SortOrder = part.SortOrder
|
SortOrder = part.SortOrder
|
||||||
};
|
};
|
||||||
selectedShape = part.Material?.Shape;
|
selectedShape = part.Material?.Shape;
|
||||||
|
partSelectedMaterialId = part.MaterialId;
|
||||||
|
partRows.Clear();
|
||||||
showPartForm = true;
|
showPartForm = true;
|
||||||
partErrorMessage = null;
|
partErrorMessage = null;
|
||||||
}
|
}
|
||||||
@@ -584,31 +653,59 @@ else
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (newPart.MaterialId == 0)
|
if (partSelectedMaterialId == 0)
|
||||||
{
|
{
|
||||||
partErrorMessage = "Please select a size";
|
partErrorMessage = "Please select a size";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (editingPart != null)
|
||||||
|
{
|
||||||
|
// Edit mode: single part
|
||||||
if (newPart.LengthInches <= 0)
|
if (newPart.LengthInches <= 0)
|
||||||
{
|
{
|
||||||
partErrorMessage = "Length must be greater than zero";
|
partErrorMessage = "Length must be greater than zero";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (newPart.Quantity < 1)
|
if (newPart.Quantity < 1)
|
||||||
{
|
{
|
||||||
partErrorMessage = "Quantity must be at least 1";
|
partErrorMessage = "Quantity must be at least 1";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (editingPart == null)
|
newPart.MaterialId = partSelectedMaterialId;
|
||||||
{
|
await JobService.UpdatePartAsync(newPart);
|
||||||
await JobService.AddPartAsync(newPart);
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
await JobService.UpdatePartAsync(newPart);
|
// Add mode: multiple rows
|
||||||
|
for (int i = 0; i < partRows.Count; i++)
|
||||||
|
{
|
||||||
|
var row = partRows[i];
|
||||||
|
if (row.LengthInches <= 0)
|
||||||
|
{
|
||||||
|
partErrorMessage = $"Row {i + 1}: Length must be greater than zero";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (row.Quantity < 1)
|
||||||
|
{
|
||||||
|
partErrorMessage = $"Row {i + 1}: Quantity must be at least 1";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var row in partRows)
|
||||||
|
{
|
||||||
|
var part = new JobPart
|
||||||
|
{
|
||||||
|
JobId = Id!.Value,
|
||||||
|
MaterialId = partSelectedMaterialId,
|
||||||
|
LengthInches = row.LengthInches,
|
||||||
|
Quantity = row.Quantity,
|
||||||
|
Name = row.Name ?? string.Empty
|
||||||
|
};
|
||||||
|
await JobService.AddPartAsync(part);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
job = (await JobService.GetByIdAsync(Id!.Value))!;
|
job = (await JobService.GetByIdAsync(Id!.Value))!;
|
||||||
@@ -931,113 +1028,158 @@ else
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Stock Summary -->
|
<!-- Purchase List -->
|
||||||
<div class="row mb-4 print-stock-summary">
|
<div class="card mb-4 print-purchase-list">
|
||||||
<div class="col-md-6 mb-3">
|
<div class="card-header d-flex justify-content-between align-items-center">
|
||||||
<div class="card border-success">
|
<h5 class="mb-0"><i class="bi bi-cart me-2"></i>Purchase List</h5>
|
||||||
<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>
|
|
||||||
@if (summary.TotalToBePurchasedBins > 0)
|
@if (summary.TotalToBePurchasedBins > 0)
|
||||||
{
|
{
|
||||||
@if (addedToOrderList)
|
@if (addedToOrderList)
|
||||||
{
|
{
|
||||||
<div class="alert alert-success mb-0 mt-2 py-2">
|
<span class="badge bg-success"><i class="bi bi-check-lg me-1"></i>Added to orders</span>
|
||||||
Added to order list. <a href="orders">View Orders</a>
|
|
||||||
</div>
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
<button class="btn btn-warning btn-sm mt-2" @onclick="AddToOrderList" disabled="@addingToOrderList">
|
<button class="btn btn-warning btn-sm" @onclick="AddToOrderList" disabled="@addingToOrderList">
|
||||||
@if (addingToOrderList)
|
@if (addingToOrderList)
|
||||||
{
|
{
|
||||||
<span class="spinner-border spinner-border-sm me-1"></span>
|
<span class="spinner-border spinner-border-sm me-1"></span>
|
||||||
}
|
}
|
||||||
<i class="bi bi-cart-plus"></i> Add to Order List
|
<i class="bi bi-cart-plus me-1"></i>Add to Order List
|
||||||
</button>
|
</button>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
@if (summary.TotalToBePurchasedBins == 0)
|
||||||
|
{
|
||||||
|
<p class="text-muted mb-0">Everything is available in stock. No purchases needed.</p>
|
||||||
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
<p class="text-muted mb-0">Everything available in stock</p>
|
@if (addedToOrderList)
|
||||||
|
{
|
||||||
|
<div class="alert alert-success py-2 mb-3">
|
||||||
|
Items added to order list. <a href="orders">View Orders</a>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-sm mb-0">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Material</th>
|
||||||
|
<th>Length</th>
|
||||||
|
<th class="text-end">Qty</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
@foreach (var materialResult in packResult.MaterialResults.Where(mr => mr.ToBePurchasedBins.Count > 0))
|
||||||
|
{
|
||||||
|
@foreach (var group in materialResult.ToBePurchasedBins.GroupBy(b => b.Length).OrderByDescending(g => g.Key))
|
||||||
|
{
|
||||||
|
<tr>
|
||||||
|
<td>@materialResult.Material.DisplayName</td>
|
||||||
|
<td>@ArchUnits.FormatFromInches(group.Key)</td>
|
||||||
|
<td class="text-end">@group.Count()</td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</tbody>
|
||||||
|
<tfoot>
|
||||||
|
<tr class="fw-bold">
|
||||||
|
<td colspan="2">Total</td>
|
||||||
|
<td class="text-end">@summary.TotalToBePurchasedBins bars</td>
|
||||||
|
</tr>
|
||||||
|
</tfoot>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Results by Material -->
|
<!-- Cut Lists by Material -->
|
||||||
@foreach (var materialResult in packResult.MaterialResults)
|
@foreach (var materialResult in packResult.MaterialResults)
|
||||||
{
|
{
|
||||||
var materialSummary = summary.MaterialSummaries.First(s => s.Material.Id == materialResult.Material.Id);
|
var materialSummary = summary.MaterialSummaries.First(s => s.Material.Id == materialResult.Material.Id);
|
||||||
|
var allBins = materialResult.InStockBins
|
||||||
|
.Select(b => new { Bin = b, Source = "Stock" })
|
||||||
|
.Concat(materialResult.ToBePurchasedBins
|
||||||
|
.Select(b => new { Bin = b, Source = "Purchase" }))
|
||||||
|
.ToList();
|
||||||
|
|
||||||
<div class="card mb-4">
|
<div class="card mb-4 cutlist-material-card">
|
||||||
<div class="card-header">
|
<div class="card-header cutlist-material-screen-header">
|
||||||
<h4 class="mb-0">@materialResult.Material.DisplayName</h4>
|
<div class="d-flex justify-content-between align-items-center">
|
||||||
|
<h5 class="mb-0">@materialResult.Material.DisplayName</h5>
|
||||||
|
<span class="text-muted">
|
||||||
|
@(materialSummary.InStockBins + materialSummary.ToBePurchasedBins) bars
|
||||||
|
· @materialSummary.TotalPieces pieces
|
||||||
|
· @materialSummary.Efficiency.ToString("F1")% efficiency
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<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)
|
@if (materialResult.PackResult.ItemsNotUsed.Count > 0)
|
||||||
{
|
{
|
||||||
<div class="alert alert-danger">
|
<div class="alert alert-danger">
|
||||||
<strong>@materialResult.PackResult.ItemsNotUsed.Count items not placed</strong> -
|
<strong>@materialResult.PackResult.ItemsNotUsed.Count items not placed</strong> —
|
||||||
No stock lengths available or parts too long.
|
No stock lengths available or parts too long.
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
@if (materialResult.InStockBins.Count > 0)
|
<div class="table-responsive">
|
||||||
|
<table class="table table-sm table-striped mb-0">
|
||||||
|
<thead>
|
||||||
|
<tr class="cutlist-material-print-header">
|
||||||
|
<th colspan="5">
|
||||||
|
<span class="cutlist-material-name">@materialResult.Material.DisplayName</span>
|
||||||
|
<span class="cutlist-material-stats">
|
||||||
|
@(materialSummary.InStockBins + materialSummary.ToBePurchasedBins) bars
|
||||||
|
· @materialSummary.TotalPieces pieces
|
||||||
|
· @materialSummary.Efficiency.ToString("F1")% efficiency
|
||||||
|
</span>
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th style="width: 50px;">#</th>
|
||||||
|
<th style="width: 90px;">Source</th>
|
||||||
|
<th style="white-space: nowrap;">Stock Length</th>
|
||||||
|
<th>Cuts</th>
|
||||||
|
<th style="width: 120px; white-space: nowrap;">Waste</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
@{ var binNum = 1; }
|
||||||
|
@foreach (var entry in allBins)
|
||||||
{
|
{
|
||||||
<h5 class="text-success mt-3">In Stock (@materialResult.InStockBins.Count bars)</h5>
|
<tr>
|
||||||
@RenderBinList(materialResult.InStockBins)
|
<td>@binNum</td>
|
||||||
|
<td>
|
||||||
|
@if (entry.Source == "Stock")
|
||||||
|
{
|
||||||
|
<span class="badge bg-success">Stock</span>
|
||||||
}
|
}
|
||||||
|
else
|
||||||
@if (materialResult.ToBePurchasedBins.Count > 0)
|
|
||||||
{
|
{
|
||||||
<h5 class="text-warning mt-3">To Be Purchased (@materialResult.ToBePurchasedBins.Count bars)</h5>
|
<span class="badge bg-warning text-dark">Purchase</span>
|
||||||
@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>
|
</td>
|
||||||
|
<td style="white-space: nowrap;">@ArchUnits.FormatFromInches(entry.Bin.Length)</td>
|
||||||
|
<td>
|
||||||
|
@foreach (var item in entry.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 style="white-space: nowrap;">@ArchUnits.FormatFromInches(entry.Bin.RemainingLength)</td>
|
||||||
|
</tr>
|
||||||
|
binNum++;
|
||||||
|
}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
</div>
|
</div>
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@@ -1052,41 +1194,6 @@ else
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
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 RunOptimization()
|
private async Task RunOptimization()
|
||||||
{
|
{
|
||||||
@@ -1443,6 +1550,13 @@ else
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private class PartRow
|
||||||
|
{
|
||||||
|
public decimal LengthInches { get; set; }
|
||||||
|
public int Quantity { get; set; } = 1;
|
||||||
|
public string? Name { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
private class ImportStockCandidate
|
private class ImportStockCandidate
|
||||||
{
|
{
|
||||||
public StockItem StockItem { get; set; } = null!;
|
public StockItem StockItem { get; set; } = null!;
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using CutList.Web.DTOs;
|
||||||
|
using CutList.Web.Services;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
namespace CutList.Web.Controllers;
|
||||||
|
|
||||||
|
[ApiController]
|
||||||
|
[Route("api/[controller]")]
|
||||||
|
public class CatalogController : ControllerBase
|
||||||
|
{
|
||||||
|
private readonly CatalogService _catalogService;
|
||||||
|
|
||||||
|
public CatalogController(CatalogService catalogService)
|
||||||
|
{
|
||||||
|
_catalogService = catalogService;
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("export")]
|
||||||
|
public async Task<IActionResult> Export()
|
||||||
|
{
|
||||||
|
var data = await _catalogService.ExportAsync();
|
||||||
|
|
||||||
|
var options = new JsonSerializerOptions
|
||||||
|
{
|
||||||
|
WriteIndented = true,
|
||||||
|
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
|
||||||
|
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
|
||||||
|
};
|
||||||
|
|
||||||
|
return new JsonResult(data, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("import")]
|
||||||
|
public async Task<ActionResult<ImportResultDto>> Import([FromBody] CatalogData data)
|
||||||
|
{
|
||||||
|
var result = await _catalogService.ImportAsync(data);
|
||||||
|
return Ok(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,94 +0,0 @@
|
|||||||
using CutList.Web.Data;
|
|
||||||
using CutList.Web.Data.Entities;
|
|
||||||
using Microsoft.AspNetCore.Mvc;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
|
|
||||||
namespace CutList.Web.Controllers;
|
|
||||||
|
|
||||||
[ApiController]
|
|
||||||
[Route("api/[controller]")]
|
|
||||||
public class SeedController : ControllerBase
|
|
||||||
{
|
|
||||||
private readonly ApplicationDbContext _context;
|
|
||||||
|
|
||||||
public SeedController(ApplicationDbContext context)
|
|
||||||
{
|
|
||||||
_context = context;
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpPost("alro-1018-round")]
|
|
||||||
public async Task<ActionResult> SeedAlro1018Round()
|
|
||||||
{
|
|
||||||
// Add Alro supplier if not exists
|
|
||||||
var alro = await _context.Suppliers.FirstOrDefaultAsync(s => s.Name == "Alro");
|
|
||||||
if (alro == null)
|
|
||||||
{
|
|
||||||
alro = new Supplier
|
|
||||||
{
|
|
||||||
Name = "Alro",
|
|
||||||
ContactInfo = "https://www.alro.com",
|
|
||||||
CreatedAt = DateTime.UtcNow
|
|
||||||
};
|
|
||||||
_context.Suppliers.Add(alro);
|
|
||||||
await _context.SaveChangesAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 1018 CF Round bar sizes from the screenshot
|
|
||||||
var sizes = new[]
|
|
||||||
{
|
|
||||||
"1/8\"",
|
|
||||||
"5/32\"",
|
|
||||||
"3/16\"",
|
|
||||||
"7/32\"",
|
|
||||||
".236\"",
|
|
||||||
"1/4\"",
|
|
||||||
"9/32\"",
|
|
||||||
"5/16\"",
|
|
||||||
"11/32\"",
|
|
||||||
"3/8\"",
|
|
||||||
".394\"",
|
|
||||||
"13/32\"",
|
|
||||||
"7/16\"",
|
|
||||||
"15/32\"",
|
|
||||||
".472\"",
|
|
||||||
"1/2\"",
|
|
||||||
"17/32\"",
|
|
||||||
"9/16\"",
|
|
||||||
".593\""
|
|
||||||
};
|
|
||||||
|
|
||||||
var created = 0;
|
|
||||||
var skipped = 0;
|
|
||||||
|
|
||||||
foreach (var size in sizes)
|
|
||||||
{
|
|
||||||
var exists = await _context.Materials
|
|
||||||
.AnyAsync(m => m.Shape == MaterialShape.RoundBar && m.Size == size && m.IsActive);
|
|
||||||
|
|
||||||
if (exists)
|
|
||||||
{
|
|
||||||
skipped++;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
_context.Materials.Add(new Material
|
|
||||||
{
|
|
||||||
Shape = MaterialShape.RoundBar,
|
|
||||||
Size = size,
|
|
||||||
Description = "1018 Cold Finished",
|
|
||||||
CreatedAt = DateTime.UtcNow
|
|
||||||
});
|
|
||||||
created++;
|
|
||||||
}
|
|
||||||
|
|
||||||
await _context.SaveChangesAsync();
|
|
||||||
|
|
||||||
return Ok(new
|
|
||||||
{
|
|
||||||
Message = "Alro 1018 CF Round materials seeded",
|
|
||||||
SupplierId = alro.Id,
|
|
||||||
MaterialsCreated = created,
|
|
||||||
MaterialsSkipped = skipped
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
namespace CutList.Web.DTOs;
|
||||||
|
|
||||||
|
public class CatalogData
|
||||||
|
{
|
||||||
|
public DateTime ExportedAt { get; set; }
|
||||||
|
public List<CatalogSupplierDto> Suppliers { get; set; } = [];
|
||||||
|
public List<CatalogCuttingToolDto> CuttingTools { get; set; } = [];
|
||||||
|
public List<CatalogMaterialDto> Materials { get; set; } = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public class CatalogSupplierDto
|
||||||
|
{
|
||||||
|
public string Name { get; set; } = "";
|
||||||
|
public string? ContactInfo { get; set; }
|
||||||
|
public string? Notes { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class CatalogCuttingToolDto
|
||||||
|
{
|
||||||
|
public string Name { get; set; } = "";
|
||||||
|
public decimal KerfInches { get; set; }
|
||||||
|
public bool IsDefault { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class CatalogMaterialDto
|
||||||
|
{
|
||||||
|
public string Shape { get; set; } = "";
|
||||||
|
public string Type { get; set; } = "";
|
||||||
|
public string? Grade { get; set; }
|
||||||
|
public string Size { get; set; } = "";
|
||||||
|
public string? Description { get; set; }
|
||||||
|
public CatalogDimensionsDto? Dimensions { get; set; }
|
||||||
|
public List<CatalogStockItemDto> StockItems { get; set; } = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public class CatalogDimensionsDto
|
||||||
|
{
|
||||||
|
public decimal? Diameter { get; set; }
|
||||||
|
public decimal? OuterDiameter { get; set; }
|
||||||
|
public decimal? Width { get; set; }
|
||||||
|
public decimal? Height { get; set; }
|
||||||
|
public decimal? Thickness { get; set; }
|
||||||
|
public decimal? Wall { get; set; }
|
||||||
|
public decimal? Size { get; set; }
|
||||||
|
public decimal? Leg1 { get; set; }
|
||||||
|
public decimal? Leg2 { get; set; }
|
||||||
|
public decimal? Flange { get; set; }
|
||||||
|
public decimal? Web { get; set; }
|
||||||
|
public decimal? WeightPerFoot { get; set; }
|
||||||
|
public decimal? NominalSize { get; set; }
|
||||||
|
public string? Schedule { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class CatalogStockItemDto
|
||||||
|
{
|
||||||
|
public decimal LengthInches { get; set; }
|
||||||
|
public string? Name { get; set; }
|
||||||
|
public int QuantityOnHand { get; set; }
|
||||||
|
public string? Notes { get; set; }
|
||||||
|
public List<CatalogSupplierOfferingDto> SupplierOfferings { get; set; } = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public class CatalogSupplierOfferingDto
|
||||||
|
{
|
||||||
|
public string SupplierName { get; set; } = "";
|
||||||
|
public string? PartNumber { get; set; }
|
||||||
|
public string? SupplierDescription { get; set; }
|
||||||
|
public decimal? Price { get; set; }
|
||||||
|
public string? Notes { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ImportResultDto
|
||||||
|
{
|
||||||
|
public int SuppliersCreated { get; set; }
|
||||||
|
public int SuppliersUpdated { get; set; }
|
||||||
|
public int CuttingToolsCreated { get; set; }
|
||||||
|
public int CuttingToolsUpdated { get; set; }
|
||||||
|
public int MaterialsCreated { get; set; }
|
||||||
|
public int MaterialsUpdated { get; set; }
|
||||||
|
public int StockItemsCreated { get; set; }
|
||||||
|
public int StockItemsUpdated { get; set; }
|
||||||
|
public int OfferingsCreated { get; set; }
|
||||||
|
public int OfferingsUpdated { get; set; }
|
||||||
|
public List<string> Errors { get; set; } = [];
|
||||||
|
public List<string> Warnings { get; set; } = [];
|
||||||
|
}
|
||||||
@@ -22,6 +22,7 @@ builder.Services.AddScoped<JobService>();
|
|||||||
builder.Services.AddScoped<CutListPackingService>();
|
builder.Services.AddScoped<CutListPackingService>();
|
||||||
builder.Services.AddScoped<ReportService>();
|
builder.Services.AddScoped<ReportService>();
|
||||||
builder.Services.AddScoped<PurchaseItemService>();
|
builder.Services.AddScoped<PurchaseItemService>();
|
||||||
|
builder.Services.AddScoped<CatalogService>();
|
||||||
|
|
||||||
var app = builder.Build();
|
var app = builder.Build();
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,460 @@
|
|||||||
|
using CutList.Web.Data;
|
||||||
|
using CutList.Web.Data.Entities;
|
||||||
|
using CutList.Web.DTOs;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace CutList.Web.Services;
|
||||||
|
|
||||||
|
public class CatalogService
|
||||||
|
{
|
||||||
|
private readonly ApplicationDbContext _context;
|
||||||
|
private readonly MaterialService _materialService;
|
||||||
|
|
||||||
|
public CatalogService(ApplicationDbContext context, MaterialService materialService)
|
||||||
|
{
|
||||||
|
_context = context;
|
||||||
|
_materialService = materialService;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<CatalogData> ExportAsync()
|
||||||
|
{
|
||||||
|
var suppliers = await _context.Suppliers
|
||||||
|
.Where(s => s.IsActive)
|
||||||
|
.OrderBy(s => s.Name)
|
||||||
|
.AsNoTracking()
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
var cuttingTools = await _context.CuttingTools
|
||||||
|
.Where(t => t.IsActive)
|
||||||
|
.OrderBy(t => t.Name)
|
||||||
|
.AsNoTracking()
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
var materials = await _context.Materials
|
||||||
|
.Include(m => m.Dimensions)
|
||||||
|
.Include(m => m.StockItems.Where(s => s.IsActive))
|
||||||
|
.ThenInclude(s => s.SupplierOfferings.Where(o => o.IsActive))
|
||||||
|
.Where(m => m.IsActive)
|
||||||
|
.OrderBy(m => m.Shape).ThenBy(m => m.SortOrder)
|
||||||
|
.AsNoTracking()
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
return new CatalogData
|
||||||
|
{
|
||||||
|
ExportedAt = DateTime.UtcNow,
|
||||||
|
Suppliers = suppliers.Select(s => new CatalogSupplierDto
|
||||||
|
{
|
||||||
|
Name = s.Name,
|
||||||
|
ContactInfo = s.ContactInfo,
|
||||||
|
Notes = s.Notes
|
||||||
|
}).ToList(),
|
||||||
|
CuttingTools = cuttingTools.Select(t => new CatalogCuttingToolDto
|
||||||
|
{
|
||||||
|
Name = t.Name,
|
||||||
|
KerfInches = t.KerfInches,
|
||||||
|
IsDefault = t.IsDefault
|
||||||
|
}).ToList(),
|
||||||
|
Materials = materials.Select(m => new CatalogMaterialDto
|
||||||
|
{
|
||||||
|
Shape = m.Shape.ToString(),
|
||||||
|
Type = m.Type.ToString(),
|
||||||
|
Grade = m.Grade,
|
||||||
|
Size = m.Size,
|
||||||
|
Description = m.Description,
|
||||||
|
Dimensions = MapDimensions(m.Dimensions),
|
||||||
|
StockItems = m.StockItems.OrderBy(s => s.LengthInches).Select(s => new CatalogStockItemDto
|
||||||
|
{
|
||||||
|
LengthInches = s.LengthInches,
|
||||||
|
Name = s.Name,
|
||||||
|
QuantityOnHand = s.QuantityOnHand,
|
||||||
|
Notes = s.Notes,
|
||||||
|
SupplierOfferings = s.SupplierOfferings.Select(o => new CatalogSupplierOfferingDto
|
||||||
|
{
|
||||||
|
SupplierName = suppliers.FirstOrDefault(sup => sup.Id == o.SupplierId)?.Name ?? "Unknown",
|
||||||
|
PartNumber = o.PartNumber,
|
||||||
|
SupplierDescription = o.SupplierDescription,
|
||||||
|
Price = o.Price,
|
||||||
|
Notes = o.Notes
|
||||||
|
}).ToList()
|
||||||
|
}).ToList()
|
||||||
|
}).ToList()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<ImportResultDto> ImportAsync(CatalogData data)
|
||||||
|
{
|
||||||
|
var result = new ImportResultDto();
|
||||||
|
|
||||||
|
await using var transaction = await _context.Database.BeginTransactionAsync();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// 1. Suppliers — upsert by name
|
||||||
|
var supplierMap = await ImportSuppliersAsync(data.Suppliers, result);
|
||||||
|
|
||||||
|
// 2. Cutting tools — upsert by name
|
||||||
|
await ImportCuttingToolsAsync(data.CuttingTools, result);
|
||||||
|
|
||||||
|
// 3. Materials + stock items + offerings
|
||||||
|
await ImportMaterialsAsync(data.Materials, supplierMap, result);
|
||||||
|
|
||||||
|
await transaction.CommitAsync();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
await transaction.RollbackAsync();
|
||||||
|
result.Errors.Add($"Transaction failed: {ex.Message}");
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<Dictionary<string, int>> ImportSuppliersAsync(
|
||||||
|
List<CatalogSupplierDto> suppliers, ImportResultDto result)
|
||||||
|
{
|
||||||
|
var map = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
var existingSuppliers = await _context.Suppliers.ToListAsync();
|
||||||
|
|
||||||
|
foreach (var dto in suppliers)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var existing = existingSuppliers.FirstOrDefault(
|
||||||
|
s => s.Name.Equals(dto.Name, StringComparison.OrdinalIgnoreCase));
|
||||||
|
|
||||||
|
if (existing != null)
|
||||||
|
{
|
||||||
|
existing.ContactInfo = dto.ContactInfo ?? existing.ContactInfo;
|
||||||
|
existing.Notes = dto.Notes ?? existing.Notes;
|
||||||
|
existing.IsActive = true;
|
||||||
|
map[dto.Name] = existing.Id;
|
||||||
|
result.SuppliersUpdated++;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var supplier = new Supplier
|
||||||
|
{
|
||||||
|
Name = dto.Name,
|
||||||
|
ContactInfo = dto.ContactInfo,
|
||||||
|
Notes = dto.Notes,
|
||||||
|
CreatedAt = DateTime.UtcNow
|
||||||
|
};
|
||||||
|
_context.Suppliers.Add(supplier);
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
existingSuppliers.Add(supplier);
|
||||||
|
map[dto.Name] = supplier.Id;
|
||||||
|
result.SuppliersCreated++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
result.Errors.Add($"Supplier '{dto.Name}': {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ImportCuttingToolsAsync(
|
||||||
|
List<CatalogCuttingToolDto> tools, ImportResultDto result)
|
||||||
|
{
|
||||||
|
var existingTools = await _context.CuttingTools.ToListAsync();
|
||||||
|
|
||||||
|
foreach (var dto in tools)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var existing = existingTools.FirstOrDefault(
|
||||||
|
t => t.Name.Equals(dto.Name, StringComparison.OrdinalIgnoreCase));
|
||||||
|
|
||||||
|
if (existing != null)
|
||||||
|
{
|
||||||
|
existing.KerfInches = dto.KerfInches;
|
||||||
|
existing.IsActive = true;
|
||||||
|
// Skip IsDefault changes to avoid conflicts
|
||||||
|
result.CuttingToolsUpdated++;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var tool = new CuttingTool
|
||||||
|
{
|
||||||
|
Name = dto.Name,
|
||||||
|
KerfInches = dto.KerfInches,
|
||||||
|
IsDefault = false // Never import as default to avoid conflicts
|
||||||
|
};
|
||||||
|
_context.CuttingTools.Add(tool);
|
||||||
|
existingTools.Add(tool);
|
||||||
|
result.CuttingToolsCreated++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
result.Errors.Add($"Cutting tool '{dto.Name}': {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ImportMaterialsAsync(
|
||||||
|
List<CatalogMaterialDto> materials, Dictionary<string, int> supplierMap, ImportResultDto result)
|
||||||
|
{
|
||||||
|
// Pre-load existing materials with their dimensions
|
||||||
|
var existingMaterials = await _context.Materials
|
||||||
|
.Include(m => m.Dimensions)
|
||||||
|
.Include(m => m.StockItems)
|
||||||
|
.ThenInclude(s => s.SupplierOfferings)
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
foreach (var dto in materials)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (!Enum.TryParse<MaterialShape>(dto.Shape, ignoreCase: true, out var shape))
|
||||||
|
{
|
||||||
|
result.Errors.Add($"Material '{dto.Shape} - {dto.Size}': Unknown shape '{dto.Shape}'");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Enum.TryParse<MaterialType>(dto.Type, ignoreCase: true, out var type))
|
||||||
|
{
|
||||||
|
type = MaterialType.Steel; // Default
|
||||||
|
result.Warnings.Add($"Material '{dto.Shape} - {dto.Size}': Unknown type '{dto.Type}', defaulting to Steel");
|
||||||
|
}
|
||||||
|
|
||||||
|
var existing = existingMaterials.FirstOrDefault(
|
||||||
|
m => m.Shape == shape && m.Size.Equals(dto.Size, StringComparison.OrdinalIgnoreCase));
|
||||||
|
|
||||||
|
Material material;
|
||||||
|
|
||||||
|
if (existing != null)
|
||||||
|
{
|
||||||
|
// Update existing material
|
||||||
|
existing.Type = type;
|
||||||
|
existing.Grade = dto.Grade ?? existing.Grade;
|
||||||
|
existing.Description = dto.Description ?? existing.Description;
|
||||||
|
existing.IsActive = true;
|
||||||
|
existing.UpdatedAt = DateTime.UtcNow;
|
||||||
|
|
||||||
|
// Update dimensions if provided
|
||||||
|
if (dto.Dimensions != null && existing.Dimensions != null)
|
||||||
|
{
|
||||||
|
ApplyDimensionValues(existing.Dimensions, dto.Dimensions);
|
||||||
|
existing.SortOrder = existing.Dimensions.GetSortOrder();
|
||||||
|
}
|
||||||
|
|
||||||
|
material = existing;
|
||||||
|
result.MaterialsUpdated++;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Create new material with dimensions
|
||||||
|
material = new Material
|
||||||
|
{
|
||||||
|
Shape = shape,
|
||||||
|
Type = type,
|
||||||
|
Grade = dto.Grade,
|
||||||
|
Size = dto.Size,
|
||||||
|
Description = dto.Description,
|
||||||
|
CreatedAt = DateTime.UtcNow
|
||||||
|
};
|
||||||
|
|
||||||
|
if (dto.Dimensions != null)
|
||||||
|
{
|
||||||
|
var dimensions = MaterialService.CreateDimensionsForShape(shape);
|
||||||
|
ApplyDimensionValues(dimensions, dto.Dimensions);
|
||||||
|
material = await _materialService.CreateWithDimensionsAsync(material, dimensions);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_context.Materials.Add(material);
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
existingMaterials.Add(material);
|
||||||
|
result.MaterialsCreated++;
|
||||||
|
}
|
||||||
|
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
|
||||||
|
// Import stock items for this material
|
||||||
|
await ImportStockItemsAsync(material, dto.StockItems, supplierMap, result);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
result.Errors.Add($"Material '{dto.Shape} - {dto.Size}': {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ImportStockItemsAsync(
|
||||||
|
Material material, List<CatalogStockItemDto> stockItems,
|
||||||
|
Dictionary<string, int> supplierMap, ImportResultDto result)
|
||||||
|
{
|
||||||
|
// Reload stock items for this material to ensure we have current state
|
||||||
|
var existingStockItems = await _context.StockItems
|
||||||
|
.Include(s => s.SupplierOfferings)
|
||||||
|
.Where(s => s.MaterialId == material.Id)
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
foreach (var dto in stockItems)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var existing = existingStockItems.FirstOrDefault(
|
||||||
|
s => s.LengthInches == dto.LengthInches);
|
||||||
|
|
||||||
|
StockItem stockItem;
|
||||||
|
|
||||||
|
if (existing != null)
|
||||||
|
{
|
||||||
|
existing.Name = dto.Name ?? existing.Name;
|
||||||
|
existing.Notes = dto.Notes ?? existing.Notes;
|
||||||
|
existing.IsActive = true;
|
||||||
|
existing.UpdatedAt = DateTime.UtcNow;
|
||||||
|
// Don't overwrite QuantityOnHand — preserve actual inventory
|
||||||
|
stockItem = existing;
|
||||||
|
result.StockItemsUpdated++;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
stockItem = new StockItem
|
||||||
|
{
|
||||||
|
MaterialId = material.Id,
|
||||||
|
LengthInches = dto.LengthInches,
|
||||||
|
Name = dto.Name,
|
||||||
|
QuantityOnHand = dto.QuantityOnHand,
|
||||||
|
Notes = dto.Notes,
|
||||||
|
CreatedAt = DateTime.UtcNow
|
||||||
|
};
|
||||||
|
_context.StockItems.Add(stockItem);
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
existingStockItems.Add(stockItem);
|
||||||
|
result.StockItemsCreated++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Import supplier offerings
|
||||||
|
foreach (var offeringDto in dto.SupplierOfferings)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (!supplierMap.TryGetValue(offeringDto.SupplierName, out var supplierId))
|
||||||
|
{
|
||||||
|
result.Warnings.Add(
|
||||||
|
$"Offering for stock '{material.DisplayName} @ {dto.LengthInches}\"': " +
|
||||||
|
$"Unknown supplier '{offeringDto.SupplierName}', skipped");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var existingOffering = stockItem.SupplierOfferings.FirstOrDefault(
|
||||||
|
o => o.SupplierId == supplierId);
|
||||||
|
|
||||||
|
if (existingOffering != null)
|
||||||
|
{
|
||||||
|
existingOffering.PartNumber = offeringDto.PartNumber ?? existingOffering.PartNumber;
|
||||||
|
existingOffering.SupplierDescription = offeringDto.SupplierDescription ?? existingOffering.SupplierDescription;
|
||||||
|
existingOffering.Price = offeringDto.Price ?? existingOffering.Price;
|
||||||
|
existingOffering.Notes = offeringDto.Notes ?? existingOffering.Notes;
|
||||||
|
existingOffering.IsActive = true;
|
||||||
|
result.OfferingsUpdated++;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var offering = new SupplierOffering
|
||||||
|
{
|
||||||
|
StockItemId = stockItem.Id,
|
||||||
|
SupplierId = supplierId,
|
||||||
|
PartNumber = offeringDto.PartNumber,
|
||||||
|
SupplierDescription = offeringDto.SupplierDescription,
|
||||||
|
Price = offeringDto.Price,
|
||||||
|
Notes = offeringDto.Notes
|
||||||
|
};
|
||||||
|
_context.SupplierOfferings.Add(offering);
|
||||||
|
stockItem.SupplierOfferings.Add(offering);
|
||||||
|
result.OfferingsCreated++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
result.Errors.Add(
|
||||||
|
$"Offering for '{material.DisplayName} @ {dto.LengthInches}\"' " +
|
||||||
|
$"from '{offeringDto.SupplierName}': {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
result.Errors.Add(
|
||||||
|
$"Stock item '{material.DisplayName} @ {dto.LengthInches}\"': {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static CatalogDimensionsDto? MapDimensions(MaterialDimensions? dim) => dim switch
|
||||||
|
{
|
||||||
|
RoundBarDimensions d => new CatalogDimensionsDto { Diameter = d.Diameter },
|
||||||
|
RoundTubeDimensions d => new CatalogDimensionsDto { OuterDiameter = d.OuterDiameter, Wall = d.Wall },
|
||||||
|
FlatBarDimensions d => new CatalogDimensionsDto { Width = d.Width, Thickness = d.Thickness },
|
||||||
|
SquareBarDimensions d => new CatalogDimensionsDto { Size = d.Size },
|
||||||
|
SquareTubeDimensions d => new CatalogDimensionsDto { Size = d.Size, Wall = d.Wall },
|
||||||
|
RectangularTubeDimensions d => new CatalogDimensionsDto { Width = d.Width, Height = d.Height, Wall = d.Wall },
|
||||||
|
AngleDimensions d => new CatalogDimensionsDto { Leg1 = d.Leg1, Leg2 = d.Leg2, Thickness = d.Thickness },
|
||||||
|
ChannelDimensions d => new CatalogDimensionsDto { Height = d.Height, Flange = d.Flange, Web = d.Web },
|
||||||
|
IBeamDimensions d => new CatalogDimensionsDto { Height = d.Height, WeightPerFoot = d.WeightPerFoot },
|
||||||
|
PipeDimensions d => new CatalogDimensionsDto { NominalSize = d.NominalSize, Wall = d.Wall, Schedule = d.Schedule },
|
||||||
|
_ => null
|
||||||
|
};
|
||||||
|
|
||||||
|
private static void ApplyDimensionValues(MaterialDimensions dimensions, CatalogDimensionsDto dto)
|
||||||
|
{
|
||||||
|
switch (dimensions)
|
||||||
|
{
|
||||||
|
case RoundBarDimensions rb:
|
||||||
|
if (dto.Diameter.HasValue) rb.Diameter = dto.Diameter.Value;
|
||||||
|
break;
|
||||||
|
case RoundTubeDimensions rt:
|
||||||
|
if (dto.OuterDiameter.HasValue) rt.OuterDiameter = dto.OuterDiameter.Value;
|
||||||
|
if (dto.Wall.HasValue) rt.Wall = dto.Wall.Value;
|
||||||
|
break;
|
||||||
|
case FlatBarDimensions fb:
|
||||||
|
if (dto.Width.HasValue) fb.Width = dto.Width.Value;
|
||||||
|
if (dto.Thickness.HasValue) fb.Thickness = dto.Thickness.Value;
|
||||||
|
break;
|
||||||
|
case SquareBarDimensions sb:
|
||||||
|
if (dto.Size.HasValue) sb.Size = dto.Size.Value;
|
||||||
|
break;
|
||||||
|
case SquareTubeDimensions st:
|
||||||
|
if (dto.Size.HasValue) st.Size = dto.Size.Value;
|
||||||
|
if (dto.Wall.HasValue) st.Wall = dto.Wall.Value;
|
||||||
|
break;
|
||||||
|
case RectangularTubeDimensions rect:
|
||||||
|
if (dto.Width.HasValue) rect.Width = dto.Width.Value;
|
||||||
|
if (dto.Height.HasValue) rect.Height = dto.Height.Value;
|
||||||
|
if (dto.Wall.HasValue) rect.Wall = dto.Wall.Value;
|
||||||
|
break;
|
||||||
|
case AngleDimensions a:
|
||||||
|
if (dto.Leg1.HasValue) a.Leg1 = dto.Leg1.Value;
|
||||||
|
if (dto.Leg2.HasValue) a.Leg2 = dto.Leg2.Value;
|
||||||
|
if (dto.Thickness.HasValue) a.Thickness = dto.Thickness.Value;
|
||||||
|
break;
|
||||||
|
case ChannelDimensions c:
|
||||||
|
if (dto.Height.HasValue) c.Height = dto.Height.Value;
|
||||||
|
if (dto.Flange.HasValue) c.Flange = dto.Flange.Value;
|
||||||
|
if (dto.Web.HasValue) c.Web = dto.Web.Value;
|
||||||
|
break;
|
||||||
|
case IBeamDimensions ib:
|
||||||
|
if (dto.Height.HasValue) ib.Height = dto.Height.Value;
|
||||||
|
if (dto.WeightPerFoot.HasValue) ib.WeightPerFoot = dto.WeightPerFoot.Value;
|
||||||
|
break;
|
||||||
|
case PipeDimensions p:
|
||||||
|
if (dto.NominalSize.HasValue) p.NominalSize = dto.NominalSize.Value;
|
||||||
|
if (dto.Wall.HasValue) p.Wall = dto.Wall.Value;
|
||||||
|
if (dto.Schedule != null) p.Schedule = dto.Schedule;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -136,6 +136,11 @@
|
|||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Cut list material headers — hidden on screen, shown in print via repeating thead */
|
||||||
|
.cutlist-material-print-header {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
/* Print styles - Compact layout to save paper */
|
/* Print styles - Compact layout to save paper */
|
||||||
@media print {
|
@media print {
|
||||||
body {
|
body {
|
||||||
@@ -299,18 +304,23 @@
|
|||||||
margin: 0 !important;
|
margin: 0 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Hide redundant stock summary (shown per-material) */
|
/* Keep purchase list with cut lists to save paper */
|
||||||
.print-stock-summary {
|
.print-purchase-list {
|
||||||
display: none !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* General card print styles */
|
|
||||||
.card {
|
|
||||||
border: 1px solid #ccc !important;
|
|
||||||
break-inside: avoid;
|
break-inside: avoid;
|
||||||
page-break-inside: avoid;
|
page-break-inside: avoid;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* General card print styles — allow large cards to break across pages */
|
||||||
|
.card {
|
||||||
|
border: 1px solid #ccc !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Keep card headers with the start of their content */
|
||||||
|
.card-header {
|
||||||
|
break-after: avoid;
|
||||||
|
page-break-after: avoid;
|
||||||
|
}
|
||||||
|
|
||||||
.card-header {
|
.card-header {
|
||||||
background-color: #f0f0f0 !important;
|
background-color: #f0f0f0 !important;
|
||||||
}
|
}
|
||||||
@@ -319,6 +329,42 @@
|
|||||||
border: 1px solid #999;
|
border: 1px solid #999;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Cut list tables: hide screen header, show repeating print header in thead */
|
||||||
|
.cutlist-material-screen-header {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cutlist-material-print-header {
|
||||||
|
display: table-row !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cutlist-material-print-header th {
|
||||||
|
background: #f0f0f0 !important;
|
||||||
|
padding: 0.4rem 0.5rem !important;
|
||||||
|
border-bottom: 1px solid #ccc !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cutlist-material-name {
|
||||||
|
font-size: 12pt;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cutlist-material-stats {
|
||||||
|
float: right;
|
||||||
|
font-size: 9pt;
|
||||||
|
font-weight: 400;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Remove card border/padding for cut list cards in print — table handles it */
|
||||||
|
.cutlist-material-card {
|
||||||
|
border: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cutlist-material-card > .card-body {
|
||||||
|
padding: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
/* Reduce spacing */
|
/* Reduce spacing */
|
||||||
.mb-4 {
|
.mb-4 {
|
||||||
margin-bottom: 0.5rem !important;
|
margin-bottom: 0.5rem !important;
|
||||||
|
|||||||
@@ -1,14 +0,0 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
|
||||||
|
|
||||||
<PropertyGroup>
|
|
||||||
<OutputType>Exe</OutputType>
|
|
||||||
<TargetFramework>net8.0</TargetFramework>
|
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
|
||||||
<Nullable>enable</Nullable>
|
|
||||||
</PropertyGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<ProjectReference Include="..\..\CutList.Web\CutList.Web.csproj" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
</Project>
|
|
||||||
@@ -1,191 +0,0 @@
|
|||||||
using System.Text.Encodings.Web;
|
|
||||||
using System.Text.Json;
|
|
||||||
using System.Text.Json.Serialization;
|
|
||||||
using CutList.Web.Data;
|
|
||||||
using CutList.Web.Data.Entities;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
|
|
||||||
// Build DbContext with the same connection string
|
|
||||||
var connectionString = "Server=localhost\\SQLEXPRESS;Database=CutListDb;Trusted_Connection=True;MultipleActiveResultSets=true;TrustServerCertificate=True";
|
|
||||||
|
|
||||||
var options = new DbContextOptionsBuilder<ApplicationDbContext>()
|
|
||||||
.UseSqlServer(connectionString)
|
|
||||||
.Options;
|
|
||||||
|
|
||||||
using var db = new ApplicationDbContext(options);
|
|
||||||
|
|
||||||
// Load all catalog data
|
|
||||||
var materials = await db.Materials
|
|
||||||
.Include(m => m.Dimensions)
|
|
||||||
.Include(m => m.StockItems.Where(s => s.IsActive))
|
|
||||||
.ThenInclude(s => s.SupplierOfferings.Where(o => o.IsActive))
|
|
||||||
.Where(m => m.IsActive)
|
|
||||||
.OrderBy(m => m.Shape).ThenBy(m => m.SortOrder)
|
|
||||||
.AsNoTracking()
|
|
||||||
.ToListAsync();
|
|
||||||
|
|
||||||
var suppliers = await db.Suppliers
|
|
||||||
.Where(s => s.IsActive)
|
|
||||||
.OrderBy(s => s.Name)
|
|
||||||
.AsNoTracking()
|
|
||||||
.ToListAsync();
|
|
||||||
|
|
||||||
var cuttingTools = await db.CuttingTools
|
|
||||||
.Where(t => t.IsActive)
|
|
||||||
.OrderBy(t => t.Name)
|
|
||||||
.AsNoTracking()
|
|
||||||
.ToListAsync();
|
|
||||||
|
|
||||||
// Build export DTOs to avoid circular references and keep it clean
|
|
||||||
var export = new ExportData
|
|
||||||
{
|
|
||||||
ExportedAt = DateTime.UtcNow,
|
|
||||||
Suppliers = suppliers.Select(s => new SupplierDto
|
|
||||||
{
|
|
||||||
Name = s.Name,
|
|
||||||
ContactInfo = s.ContactInfo,
|
|
||||||
Notes = s.Notes
|
|
||||||
}).ToList(),
|
|
||||||
CuttingTools = cuttingTools.Select(t => new CuttingToolDto
|
|
||||||
{
|
|
||||||
Name = t.Name,
|
|
||||||
KerfInches = t.KerfInches,
|
|
||||||
IsDefault = t.IsDefault
|
|
||||||
}).ToList(),
|
|
||||||
Materials = materials.Select(m => new MaterialDto
|
|
||||||
{
|
|
||||||
Shape = m.Shape.ToString(),
|
|
||||||
Type = m.Type.ToString(),
|
|
||||||
Grade = m.Grade,
|
|
||||||
Size = m.Size,
|
|
||||||
Description = m.Description,
|
|
||||||
Dimensions = MapDimensions(m.Dimensions),
|
|
||||||
StockItems = m.StockItems.OrderBy(s => s.LengthInches).Select(s => new StockItemDto
|
|
||||||
{
|
|
||||||
LengthInches = s.LengthInches,
|
|
||||||
Name = s.Name,
|
|
||||||
QuantityOnHand = s.QuantityOnHand,
|
|
||||||
Notes = s.Notes,
|
|
||||||
SupplierOfferings = s.SupplierOfferings.Select(o => new SupplierOfferingDto
|
|
||||||
{
|
|
||||||
SupplierName = suppliers.FirstOrDefault(sup => sup.Id == o.SupplierId)?.Name ?? "Unknown",
|
|
||||||
PartNumber = o.PartNumber,
|
|
||||||
SupplierDescription = o.SupplierDescription,
|
|
||||||
Price = o.Price,
|
|
||||||
Notes = o.Notes
|
|
||||||
}).ToList()
|
|
||||||
}).ToList()
|
|
||||||
}).ToList()
|
|
||||||
};
|
|
||||||
|
|
||||||
// Serialize to JSON
|
|
||||||
var jsonOptions = new JsonSerializerOptions
|
|
||||||
{
|
|
||||||
WriteIndented = true,
|
|
||||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
|
|
||||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
|
||||||
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping
|
|
||||||
};
|
|
||||||
|
|
||||||
var json = JsonSerializer.Serialize(export, jsonOptions);
|
|
||||||
|
|
||||||
// Determine output path - default to CutList.Web/Data/SeedData/
|
|
||||||
var repoRoot = Path.GetFullPath(Path.Combine(AppContext.BaseDirectory, "..", "..", "..", "..", ".."));
|
|
||||||
var outputPath = args.Length > 0
|
|
||||||
? args[0]
|
|
||||||
: Path.Combine(repoRoot, "CutList.Web", "Data", "SeedData", "oneals-catalog.json");
|
|
||||||
|
|
||||||
var outputDir = Path.GetDirectoryName(Path.GetFullPath(outputPath))!;
|
|
||||||
Directory.CreateDirectory(outputDir);
|
|
||||||
|
|
||||||
await File.WriteAllTextAsync(outputPath, json);
|
|
||||||
|
|
||||||
var fullPath = Path.GetFullPath(outputPath);
|
|
||||||
Console.WriteLine($"Exported {export.Materials.Count} materials, {export.Materials.Sum(m => m.StockItems.Count)} stock items, {export.Suppliers.Count} suppliers");
|
|
||||||
Console.WriteLine($"Written to: {fullPath}");
|
|
||||||
|
|
||||||
// --- Helper ---
|
|
||||||
static DimensionsDto? MapDimensions(MaterialDimensions? dim) => dim switch
|
|
||||||
{
|
|
||||||
RoundBarDimensions d => new DimensionsDto { Diameter = d.Diameter },
|
|
||||||
RoundTubeDimensions d => new DimensionsDto { OuterDiameter = d.OuterDiameter, Wall = d.Wall },
|
|
||||||
FlatBarDimensions d => new DimensionsDto { Width = d.Width, Thickness = d.Thickness },
|
|
||||||
SquareBarDimensions d => new DimensionsDto { Size = d.Size },
|
|
||||||
SquareTubeDimensions d => new DimensionsDto { Size = d.Size, Wall = d.Wall },
|
|
||||||
RectangularTubeDimensions d => new DimensionsDto { Width = d.Width, Height = d.Height, Wall = d.Wall },
|
|
||||||
AngleDimensions d => new DimensionsDto { Leg1 = d.Leg1, Leg2 = d.Leg2, Thickness = d.Thickness },
|
|
||||||
ChannelDimensions d => new DimensionsDto { Height = d.Height, Flange = d.Flange, Web = d.Web },
|
|
||||||
IBeamDimensions d => new DimensionsDto { Height = d.Height, WeightPerFoot = d.WeightPerFoot },
|
|
||||||
PipeDimensions d => new DimensionsDto { NominalSize = d.NominalSize, Wall = d.Wall, Schedule = d.Schedule },
|
|
||||||
_ => null
|
|
||||||
};
|
|
||||||
|
|
||||||
// --- DTOs ---
|
|
||||||
class ExportData
|
|
||||||
{
|
|
||||||
public DateTime ExportedAt { get; set; }
|
|
||||||
public List<SupplierDto> Suppliers { get; set; } = [];
|
|
||||||
public List<CuttingToolDto> CuttingTools { get; set; } = [];
|
|
||||||
public List<MaterialDto> Materials { get; set; } = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
class SupplierDto
|
|
||||||
{
|
|
||||||
public string Name { get; set; } = "";
|
|
||||||
public string? ContactInfo { get; set; }
|
|
||||||
public string? Notes { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
class CuttingToolDto
|
|
||||||
{
|
|
||||||
public string Name { get; set; } = "";
|
|
||||||
public decimal KerfInches { get; set; }
|
|
||||||
public bool IsDefault { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
class MaterialDto
|
|
||||||
{
|
|
||||||
public string Shape { get; set; } = "";
|
|
||||||
public string Type { get; set; } = "";
|
|
||||||
public string? Grade { get; set; }
|
|
||||||
public string Size { get; set; } = "";
|
|
||||||
public string? Description { get; set; }
|
|
||||||
public DimensionsDto? Dimensions { get; set; }
|
|
||||||
public List<StockItemDto> StockItems { get; set; } = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
class DimensionsDto
|
|
||||||
{
|
|
||||||
public decimal? Diameter { get; set; }
|
|
||||||
public decimal? OuterDiameter { get; set; }
|
|
||||||
public decimal? Width { get; set; }
|
|
||||||
public decimal? Height { get; set; }
|
|
||||||
public decimal? Thickness { get; set; }
|
|
||||||
public decimal? Wall { get; set; }
|
|
||||||
public decimal? Size { get; set; }
|
|
||||||
public decimal? Leg1 { get; set; }
|
|
||||||
public decimal? Leg2 { get; set; }
|
|
||||||
public decimal? Flange { get; set; }
|
|
||||||
public decimal? Web { get; set; }
|
|
||||||
public decimal? WeightPerFoot { get; set; }
|
|
||||||
public decimal? NominalSize { get; set; }
|
|
||||||
public string? Schedule { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
class StockItemDto
|
|
||||||
{
|
|
||||||
public decimal LengthInches { get; set; }
|
|
||||||
public string? Name { get; set; }
|
|
||||||
public int QuantityOnHand { get; set; }
|
|
||||||
public string? Notes { get; set; }
|
|
||||||
public List<SupplierOfferingDto> SupplierOfferings { get; set; } = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
class SupplierOfferingDto
|
|
||||||
{
|
|
||||||
public string SupplierName { get; set; } = "";
|
|
||||||
public string? PartNumber { get; set; }
|
|
||||||
public string? SupplierDescription { get; set; }
|
|
||||||
public decimal? Price { get; set; }
|
|
||||||
public string? Notes { get; set; }
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user