Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2a94ad63cb | |||
| b0a9d7fdcc | |||
| f20770d03e | |||
| 4aec4c2275 | |||
| 261f64a895 | |||
| 9b757acac3 | |||
| 177affabf0 | |||
| 17f16901ef |
Binary file not shown.
Binary file not shown.
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
Binary file not shown.
@@ -1,20 +0,0 @@
|
||||
{
|
||||
"runtimeOptions": {
|
||||
"tfm": "net8.0",
|
||||
"frameworks": [
|
||||
{
|
||||
"name": "Microsoft.NETCore.App",
|
||||
"version": "8.0.0"
|
||||
},
|
||||
{
|
||||
"name": "Microsoft.AspNetCore.App",
|
||||
"version": "8.0.0"
|
||||
}
|
||||
],
|
||||
"configProperties": {
|
||||
"System.Reflection.Metadata.MetadataUpdater.IsSupported": false,
|
||||
"System.Reflection.NullabilityInfoContext.IsSupported": true,
|
||||
"System.Runtime.Serialization.EnableUnsafeBinaryFormatterSerialization": false
|
||||
}
|
||||
}
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
@@ -1,21 +0,0 @@
|
||||
{
|
||||
"runtimeOptions": {
|
||||
"tfm": "net8.0",
|
||||
"frameworks": [
|
||||
{
|
||||
"name": "Microsoft.NETCore.App",
|
||||
"version": "8.0.0"
|
||||
},
|
||||
{
|
||||
"name": "Microsoft.AspNetCore.App",
|
||||
"version": "8.0.0"
|
||||
}
|
||||
],
|
||||
"configProperties": {
|
||||
"System.GC.Server": true,
|
||||
"System.Reflection.Metadata.MetadataUpdater.IsSupported": false,
|
||||
"System.Reflection.NullabilityInfoContext.IsSupported": true,
|
||||
"System.Runtime.Serialization.EnableUnsafeBinaryFormatterSerialization": false
|
||||
}
|
||||
}
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,8 +0,0 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning",
|
||||
"Microsoft.EntityFrameworkCore.Database.Command": "Warning"
|
||||
}
|
||||
},
|
||||
"AllowedHosts": "*",
|
||||
"ConnectionStrings": {
|
||||
"DefaultConnection": "Server=(localdb)\\mssqllocaldb;Database=CutListDb;Trusted_Connection=True;MultipleActiveResultSets=true"
|
||||
}
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,214 @@
|
||||
using System.Net.Http.Json;
|
||||
|
||||
namespace CutList.Mcp;
|
||||
|
||||
/// <summary>
|
||||
/// Typed HTTP client for calling the CutList.Web REST API.
|
||||
/// </summary>
|
||||
public class ApiClient
|
||||
{
|
||||
private readonly HttpClient _http;
|
||||
|
||||
public ApiClient(HttpClient http)
|
||||
{
|
||||
_http = http;
|
||||
}
|
||||
|
||||
#region Suppliers
|
||||
|
||||
public async Task<List<ApiSupplierDto>> GetSuppliersAsync(bool includeInactive = false)
|
||||
{
|
||||
var url = $"api/suppliers?includeInactive={includeInactive}";
|
||||
return await _http.GetFromJsonAsync<List<ApiSupplierDto>>(url) ?? [];
|
||||
}
|
||||
|
||||
public async Task<ApiSupplierDto?> CreateSupplierAsync(string name, string? contactInfo, string? notes)
|
||||
{
|
||||
var response = await _http.PostAsJsonAsync("api/suppliers", new { Name = name, ContactInfo = contactInfo, Notes = notes });
|
||||
response.EnsureSuccessStatusCode();
|
||||
return await response.Content.ReadFromJsonAsync<ApiSupplierDto>();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Materials
|
||||
|
||||
public async Task<List<ApiMaterialDto>> GetMaterialsAsync(string? shape = null, bool includeInactive = false)
|
||||
{
|
||||
var url = $"api/materials?includeInactive={includeInactive}";
|
||||
if (!string.IsNullOrEmpty(shape))
|
||||
url += $"&shape={Uri.EscapeDataString(shape)}";
|
||||
return await _http.GetFromJsonAsync<List<ApiMaterialDto>>(url) ?? [];
|
||||
}
|
||||
|
||||
public async Task<ApiMaterialDto?> CreateMaterialAsync(string shape, string? size, string? description,
|
||||
string? type, string? grade, Dictionary<string, decimal>? dimensions)
|
||||
{
|
||||
var body = new
|
||||
{
|
||||
Shape = shape,
|
||||
Size = size,
|
||||
Description = description,
|
||||
Type = type,
|
||||
Grade = grade,
|
||||
Dimensions = dimensions
|
||||
};
|
||||
var response = await _http.PostAsJsonAsync("api/materials", body);
|
||||
if (response.StatusCode == System.Net.HttpStatusCode.Conflict)
|
||||
{
|
||||
var error = await response.Content.ReadAsStringAsync();
|
||||
throw new ApiConflictException(error);
|
||||
}
|
||||
response.EnsureSuccessStatusCode();
|
||||
return await response.Content.ReadFromJsonAsync<ApiMaterialDto>();
|
||||
}
|
||||
|
||||
public async Task<List<ApiMaterialDto>> SearchMaterialsAsync(string shape, decimal targetValue, decimal tolerance)
|
||||
{
|
||||
var response = await _http.PostAsJsonAsync("api/materials/search", new
|
||||
{
|
||||
Shape = shape,
|
||||
TargetValue = targetValue,
|
||||
Tolerance = tolerance
|
||||
});
|
||||
response.EnsureSuccessStatusCode();
|
||||
return await response.Content.ReadFromJsonAsync<List<ApiMaterialDto>>() ?? [];
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Stock Items
|
||||
|
||||
public async Task<List<ApiStockItemDto>> GetStockItemsAsync(int? materialId = null, bool includeInactive = false)
|
||||
{
|
||||
var url = $"api/stock-items?includeInactive={includeInactive}";
|
||||
if (materialId.HasValue)
|
||||
url += $"&materialId={materialId.Value}";
|
||||
return await _http.GetFromJsonAsync<List<ApiStockItemDto>>(url) ?? [];
|
||||
}
|
||||
|
||||
public async Task<ApiStockItemDto?> CreateStockItemAsync(int materialId, string length, string? name, int quantityOnHand, string? notes)
|
||||
{
|
||||
var body = new
|
||||
{
|
||||
MaterialId = materialId,
|
||||
Length = length,
|
||||
Name = name,
|
||||
QuantityOnHand = quantityOnHand,
|
||||
Notes = notes
|
||||
};
|
||||
var response = await _http.PostAsJsonAsync("api/stock-items", body);
|
||||
if (response.StatusCode == System.Net.HttpStatusCode.Conflict)
|
||||
{
|
||||
var error = await response.Content.ReadAsStringAsync();
|
||||
throw new ApiConflictException(error);
|
||||
}
|
||||
response.EnsureSuccessStatusCode();
|
||||
return await response.Content.ReadFromJsonAsync<ApiStockItemDto>();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Offerings
|
||||
|
||||
public async Task<List<ApiOfferingDto>> GetOfferingsForSupplierAsync(int supplierId)
|
||||
{
|
||||
return await _http.GetFromJsonAsync<List<ApiOfferingDto>>($"api/suppliers/{supplierId}/offerings") ?? [];
|
||||
}
|
||||
|
||||
public async Task<List<ApiOfferingDto>> GetOfferingsForStockItemAsync(int stockItemId)
|
||||
{
|
||||
return await _http.GetFromJsonAsync<List<ApiOfferingDto>>($"api/stock-items/{stockItemId}/offerings") ?? [];
|
||||
}
|
||||
|
||||
public async Task<ApiOfferingDto?> CreateOfferingAsync(int supplierId, int stockItemId,
|
||||
string? partNumber, string? supplierDescription, decimal? price, string? notes)
|
||||
{
|
||||
var body = new
|
||||
{
|
||||
StockItemId = stockItemId,
|
||||
PartNumber = partNumber,
|
||||
SupplierDescription = supplierDescription,
|
||||
Price = price,
|
||||
Notes = notes
|
||||
};
|
||||
var response = await _http.PostAsJsonAsync($"api/suppliers/{supplierId}/offerings", body);
|
||||
if (response.StatusCode == System.Net.HttpStatusCode.Conflict)
|
||||
{
|
||||
var error = await response.Content.ReadAsStringAsync();
|
||||
throw new ApiConflictException(error);
|
||||
}
|
||||
response.EnsureSuccessStatusCode();
|
||||
return await response.Content.ReadFromJsonAsync<ApiOfferingDto>();
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Thrown when the API returns 409 Conflict (duplicate resource).
|
||||
/// </summary>
|
||||
public class ApiConflictException : Exception
|
||||
{
|
||||
public ApiConflictException(string message) : base(message) { }
|
||||
}
|
||||
|
||||
#region API Response DTOs
|
||||
|
||||
public class ApiSupplierDto
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public string? ContactInfo { get; set; }
|
||||
public string? Notes { get; set; }
|
||||
public bool IsActive { get; set; }
|
||||
}
|
||||
|
||||
public class ApiMaterialDto
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string Shape { get; set; } = string.Empty;
|
||||
public string Type { get; set; } = string.Empty;
|
||||
public string? Grade { get; set; }
|
||||
public string Size { get; set; } = string.Empty;
|
||||
public string? Description { get; set; }
|
||||
public bool IsActive { get; set; }
|
||||
public ApiMaterialDimensionsDto? Dimensions { get; set; }
|
||||
}
|
||||
|
||||
public class ApiMaterialDimensionsDto
|
||||
{
|
||||
public string DimensionType { get; set; } = string.Empty;
|
||||
public Dictionary<string, decimal> Values { get; set; } = new();
|
||||
}
|
||||
|
||||
public class ApiStockItemDto
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public int MaterialId { get; set; }
|
||||
public string MaterialName { get; set; } = string.Empty;
|
||||
public decimal LengthInches { get; set; }
|
||||
public string LengthFormatted { get; set; } = string.Empty;
|
||||
public string? Name { get; set; }
|
||||
public int QuantityOnHand { get; set; }
|
||||
public string? Notes { get; set; }
|
||||
public bool IsActive { get; set; }
|
||||
}
|
||||
|
||||
public class ApiOfferingDto
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public int SupplierId { get; set; }
|
||||
public string? SupplierName { get; set; }
|
||||
public int StockItemId { get; set; }
|
||||
public string? MaterialName { get; set; }
|
||||
public decimal? LengthInches { get; set; }
|
||||
public string? LengthFormatted { get; set; }
|
||||
public string? PartNumber { get; set; }
|
||||
public string? SupplierDescription { get; set; }
|
||||
public decimal? Price { get; set; }
|
||||
public string? Notes { get; set; }
|
||||
public bool IsActive { get; set; }
|
||||
}
|
||||
|
||||
#endregion
|
||||
@@ -2,11 +2,11 @@
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\CutList.Core\CutList.Core.csproj" />
|
||||
<ProjectReference Include="..\CutList.Web\CutList.Web.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting" Version="10.0.2" />
|
||||
<PackageReference Include="Microsoft.Extensions.Http" Version="9.0.4" />
|
||||
<PackageReference Include="ModelContextProtocol" Version="0.7.0-preview.1" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
+353
-639
File diff suppressed because it is too large
Load Diff
@@ -1,14 +1,15 @@
|
||||
using CutList.Web.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using CutList.Mcp;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using ModelContextProtocol.Server;
|
||||
|
||||
var builder = Host.CreateApplicationBuilder(args);
|
||||
|
||||
// Add DbContext for inventory tools
|
||||
builder.Services.AddDbContext<ApplicationDbContext>(options =>
|
||||
options.UseSqlServer("Server=(localdb)\\mssqllocaldb;Database=CutListDb;Trusted_Connection=True;MultipleActiveResultSets=true"));
|
||||
// Register HttpClient for API calls to CutList.Web
|
||||
builder.Services.AddHttpClient<ApiClient>(client =>
|
||||
{
|
||||
client.BaseAddress = new Uri("http://localhost:5009");
|
||||
});
|
||||
|
||||
builder.Services
|
||||
.AddMcpServer()
|
||||
|
||||
@@ -143,6 +143,102 @@ else
|
||||
</div>
|
||||
}
|
||||
|
||||
@* Import Stock Modal *@
|
||||
@if (showImportModal)
|
||||
{
|
||||
<div class="modal fade show d-block" tabindex="-1" style="background-color: rgba(0,0,0,0.5);">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Import Stock from Inventory</h5>
|
||||
<button type="button" class="btn-close" @onclick="CloseImportModal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
@if (loadingImport)
|
||||
{
|
||||
<div class="text-center py-4">
|
||||
<span class="spinner-border"></span>
|
||||
<p class="mt-2 text-muted">Finding matching stock...</p>
|
||||
</div>
|
||||
}
|
||||
else if (importCandidates.Count == 0)
|
||||
{
|
||||
<div class="text-center py-4 text-muted">
|
||||
<p class="mb-2">No matching inventory stock found.</p>
|
||||
<p class="small">Either no stock items exist for the materials in your parts, or they have already been added to this job.</p>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<div class="btn-group btn-group-sm">
|
||||
<button class="btn btn-outline-secondary" @onclick="() => ToggleAllImportCandidates(true)">Select All</button>
|
||||
<button class="btn btn-outline-secondary" @onclick="() => ToggleAllImportCandidates(false)">Select None</button>
|
||||
</div>
|
||||
<small class="text-muted">@importCandidates.Count(c => c.Selected) of @importCandidates.Count selected</small>
|
||||
</div>
|
||||
|
||||
@foreach (var group in importCandidates
|
||||
.GroupBy(c => c.StockItem.MaterialId)
|
||||
.OrderBy(g => g.First().StockItem.Material.Shape)
|
||||
.ThenBy(g => g.First().StockItem.Material.Size))
|
||||
{
|
||||
var material = group.First().StockItem.Material;
|
||||
<h6 class="mt-3 mb-2 text-primary">@material.DisplayName</h6>
|
||||
<table class="table table-sm table-hover mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 40px;"></th>
|
||||
<th>Length</th>
|
||||
<th>On Hand</th>
|
||||
<th style="width: 120px;">Qty to Use</th>
|
||||
<th style="width: 100px;">Priority</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var candidate in group.OrderByDescending(c => c.StockItem.LengthInches))
|
||||
{
|
||||
<tr class="@(candidate.Selected ? "" : "text-muted")">
|
||||
<td>
|
||||
<input type="checkbox" class="form-check-input" @bind="candidate.Selected" />
|
||||
</td>
|
||||
<td>@ArchUnits.FormatFromInches((double)candidate.StockItem.LengthInches)</td>
|
||||
<td>@candidate.StockItem.QuantityOnHand</td>
|
||||
<td>
|
||||
<input type="number" class="form-control form-control-sm" @bind="candidate.Quantity"
|
||||
min="-1" disabled="@(!candidate.Selected)" />
|
||||
<small class="text-muted">-1 = unlimited</small>
|
||||
</td>
|
||||
<td>
|
||||
<input type="number" class="form-control form-control-sm" @bind="candidate.Priority"
|
||||
min="1" disabled="@(!candidate.Selected)" />
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
}
|
||||
}
|
||||
@if (!string.IsNullOrEmpty(importErrorMessage))
|
||||
{
|
||||
<div class="alert alert-danger mt-3 mb-0">@importErrorMessage</div>
|
||||
}
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-outline-secondary" @onclick="CloseImportModal">Cancel</button>
|
||||
@if (importCandidates.Count > 0)
|
||||
{
|
||||
<button type="button" class="btn btn-primary" @onclick="ImportSelectedStockAsync"
|
||||
disabled="@(!importCandidates.Any(c => c.Selected))">
|
||||
Import @importCandidates.Count(c => c.Selected) Item@(importCandidates.Count(c => c.Selected) != 1 ? "s" : "")
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@code {
|
||||
private enum Tab { Details, Parts, Stock }
|
||||
|
||||
@@ -177,6 +273,12 @@ else
|
||||
private int stockSelectedMaterialId;
|
||||
private List<StockItem> availableStockItems = new();
|
||||
|
||||
// Import modal
|
||||
private bool showImportModal;
|
||||
private bool loadingImport;
|
||||
private List<ImportStockCandidate> importCandidates = new();
|
||||
private string? importErrorMessage;
|
||||
|
||||
private IEnumerable<MaterialShape> DistinctShapes => materials.Select(m => m.Shape).Distinct().OrderBy(s => s);
|
||||
private IEnumerable<Material> FilteredMaterials => !selectedShape.HasValue
|
||||
? Enumerable.Empty<Material>()
|
||||
@@ -443,9 +545,15 @@ else
|
||||
<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 class="d-flex gap-2">
|
||||
<button class="btn btn-success" @onclick="ShowImportModal" disabled="@(job.Parts.Count == 0)"
|
||||
title="@(job.Parts.Count == 0 ? "Add parts first to match against inventory" : "Find and import stock matching your parts")">
|
||||
Import from Inventory
|
||||
</button>
|
||||
<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>
|
||||
<div class="card-body">
|
||||
@@ -819,4 +927,103 @@ else
|
||||
await JobService.DeleteStockAsync(stock.Id);
|
||||
job = (await JobService.GetByIdAsync(Id!.Value))!;
|
||||
}
|
||||
|
||||
// Import modal methods
|
||||
private async Task ShowImportModal()
|
||||
{
|
||||
importErrorMessage = null;
|
||||
importCandidates.Clear();
|
||||
loadingImport = true;
|
||||
showImportModal = true;
|
||||
|
||||
try
|
||||
{
|
||||
var materialIds = job.Parts.Select(p => p.MaterialId).Distinct().ToList();
|
||||
var existingStockItemIds = job.Stock
|
||||
.Where(s => s.StockItemId.HasValue)
|
||||
.Select(s => s.StockItemId!.Value)
|
||||
.ToHashSet();
|
||||
|
||||
foreach (var materialId in materialIds)
|
||||
{
|
||||
var stockItems = await JobService.GetAvailableStockForMaterialAsync(materialId);
|
||||
foreach (var item in stockItems.Where(s => !existingStockItemIds.Contains(s.Id)))
|
||||
{
|
||||
importCandidates.Add(new ImportStockCandidate
|
||||
{
|
||||
StockItem = item,
|
||||
Selected = true,
|
||||
Quantity = -1,
|
||||
Priority = 10
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
importErrorMessage = $"Error loading stock: {ex.Message}";
|
||||
}
|
||||
finally
|
||||
{
|
||||
loadingImport = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void CloseImportModal()
|
||||
{
|
||||
showImportModal = false;
|
||||
importCandidates.Clear();
|
||||
importErrorMessage = null;
|
||||
}
|
||||
|
||||
private void ToggleAllImportCandidates(bool selected)
|
||||
{
|
||||
foreach (var c in importCandidates)
|
||||
c.Selected = selected;
|
||||
}
|
||||
|
||||
private async Task ImportSelectedStockAsync()
|
||||
{
|
||||
importErrorMessage = null;
|
||||
var selected = importCandidates.Where(c => c.Selected).ToList();
|
||||
if (selected.Count == 0)
|
||||
{
|
||||
importErrorMessage = "No items selected";
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
foreach (var candidate in selected)
|
||||
{
|
||||
var jobStock = new JobStock
|
||||
{
|
||||
JobId = Id!.Value,
|
||||
MaterialId = candidate.StockItem.MaterialId,
|
||||
StockItemId = candidate.StockItem.Id,
|
||||
LengthInches = candidate.StockItem.LengthInches,
|
||||
Quantity = candidate.Quantity,
|
||||
Priority = candidate.Priority,
|
||||
IsCustomLength = false
|
||||
};
|
||||
await JobService.AddStockAsync(jobStock);
|
||||
}
|
||||
|
||||
job = (await JobService.GetByIdAsync(Id!.Value))!;
|
||||
showImportModal = false;
|
||||
importCandidates.Clear();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
importErrorMessage = $"Error importing stock: {ex.Message}";
|
||||
}
|
||||
}
|
||||
|
||||
private class ImportStockCandidate
|
||||
{
|
||||
public StockItem StockItem { get; set; } = null!;
|
||||
public bool Selected { get; set; } = true;
|
||||
public int Quantity { get; set; } = -1;
|
||||
public int Priority { get; set; } = 10;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,11 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="text-muted mb-4">
|
||||
Jobs organize the parts you need to cut for a project. Add parts with their required lengths and quantities,
|
||||
assign stock materials, then run the optimizer to generate an efficient cut list that minimizes waste.
|
||||
</p>
|
||||
|
||||
@if (loading)
|
||||
{
|
||||
<p><em>Loading...</em></p>
|
||||
|
||||
@@ -9,6 +9,11 @@
|
||||
<a href="materials/new" class="btn btn-primary">Add Material</a>
|
||||
</div>
|
||||
|
||||
<p class="text-muted mb-4">
|
||||
Manage your material catalog here. Materials define the types of stock you work with — shape, size, type, and
|
||||
grade. Once added, materials can be assigned to jobs and used to generate optimized cut lists.
|
||||
</p>
|
||||
|
||||
@if (loading)
|
||||
{
|
||||
<p><em>Loading...</em></p>
|
||||
|
||||
@@ -11,6 +11,11 @@
|
||||
<a href="stock/new" class="btn btn-primary">Add Stock Item</a>
|
||||
</div>
|
||||
|
||||
<p class="text-muted mb-4">
|
||||
Stock items represent the specific lengths of material you have available for cutting. Each stock item links
|
||||
a material to a length and tracks how many pieces you have on hand.
|
||||
</p>
|
||||
|
||||
@if (loading)
|
||||
{
|
||||
<p><em>Loading...</em></p>
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
using CutList.Web.Data.Entities;
|
||||
using CutList.Web.DTOs;
|
||||
using CutList.Web.Services;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace CutList.Web.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/cutting-tools")]
|
||||
public class CuttingToolsController : ControllerBase
|
||||
{
|
||||
private readonly JobService _jobService;
|
||||
|
||||
public CuttingToolsController(JobService jobService)
|
||||
{
|
||||
_jobService = jobService;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public async Task<ActionResult<List<CuttingToolDto>>> GetAll([FromQuery] bool includeInactive = false)
|
||||
{
|
||||
var tools = await _jobService.GetCuttingToolsAsync(includeInactive);
|
||||
return Ok(tools.Select(MapToDto).ToList());
|
||||
}
|
||||
|
||||
[HttpGet("{id}")]
|
||||
public async Task<ActionResult<CuttingToolDto>> GetById(int id)
|
||||
{
|
||||
var tool = await _jobService.GetCuttingToolByIdAsync(id);
|
||||
if (tool == null)
|
||||
return NotFound();
|
||||
|
||||
return Ok(MapToDto(tool));
|
||||
}
|
||||
|
||||
[HttpGet("default")]
|
||||
public async Task<ActionResult<CuttingToolDto>> GetDefault()
|
||||
{
|
||||
var tool = await _jobService.GetDefaultCuttingToolAsync();
|
||||
if (tool == null)
|
||||
return NotFound();
|
||||
|
||||
return Ok(MapToDto(tool));
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
public async Task<ActionResult<CuttingToolDto>> Create(CreateCuttingToolDto dto)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(dto.Name))
|
||||
return BadRequest("Name is required");
|
||||
|
||||
var tool = new CuttingTool
|
||||
{
|
||||
Name = dto.Name,
|
||||
KerfInches = dto.KerfInches,
|
||||
IsDefault = dto.IsDefault
|
||||
};
|
||||
|
||||
await _jobService.CreateCuttingToolAsync(tool);
|
||||
return CreatedAtAction(nameof(GetById), new { id = tool.Id }, MapToDto(tool));
|
||||
}
|
||||
|
||||
[HttpPut("{id}")]
|
||||
public async Task<ActionResult<CuttingToolDto>> Update(int id, UpdateCuttingToolDto dto)
|
||||
{
|
||||
var tool = await _jobService.GetCuttingToolByIdAsync(id);
|
||||
if (tool == null)
|
||||
return NotFound();
|
||||
|
||||
if (dto.Name != null) tool.Name = dto.Name;
|
||||
if (dto.KerfInches.HasValue) tool.KerfInches = dto.KerfInches.Value;
|
||||
if (dto.IsDefault.HasValue) tool.IsDefault = dto.IsDefault.Value;
|
||||
|
||||
await _jobService.UpdateCuttingToolAsync(tool);
|
||||
return Ok(MapToDto(tool));
|
||||
}
|
||||
|
||||
[HttpDelete("{id}")]
|
||||
public async Task<IActionResult> Delete(int id)
|
||||
{
|
||||
var tool = await _jobService.GetCuttingToolByIdAsync(id);
|
||||
if (tool == null)
|
||||
return NotFound();
|
||||
|
||||
await _jobService.DeleteCuttingToolAsync(id);
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
private static CuttingToolDto MapToDto(CuttingTool tool) => new()
|
||||
{
|
||||
Id = tool.Id,
|
||||
Name = tool.Name,
|
||||
KerfInches = tool.KerfInches,
|
||||
IsDefault = tool.IsDefault,
|
||||
IsActive = tool.IsActive
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,492 @@
|
||||
using CutList.Core.Formatting;
|
||||
using CutList.Web.Data.Entities;
|
||||
using CutList.Web.DTOs;
|
||||
using CutList.Web.Services;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace CutList.Web.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/[controller]")]
|
||||
public class JobsController : ControllerBase
|
||||
{
|
||||
private readonly JobService _jobService;
|
||||
private readonly CutListPackingService _packingService;
|
||||
|
||||
public JobsController(JobService jobService, CutListPackingService packingService)
|
||||
{
|
||||
_jobService = jobService;
|
||||
_packingService = packingService;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public async Task<ActionResult<List<JobDto>>> GetAll()
|
||||
{
|
||||
var jobs = await _jobService.GetAllAsync();
|
||||
return Ok(jobs.Select(MapToDto).ToList());
|
||||
}
|
||||
|
||||
[HttpGet("{id}")]
|
||||
public async Task<ActionResult<JobDetailDto>> GetById(int id)
|
||||
{
|
||||
var job = await _jobService.GetByIdAsync(id);
|
||||
if (job == null)
|
||||
return NotFound();
|
||||
|
||||
return Ok(MapToDetailDto(job));
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
public async Task<ActionResult<JobDetailDto>> Create(CreateJobDto dto)
|
||||
{
|
||||
var job = new Job
|
||||
{
|
||||
Name = dto.Name,
|
||||
Customer = dto.Customer,
|
||||
CuttingToolId = dto.CuttingToolId,
|
||||
Notes = dto.Notes
|
||||
};
|
||||
|
||||
await _jobService.CreateAsync(job);
|
||||
var created = await _jobService.GetByIdAsync(job.Id);
|
||||
return CreatedAtAction(nameof(GetById), new { id = job.Id }, MapToDetailDto(created!));
|
||||
}
|
||||
|
||||
[HttpPost("quick-create")]
|
||||
public async Task<ActionResult<JobDetailDto>> QuickCreate(QuickCreateJobDto dto)
|
||||
{
|
||||
var job = await _jobService.QuickCreateAsync(dto.Customer);
|
||||
var created = await _jobService.GetByIdAsync(job.Id);
|
||||
return CreatedAtAction(nameof(GetById), new { id = job.Id }, MapToDetailDto(created!));
|
||||
}
|
||||
|
||||
[HttpPut("{id}")]
|
||||
public async Task<ActionResult<JobDetailDto>> Update(int id, UpdateJobDto dto)
|
||||
{
|
||||
var job = await _jobService.GetByIdAsync(id);
|
||||
if (job == null)
|
||||
return NotFound();
|
||||
|
||||
if (dto.Name != null) job.Name = dto.Name;
|
||||
if (dto.Customer != null) job.Customer = dto.Customer;
|
||||
if (dto.CuttingToolId.HasValue) job.CuttingToolId = dto.CuttingToolId;
|
||||
if (dto.Notes != null) job.Notes = dto.Notes;
|
||||
|
||||
await _jobService.UpdateAsync(job);
|
||||
|
||||
var updated = await _jobService.GetByIdAsync(id);
|
||||
return Ok(MapToDetailDto(updated!));
|
||||
}
|
||||
|
||||
[HttpDelete("{id}")]
|
||||
public async Task<IActionResult> Delete(int id)
|
||||
{
|
||||
var job = await _jobService.GetByIdAsync(id);
|
||||
if (job == null)
|
||||
return NotFound();
|
||||
|
||||
await _jobService.DeleteAsync(id);
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
[HttpPost("{id}/duplicate")]
|
||||
public async Task<ActionResult<JobDetailDto>> Duplicate(int id)
|
||||
{
|
||||
try
|
||||
{
|
||||
var duplicate = await _jobService.DuplicateAsync(id);
|
||||
var loaded = await _jobService.GetByIdAsync(duplicate.Id);
|
||||
return CreatedAtAction(nameof(GetById), new { id = duplicate.Id }, MapToDetailDto(loaded!));
|
||||
}
|
||||
catch (ArgumentException)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
}
|
||||
|
||||
// --- Parts ---
|
||||
|
||||
[HttpGet("{id}/parts")]
|
||||
public async Task<ActionResult<List<JobPartDto>>> GetParts(int id)
|
||||
{
|
||||
var job = await _jobService.GetByIdAsync(id);
|
||||
if (job == null)
|
||||
return NotFound();
|
||||
|
||||
return Ok(job.Parts.Select(MapPartToDto).ToList());
|
||||
}
|
||||
|
||||
[HttpPost("{id}/parts")]
|
||||
public async Task<ActionResult<JobPartDto>> AddPart(int id, CreateJobPartDto dto)
|
||||
{
|
||||
var job = await _jobService.GetByIdAsync(id);
|
||||
if (job == null)
|
||||
return NotFound();
|
||||
|
||||
decimal lengthInches;
|
||||
try
|
||||
{
|
||||
lengthInches = (decimal)ArchUnits.ParseToInches(dto.Length);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return BadRequest($"Invalid length format: {dto.Length}");
|
||||
}
|
||||
|
||||
var part = new JobPart
|
||||
{
|
||||
JobId = id,
|
||||
MaterialId = dto.MaterialId,
|
||||
Name = dto.Name,
|
||||
LengthInches = lengthInches,
|
||||
Quantity = dto.Quantity
|
||||
};
|
||||
|
||||
await _jobService.AddPartAsync(part);
|
||||
|
||||
// Reload to get material name
|
||||
var reloadedJob = await _jobService.GetByIdAsync(id);
|
||||
var addedPart = reloadedJob!.Parts.FirstOrDefault(p => p.Id == part.Id);
|
||||
return CreatedAtAction(nameof(GetParts), new { id }, MapPartToDto(addedPart ?? part));
|
||||
}
|
||||
|
||||
[HttpPut("{id}/parts/{partId}")]
|
||||
public async Task<ActionResult<JobPartDto>> UpdatePart(int id, int partId, UpdateJobPartDto dto)
|
||||
{
|
||||
var job = await _jobService.GetByIdAsync(id);
|
||||
if (job == null)
|
||||
return NotFound();
|
||||
|
||||
var part = job.Parts.FirstOrDefault(p => p.Id == partId);
|
||||
if (part == null)
|
||||
return NotFound();
|
||||
|
||||
if (dto.MaterialId.HasValue) part.MaterialId = dto.MaterialId.Value;
|
||||
if (dto.Name != null) part.Name = dto.Name;
|
||||
if (dto.Length != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
part.LengthInches = (decimal)ArchUnits.ParseToInches(dto.Length);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return BadRequest($"Invalid length format: {dto.Length}");
|
||||
}
|
||||
}
|
||||
if (dto.Quantity.HasValue) part.Quantity = dto.Quantity.Value;
|
||||
|
||||
await _jobService.UpdatePartAsync(part);
|
||||
|
||||
var reloadedJob = await _jobService.GetByIdAsync(id);
|
||||
var updatedPart = reloadedJob!.Parts.FirstOrDefault(p => p.Id == partId);
|
||||
return Ok(MapPartToDto(updatedPart ?? part));
|
||||
}
|
||||
|
||||
[HttpDelete("{id}/parts/{partId}")]
|
||||
public async Task<IActionResult> DeletePart(int id, int partId)
|
||||
{
|
||||
var job = await _jobService.GetByIdAsync(id);
|
||||
if (job == null)
|
||||
return NotFound();
|
||||
|
||||
var part = job.Parts.FirstOrDefault(p => p.Id == partId);
|
||||
if (part == null)
|
||||
return NotFound();
|
||||
|
||||
await _jobService.DeletePartAsync(partId);
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
// --- Stock ---
|
||||
|
||||
[HttpGet("{id}/stock")]
|
||||
public async Task<ActionResult<List<JobStockDto>>> GetStock(int id)
|
||||
{
|
||||
var job = await _jobService.GetByIdAsync(id);
|
||||
if (job == null)
|
||||
return NotFound();
|
||||
|
||||
return Ok(job.Stock.Select(MapStockToDto).ToList());
|
||||
}
|
||||
|
||||
[HttpPost("{id}/stock")]
|
||||
public async Task<ActionResult<JobStockDto>> AddStock(int id, CreateJobStockDto dto)
|
||||
{
|
||||
var job = await _jobService.GetByIdAsync(id);
|
||||
if (job == null)
|
||||
return NotFound();
|
||||
|
||||
decimal lengthInches;
|
||||
try
|
||||
{
|
||||
lengthInches = (decimal)ArchUnits.ParseToInches(dto.Length);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return BadRequest($"Invalid length format: {dto.Length}");
|
||||
}
|
||||
|
||||
var stock = new JobStock
|
||||
{
|
||||
JobId = id,
|
||||
MaterialId = dto.MaterialId,
|
||||
StockItemId = dto.StockItemId,
|
||||
LengthInches = lengthInches,
|
||||
Quantity = dto.Quantity,
|
||||
IsCustomLength = dto.IsCustomLength,
|
||||
Priority = dto.Priority
|
||||
};
|
||||
|
||||
await _jobService.AddStockAsync(stock);
|
||||
|
||||
var reloadedJob = await _jobService.GetByIdAsync(id);
|
||||
var addedStock = reloadedJob!.Stock.FirstOrDefault(s => s.Id == stock.Id);
|
||||
return CreatedAtAction(nameof(GetStock), new { id }, MapStockToDto(addedStock ?? stock));
|
||||
}
|
||||
|
||||
[HttpPut("{id}/stock/{stockId}")]
|
||||
public async Task<ActionResult<JobStockDto>> UpdateStock(int id, int stockId, UpdateJobStockDto dto)
|
||||
{
|
||||
var job = await _jobService.GetByIdAsync(id);
|
||||
if (job == null)
|
||||
return NotFound();
|
||||
|
||||
var stock = job.Stock.FirstOrDefault(s => s.Id == stockId);
|
||||
if (stock == null)
|
||||
return NotFound();
|
||||
|
||||
if (dto.StockItemId.HasValue) stock.StockItemId = dto.StockItemId;
|
||||
if (dto.Length != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
stock.LengthInches = (decimal)ArchUnits.ParseToInches(dto.Length);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return BadRequest($"Invalid length format: {dto.Length}");
|
||||
}
|
||||
}
|
||||
if (dto.Quantity.HasValue) stock.Quantity = dto.Quantity.Value;
|
||||
if (dto.IsCustomLength.HasValue) stock.IsCustomLength = dto.IsCustomLength.Value;
|
||||
if (dto.Priority.HasValue) stock.Priority = dto.Priority.Value;
|
||||
|
||||
await _jobService.UpdateStockAsync(stock);
|
||||
|
||||
var reloadedJob = await _jobService.GetByIdAsync(id);
|
||||
var updatedStock = reloadedJob!.Stock.FirstOrDefault(s => s.Id == stockId);
|
||||
return Ok(MapStockToDto(updatedStock ?? stock));
|
||||
}
|
||||
|
||||
[HttpDelete("{id}/stock/{stockId}")]
|
||||
public async Task<IActionResult> DeleteStock(int id, int stockId)
|
||||
{
|
||||
var job = await _jobService.GetByIdAsync(id);
|
||||
if (job == null)
|
||||
return NotFound();
|
||||
|
||||
var stock = job.Stock.FirstOrDefault(s => s.Id == stockId);
|
||||
if (stock == null)
|
||||
return NotFound();
|
||||
|
||||
await _jobService.DeleteStockAsync(stockId);
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
[HttpGet("{id}/available-stock/{materialId}")]
|
||||
public async Task<ActionResult<List<StockItemDto>>> GetAvailableStock(int id, int materialId)
|
||||
{
|
||||
var job = await _jobService.GetByIdAsync(id);
|
||||
if (job == null)
|
||||
return NotFound();
|
||||
|
||||
var items = await _jobService.GetAvailableStockForMaterialAsync(materialId);
|
||||
return Ok(items.Select(s => new StockItemDto
|
||||
{
|
||||
Id = s.Id,
|
||||
MaterialId = s.MaterialId,
|
||||
MaterialName = s.Material?.DisplayName ?? string.Empty,
|
||||
LengthInches = s.LengthInches,
|
||||
LengthFormatted = ArchUnits.FormatFromInches((double)s.LengthInches),
|
||||
Name = s.Name,
|
||||
QuantityOnHand = s.QuantityOnHand,
|
||||
IsActive = s.IsActive
|
||||
}).ToList());
|
||||
}
|
||||
|
||||
// --- Packing ---
|
||||
|
||||
[HttpPost("{id}/pack")]
|
||||
public async Task<ActionResult<PackResponseDto>> Pack(int id, PackJobRequestDto? dto = null)
|
||||
{
|
||||
var job = await _jobService.GetByIdAsync(id);
|
||||
if (job == null)
|
||||
return NotFound();
|
||||
|
||||
if (job.Parts.Count == 0)
|
||||
return BadRequest("Job has no parts to pack");
|
||||
|
||||
// Determine kerf
|
||||
decimal kerf = dto?.KerfOverride
|
||||
?? job.CuttingTool?.KerfInches
|
||||
?? (await _jobService.GetDefaultCuttingToolAsync())?.KerfInches
|
||||
?? 0.125m;
|
||||
|
||||
var result = await _packingService.PackAsync(job.Parts, kerf, job.Stock.Any() ? job.Stock : null);
|
||||
var summary = _packingService.GetSummary(result);
|
||||
|
||||
return Ok(MapPackResult(result, summary));
|
||||
}
|
||||
|
||||
// --- Mapping helpers ---
|
||||
|
||||
private static JobDto MapToDto(Job j) => new()
|
||||
{
|
||||
Id = j.Id,
|
||||
JobNumber = j.JobNumber,
|
||||
Name = j.Name,
|
||||
Customer = j.Customer,
|
||||
CuttingToolId = j.CuttingToolId,
|
||||
CuttingToolName = j.CuttingTool?.Name,
|
||||
Notes = j.Notes,
|
||||
CreatedAt = j.CreatedAt,
|
||||
UpdatedAt = j.UpdatedAt,
|
||||
PartCount = j.Parts?.Count ?? 0,
|
||||
StockCount = j.Stock?.Count ?? 0
|
||||
};
|
||||
|
||||
private static JobDetailDto MapToDetailDto(Job j) => new()
|
||||
{
|
||||
Id = j.Id,
|
||||
JobNumber = j.JobNumber,
|
||||
Name = j.Name,
|
||||
Customer = j.Customer,
|
||||
CuttingToolId = j.CuttingToolId,
|
||||
CuttingToolName = j.CuttingTool?.Name,
|
||||
Notes = j.Notes,
|
||||
CreatedAt = j.CreatedAt,
|
||||
UpdatedAt = j.UpdatedAt,
|
||||
PartCount = j.Parts?.Count ?? 0,
|
||||
StockCount = j.Stock?.Count ?? 0,
|
||||
Parts = j.Parts?.Select(MapPartToDto).ToList() ?? new(),
|
||||
Stock = j.Stock?.Select(MapStockToDto).ToList() ?? new()
|
||||
};
|
||||
|
||||
private static JobPartDto MapPartToDto(JobPart p) => new()
|
||||
{
|
||||
Id = p.Id,
|
||||
JobId = p.JobId,
|
||||
MaterialId = p.MaterialId,
|
||||
MaterialName = p.Material?.DisplayName ?? string.Empty,
|
||||
Name = p.Name,
|
||||
LengthInches = p.LengthInches,
|
||||
LengthFormatted = ArchUnits.FormatFromInches((double)p.LengthInches),
|
||||
Quantity = p.Quantity,
|
||||
SortOrder = p.SortOrder
|
||||
};
|
||||
|
||||
private static JobStockDto MapStockToDto(JobStock s) => new()
|
||||
{
|
||||
Id = s.Id,
|
||||
JobId = s.JobId,
|
||||
MaterialId = s.MaterialId,
|
||||
MaterialName = s.Material?.DisplayName ?? string.Empty,
|
||||
StockItemId = s.StockItemId,
|
||||
LengthInches = s.LengthInches,
|
||||
LengthFormatted = ArchUnits.FormatFromInches((double)s.LengthInches),
|
||||
Quantity = s.Quantity,
|
||||
IsCustomLength = s.IsCustomLength,
|
||||
Priority = s.Priority,
|
||||
SortOrder = s.SortOrder
|
||||
};
|
||||
|
||||
private static PackResponseDto MapPackResult(MultiMaterialPackResult result, MultiMaterialPackingSummary summary)
|
||||
{
|
||||
var response = new PackResponseDto();
|
||||
|
||||
foreach (var mr in result.MaterialResults)
|
||||
{
|
||||
var matResult = new MaterialPackResultDto
|
||||
{
|
||||
MaterialId = mr.Material.Id,
|
||||
MaterialName = mr.Material.DisplayName,
|
||||
InStockBins = mr.InStockBins.Select(MapBinToDto).ToList(),
|
||||
ToBePurchasedBins = mr.ToBePurchasedBins.Select(MapBinToDto).ToList(),
|
||||
ItemsNotPlaced = mr.PackResult.ItemsNotUsed.Select(i => new PackedItemDto
|
||||
{
|
||||
Name = i.Name,
|
||||
LengthInches = i.Length,
|
||||
LengthFormatted = ArchUnits.FormatFromInches(i.Length)
|
||||
}).ToList()
|
||||
};
|
||||
|
||||
var ms = summary.MaterialSummaries.FirstOrDefault(s => s.Material.Id == mr.Material.Id);
|
||||
if (ms != null)
|
||||
{
|
||||
matResult.Summary = new MaterialPackingSummaryDto
|
||||
{
|
||||
MaterialId = ms.Material.Id,
|
||||
MaterialName = ms.Material.DisplayName,
|
||||
InStockBins = ms.InStockBins,
|
||||
ToBePurchasedBins = ms.ToBePurchasedBins,
|
||||
TotalPieces = ms.TotalPieces,
|
||||
TotalMaterialInches = ms.TotalMaterial,
|
||||
TotalUsedInches = ms.TotalUsed,
|
||||
TotalWasteInches = ms.TotalWaste,
|
||||
Efficiency = ms.Efficiency,
|
||||
ItemsNotPlaced = ms.ItemsNotPlaced
|
||||
};
|
||||
}
|
||||
|
||||
response.Materials.Add(matResult);
|
||||
}
|
||||
|
||||
response.Summary = new PackingSummaryDto
|
||||
{
|
||||
TotalInStockBins = summary.TotalInStockBins,
|
||||
TotalToBePurchasedBins = summary.TotalToBePurchasedBins,
|
||||
TotalPieces = summary.TotalPieces,
|
||||
TotalMaterialInches = summary.TotalMaterial,
|
||||
TotalMaterialFormatted = ArchUnits.FormatFromInches(summary.TotalMaterial),
|
||||
TotalUsedInches = summary.TotalUsed,
|
||||
TotalUsedFormatted = ArchUnits.FormatFromInches(summary.TotalUsed),
|
||||
TotalWasteInches = summary.TotalWaste,
|
||||
TotalWasteFormatted = ArchUnits.FormatFromInches(summary.TotalWaste),
|
||||
Efficiency = summary.Efficiency,
|
||||
TotalItemsNotPlaced = summary.TotalItemsNotPlaced,
|
||||
MaterialSummaries = summary.MaterialSummaries.Select(ms => new MaterialPackingSummaryDto
|
||||
{
|
||||
MaterialId = ms.Material.Id,
|
||||
MaterialName = ms.Material.DisplayName,
|
||||
InStockBins = ms.InStockBins,
|
||||
ToBePurchasedBins = ms.ToBePurchasedBins,
|
||||
TotalPieces = ms.TotalPieces,
|
||||
TotalMaterialInches = ms.TotalMaterial,
|
||||
TotalUsedInches = ms.TotalUsed,
|
||||
TotalWasteInches = ms.TotalWaste,
|
||||
Efficiency = ms.Efficiency,
|
||||
ItemsNotPlaced = ms.ItemsNotPlaced
|
||||
}).ToList()
|
||||
};
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
private static PackedBinDto MapBinToDto(CutList.Core.Bin bin) => new()
|
||||
{
|
||||
LengthInches = bin.Length,
|
||||
LengthFormatted = ArchUnits.FormatFromInches(bin.Length),
|
||||
UsedInches = bin.UsedLength,
|
||||
UsedFormatted = ArchUnits.FormatFromInches(bin.UsedLength),
|
||||
WasteInches = bin.RemainingLength,
|
||||
WasteFormatted = ArchUnits.FormatFromInches(bin.RemainingLength),
|
||||
Efficiency = bin.Length > 0 ? bin.UsedLength / bin.Length * 100 : 0,
|
||||
Items = bin.Items.Select(i => new PackedItemDto
|
||||
{
|
||||
Name = i.Name,
|
||||
LengthInches = i.Length,
|
||||
LengthFormatted = ArchUnits.FormatFromInches(i.Length)
|
||||
}).ToList()
|
||||
};
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
using CutList.Web.Data;
|
||||
using CutList.Web.Data.Entities;
|
||||
using CutList.Web.DTOs;
|
||||
using CutList.Web.Services;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace CutList.Web.Controllers;
|
||||
|
||||
@@ -9,48 +9,55 @@ namespace CutList.Web.Controllers;
|
||||
[Route("api/[controller]")]
|
||||
public class MaterialsController : ControllerBase
|
||||
{
|
||||
private readonly ApplicationDbContext _context;
|
||||
private readonly MaterialService _materialService;
|
||||
|
||||
public MaterialsController(ApplicationDbContext context)
|
||||
public MaterialsController(MaterialService materialService)
|
||||
{
|
||||
_context = context;
|
||||
_materialService = materialService;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public async Task<ActionResult<IEnumerable<MaterialDto>>> GetMaterials()
|
||||
public async Task<ActionResult<List<MaterialDto>>> GetMaterials(
|
||||
[FromQuery] bool includeInactive = false,
|
||||
[FromQuery] string? shape = null)
|
||||
{
|
||||
var materials = await _context.Materials
|
||||
.Where(m => m.IsActive)
|
||||
.OrderBy(m => m.Shape)
|
||||
.ThenBy(m => m.SortOrder)
|
||||
.ThenBy(m => m.Size)
|
||||
.Select(m => new MaterialDto
|
||||
{
|
||||
Id = m.Id,
|
||||
Shape = m.Shape.GetDisplayName(),
|
||||
Size = m.Size,
|
||||
Description = m.Description
|
||||
})
|
||||
.ToListAsync();
|
||||
List<Material> materials;
|
||||
|
||||
return Ok(materials);
|
||||
if (!string.IsNullOrWhiteSpace(shape))
|
||||
{
|
||||
var parsedShape = MaterialShapeExtensions.ParseShape(shape);
|
||||
if (!parsedShape.HasValue)
|
||||
return BadRequest($"Unknown shape: {shape}");
|
||||
|
||||
materials = await _materialService.GetByShapeAsync(parsedShape.Value, includeInactive);
|
||||
}
|
||||
else
|
||||
{
|
||||
materials = await _materialService.GetAllAsync(includeInactive);
|
||||
}
|
||||
|
||||
return Ok(materials.Select(MapToDto).ToList());
|
||||
}
|
||||
|
||||
[HttpGet("{id}")]
|
||||
public async Task<ActionResult<MaterialDto>> GetMaterial(int id)
|
||||
{
|
||||
var material = await _context.Materials.FindAsync(id);
|
||||
|
||||
if (material == null || !material.IsActive)
|
||||
var material = await _materialService.GetByIdAsync(id);
|
||||
if (material == null)
|
||||
return NotFound();
|
||||
|
||||
return Ok(new MaterialDto
|
||||
{
|
||||
Id = material.Id,
|
||||
Shape = material.Shape.GetDisplayName(),
|
||||
Size = material.Size,
|
||||
Description = material.Description
|
||||
});
|
||||
return Ok(MapToDto(material));
|
||||
}
|
||||
|
||||
[HttpGet("by-shape/{shape}")]
|
||||
public async Task<ActionResult<List<MaterialDto>>> GetByShape(string shape, [FromQuery] bool includeInactive = false)
|
||||
{
|
||||
var parsedShape = MaterialShapeExtensions.ParseShape(shape);
|
||||
if (!parsedShape.HasValue)
|
||||
return BadRequest($"Unknown shape: {shape}");
|
||||
|
||||
var materials = await _materialService.GetByShapeAsync(parsedShape.Value, includeInactive);
|
||||
return Ok(materials.Select(MapToDto).ToList());
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
@@ -59,38 +66,53 @@ public class MaterialsController : ControllerBase
|
||||
if (string.IsNullOrWhiteSpace(dto.Shape))
|
||||
return BadRequest("Shape is required");
|
||||
|
||||
if (string.IsNullOrWhiteSpace(dto.Size))
|
||||
return BadRequest("Size is required");
|
||||
|
||||
var parsedShape = MaterialShapeExtensions.ParseShape(dto.Shape);
|
||||
if (!parsedShape.HasValue)
|
||||
return BadRequest($"Unknown shape: {dto.Shape}");
|
||||
|
||||
// Check for duplicates
|
||||
var exists = await _context.Materials
|
||||
.AnyAsync(m => m.Shape == parsedShape.Value && m.Size == dto.Size && m.IsActive);
|
||||
|
||||
if (exists)
|
||||
return Conflict($"Material '{dto.Shape} - {dto.Size}' already exists");
|
||||
// Parse material type
|
||||
MaterialType materialType = MaterialType.Steel;
|
||||
if (!string.IsNullOrWhiteSpace(dto.Type))
|
||||
{
|
||||
if (!Enum.TryParse<MaterialType>(dto.Type, true, out materialType))
|
||||
return BadRequest($"Unknown material type: {dto.Type}");
|
||||
}
|
||||
|
||||
var material = new Material
|
||||
{
|
||||
Shape = parsedShape.Value,
|
||||
Size = dto.Size,
|
||||
Description = dto.Description,
|
||||
CreatedAt = DateTime.UtcNow
|
||||
Type = materialType,
|
||||
Grade = dto.Grade,
|
||||
Size = dto.Size ?? string.Empty,
|
||||
Description = dto.Description
|
||||
};
|
||||
|
||||
_context.Materials.Add(material);
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
return CreatedAtAction(nameof(GetMaterial), new { id = material.Id }, new MaterialDto
|
||||
if (dto.Dimensions != null && dto.Dimensions.Count > 0)
|
||||
{
|
||||
Id = material.Id,
|
||||
Shape = material.Shape.GetDisplayName(),
|
||||
Size = material.Size,
|
||||
Description = material.Description
|
||||
});
|
||||
var dimensions = MaterialService.CreateDimensionsForShape(parsedShape.Value);
|
||||
ApplyDimensionValues(dimensions, dto.Dimensions);
|
||||
|
||||
// Check for duplicates using generated size
|
||||
var generatedSize = dimensions.GenerateSizeString();
|
||||
var exists = await _materialService.ExistsAsync(parsedShape.Value, generatedSize);
|
||||
if (exists)
|
||||
return Conflict($"Material '{parsedShape.Value.GetDisplayName()} - {generatedSize}' already exists");
|
||||
|
||||
var created = await _materialService.CreateWithDimensionsAsync(material, dimensions);
|
||||
return CreatedAtAction(nameof(GetMaterial), new { id = created.Id }, MapToDto(created));
|
||||
}
|
||||
else
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(material.Size))
|
||||
return BadRequest("Size is required when dimensions are not provided");
|
||||
|
||||
var exists = await _materialService.ExistsAsync(parsedShape.Value, material.Size);
|
||||
if (exists)
|
||||
return Conflict($"Material '{parsedShape.Value.GetDisplayName()} - {material.Size}' already exists");
|
||||
|
||||
var created = await _materialService.CreateAsync(material);
|
||||
return CreatedAtAction(nameof(GetMaterial), new { id = created.Id }, MapToDto(created));
|
||||
}
|
||||
}
|
||||
|
||||
[HttpPost("bulk")]
|
||||
@@ -102,9 +124,9 @@ public class MaterialsController : ControllerBase
|
||||
|
||||
foreach (var dto in materials)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(dto.Shape) || string.IsNullOrWhiteSpace(dto.Size))
|
||||
if (string.IsNullOrWhiteSpace(dto.Shape))
|
||||
{
|
||||
errors.Add($"Invalid material: Shape and Size are required");
|
||||
errors.Add("Invalid material: Shape is required");
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -115,27 +137,61 @@ public class MaterialsController : ControllerBase
|
||||
continue;
|
||||
}
|
||||
|
||||
var exists = await _context.Materials
|
||||
.AnyAsync(m => m.Shape == parsedShape.Value && m.Size == dto.Size && m.IsActive);
|
||||
var size = dto.Size ?? string.Empty;
|
||||
|
||||
if (dto.Dimensions != null && dto.Dimensions.Count > 0)
|
||||
{
|
||||
var dimensions = MaterialService.CreateDimensionsForShape(parsedShape.Value);
|
||||
ApplyDimensionValues(dimensions, dto.Dimensions);
|
||||
size = dimensions.GenerateSizeString();
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(size))
|
||||
{
|
||||
errors.Add($"Size is required for {dto.Shape}");
|
||||
continue;
|
||||
}
|
||||
|
||||
var exists = await _materialService.ExistsAsync(parsedShape.Value, size);
|
||||
if (exists)
|
||||
{
|
||||
skipped++;
|
||||
continue;
|
||||
}
|
||||
|
||||
_context.Materials.Add(new Material
|
||||
MaterialType materialType = MaterialType.Steel;
|
||||
if (!string.IsNullOrWhiteSpace(dto.Type))
|
||||
{
|
||||
if (!Enum.TryParse<MaterialType>(dto.Type, true, out materialType))
|
||||
{
|
||||
errors.Add($"Unknown material type: {dto.Type}");
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
var material = new Material
|
||||
{
|
||||
Shape = parsedShape.Value,
|
||||
Size = dto.Size,
|
||||
Description = dto.Description,
|
||||
CreatedAt = DateTime.UtcNow
|
||||
});
|
||||
Type = materialType,
|
||||
Grade = dto.Grade,
|
||||
Size = size,
|
||||
Description = dto.Description
|
||||
};
|
||||
|
||||
if (dto.Dimensions != null && dto.Dimensions.Count > 0)
|
||||
{
|
||||
var dimensions = MaterialService.CreateDimensionsForShape(parsedShape.Value);
|
||||
ApplyDimensionValues(dimensions, dto.Dimensions);
|
||||
await _materialService.CreateWithDimensionsAsync(material, dimensions);
|
||||
}
|
||||
else
|
||||
{
|
||||
await _materialService.CreateAsync(material);
|
||||
}
|
||||
|
||||
created++;
|
||||
}
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
return Ok(new BulkCreateResult
|
||||
{
|
||||
Created = created,
|
||||
@@ -144,39 +200,192 @@ public class MaterialsController : ControllerBase
|
||||
});
|
||||
}
|
||||
|
||||
[HttpDelete("{id}")]
|
||||
public async Task<IActionResult> DeleteMaterial(int id)
|
||||
[HttpPut("{id}")]
|
||||
public async Task<ActionResult<MaterialDto>> UpdateMaterial(int id, UpdateMaterialDto dto)
|
||||
{
|
||||
var material = await _context.Materials.FindAsync(id);
|
||||
|
||||
var material = await _materialService.GetByIdAsync(id);
|
||||
if (material == null)
|
||||
return NotFound();
|
||||
|
||||
material.IsActive = false;
|
||||
await _context.SaveChangesAsync();
|
||||
if (dto.Type != null)
|
||||
{
|
||||
if (!Enum.TryParse<MaterialType>(dto.Type, true, out var materialType))
|
||||
return BadRequest($"Unknown material type: {dto.Type}");
|
||||
material.Type = materialType;
|
||||
}
|
||||
|
||||
if (dto.Grade != null) material.Grade = dto.Grade;
|
||||
if (dto.Size != null) material.Size = dto.Size;
|
||||
if (dto.Description != null) material.Description = dto.Description;
|
||||
|
||||
if (dto.Dimensions != null && dto.Dimensions.Count > 0)
|
||||
{
|
||||
var dimensions = material.Dimensions ?? MaterialService.CreateDimensionsForShape(material.Shape);
|
||||
ApplyDimensionValues(dimensions, dto.Dimensions);
|
||||
await _materialService.UpdateWithDimensionsAsync(material, dimensions, dto.RegenerateSize ?? false);
|
||||
}
|
||||
else
|
||||
{
|
||||
await _materialService.UpdateAsync(material);
|
||||
}
|
||||
|
||||
var updated = await _materialService.GetByIdAsync(id);
|
||||
return Ok(MapToDto(updated!));
|
||||
}
|
||||
|
||||
[HttpDelete("{id}")]
|
||||
public async Task<IActionResult> DeleteMaterial(int id)
|
||||
{
|
||||
var material = await _materialService.GetByIdAsync(id);
|
||||
if (material == null)
|
||||
return NotFound();
|
||||
|
||||
await _materialService.DeleteAsync(id);
|
||||
return NoContent();
|
||||
}
|
||||
}
|
||||
|
||||
public class MaterialDto
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string Shape { get; set; } = string.Empty;
|
||||
public string Size { get; set; } = string.Empty;
|
||||
public string? Description { get; set; }
|
||||
}
|
||||
[HttpPost("search")]
|
||||
public async Task<ActionResult<List<MaterialDto>>> SearchMaterials(MaterialSearchDto dto)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(dto.Shape))
|
||||
return BadRequest("Shape is required");
|
||||
|
||||
public class CreateMaterialDto
|
||||
{
|
||||
public string Shape { get; set; } = string.Empty;
|
||||
public string Size { get; set; } = string.Empty;
|
||||
public string? Description { get; set; }
|
||||
}
|
||||
var parsedShape = MaterialShapeExtensions.ParseShape(dto.Shape);
|
||||
if (!parsedShape.HasValue)
|
||||
return BadRequest($"Unknown shape: {dto.Shape}");
|
||||
|
||||
public class BulkCreateResult
|
||||
{
|
||||
public int Created { get; set; }
|
||||
public int Skipped { get; set; }
|
||||
public List<string> Errors { get; set; } = new();
|
||||
var results = parsedShape.Value switch
|
||||
{
|
||||
MaterialShape.RoundBar => await _materialService.SearchRoundBarByDiameterAsync(dto.TargetValue, dto.Tolerance),
|
||||
MaterialShape.RoundTube => await _materialService.SearchRoundTubeByODAsync(dto.TargetValue, dto.Tolerance),
|
||||
MaterialShape.FlatBar => await _materialService.SearchFlatBarByWidthAsync(dto.TargetValue, dto.Tolerance),
|
||||
MaterialShape.SquareBar => await _materialService.SearchSquareBarBySizeAsync(dto.TargetValue, dto.Tolerance),
|
||||
MaterialShape.SquareTube => await _materialService.SearchSquareTubeBySizeAsync(dto.TargetValue, dto.Tolerance),
|
||||
MaterialShape.RectangularTube => await _materialService.SearchRectangularTubeByWidthAsync(dto.TargetValue, dto.Tolerance),
|
||||
MaterialShape.Angle => await _materialService.SearchAngleByLegAsync(dto.TargetValue, dto.Tolerance),
|
||||
MaterialShape.Channel => await _materialService.SearchChannelByHeightAsync(dto.TargetValue, dto.Tolerance),
|
||||
MaterialShape.IBeam => await _materialService.SearchIBeamByHeightAsync(dto.TargetValue, dto.Tolerance),
|
||||
MaterialShape.Pipe => await _materialService.SearchPipeByNominalSizeAsync(dto.TargetValue, dto.Tolerance),
|
||||
_ => new List<Material>()
|
||||
};
|
||||
|
||||
return Ok(results.Select(MapToDto).ToList());
|
||||
}
|
||||
|
||||
private static MaterialDto MapToDto(Material m) => new()
|
||||
{
|
||||
Id = m.Id,
|
||||
Shape = m.Shape.GetDisplayName(),
|
||||
Type = m.Type.ToString(),
|
||||
Grade = m.Grade,
|
||||
Size = m.Size,
|
||||
Description = m.Description,
|
||||
IsActive = m.IsActive,
|
||||
Dimensions = m.Dimensions != null ? MapDimensionsToDto(m.Dimensions) : null
|
||||
};
|
||||
|
||||
private static MaterialDimensionsDto MapDimensionsToDto(MaterialDimensions d)
|
||||
{
|
||||
var dto = new MaterialDimensionsDto
|
||||
{
|
||||
DimensionType = d.GetType().Name.Replace("Dimensions", "")
|
||||
};
|
||||
|
||||
// Extract dimension values based on type
|
||||
switch (d)
|
||||
{
|
||||
case RoundBarDimensions rb:
|
||||
dto.Values["Diameter"] = rb.Diameter;
|
||||
break;
|
||||
case RoundTubeDimensions rt:
|
||||
dto.Values["OuterDiameter"] = rt.OuterDiameter;
|
||||
dto.Values["Wall"] = rt.Wall;
|
||||
break;
|
||||
case FlatBarDimensions fb:
|
||||
dto.Values["Width"] = fb.Width;
|
||||
dto.Values["Thickness"] = fb.Thickness;
|
||||
break;
|
||||
case SquareBarDimensions sb:
|
||||
dto.Values["Size"] = sb.Size;
|
||||
break;
|
||||
case SquareTubeDimensions st:
|
||||
dto.Values["Size"] = st.Size;
|
||||
dto.Values["Wall"] = st.Wall;
|
||||
break;
|
||||
case RectangularTubeDimensions rect:
|
||||
dto.Values["Width"] = rect.Width;
|
||||
dto.Values["Height"] = rect.Height;
|
||||
dto.Values["Wall"] = rect.Wall;
|
||||
break;
|
||||
case AngleDimensions a:
|
||||
dto.Values["Leg1"] = a.Leg1;
|
||||
dto.Values["Leg2"] = a.Leg2;
|
||||
dto.Values["Thickness"] = a.Thickness;
|
||||
break;
|
||||
case ChannelDimensions c:
|
||||
dto.Values["Height"] = c.Height;
|
||||
dto.Values["Flange"] = c.Flange;
|
||||
dto.Values["Web"] = c.Web;
|
||||
break;
|
||||
case IBeamDimensions ib:
|
||||
dto.Values["Height"] = ib.Height;
|
||||
dto.Values["WeightPerFoot"] = ib.WeightPerFoot;
|
||||
break;
|
||||
case PipeDimensions p:
|
||||
dto.Values["NominalSize"] = p.NominalSize;
|
||||
if (p.Wall.HasValue) dto.Values["Wall"] = p.Wall.Value;
|
||||
break;
|
||||
}
|
||||
|
||||
return dto;
|
||||
}
|
||||
|
||||
private static void ApplyDimensionValues(MaterialDimensions dimensions, Dictionary<string, decimal> values)
|
||||
{
|
||||
switch (dimensions)
|
||||
{
|
||||
case RoundBarDimensions rb:
|
||||
if (values.TryGetValue("Diameter", out var diameter)) rb.Diameter = diameter;
|
||||
break;
|
||||
case RoundTubeDimensions rt:
|
||||
if (values.TryGetValue("OuterDiameter", out var od)) rt.OuterDiameter = od;
|
||||
if (values.TryGetValue("Wall", out var rtWall)) rt.Wall = rtWall;
|
||||
break;
|
||||
case FlatBarDimensions fb:
|
||||
if (values.TryGetValue("Width", out var fbWidth)) fb.Width = fbWidth;
|
||||
if (values.TryGetValue("Thickness", out var fbThick)) fb.Thickness = fbThick;
|
||||
break;
|
||||
case SquareBarDimensions sb:
|
||||
if (values.TryGetValue("Size", out var sbSize)) sb.Size = sbSize;
|
||||
break;
|
||||
case SquareTubeDimensions st:
|
||||
if (values.TryGetValue("Size", out var stSize)) st.Size = stSize;
|
||||
if (values.TryGetValue("Wall", out var stWall)) st.Wall = stWall;
|
||||
break;
|
||||
case RectangularTubeDimensions rect:
|
||||
if (values.TryGetValue("Width", out var rectWidth)) rect.Width = rectWidth;
|
||||
if (values.TryGetValue("Height", out var rectHeight)) rect.Height = rectHeight;
|
||||
if (values.TryGetValue("Wall", out var rectWall)) rect.Wall = rectWall;
|
||||
break;
|
||||
case AngleDimensions a:
|
||||
if (values.TryGetValue("Leg1", out var leg1)) a.Leg1 = leg1;
|
||||
if (values.TryGetValue("Leg2", out var leg2)) a.Leg2 = leg2;
|
||||
if (values.TryGetValue("Thickness", out var aThick)) a.Thickness = aThick;
|
||||
break;
|
||||
case ChannelDimensions c:
|
||||
if (values.TryGetValue("Height", out var cHeight)) c.Height = cHeight;
|
||||
if (values.TryGetValue("Flange", out var flange)) c.Flange = flange;
|
||||
if (values.TryGetValue("Web", out var web)) c.Web = web;
|
||||
break;
|
||||
case IBeamDimensions ib:
|
||||
if (values.TryGetValue("Height", out var ibHeight)) ib.Height = ibHeight;
|
||||
if (values.TryGetValue("WeightPerFoot", out var weight)) ib.WeightPerFoot = weight;
|
||||
break;
|
||||
case PipeDimensions p:
|
||||
if (values.TryGetValue("NominalSize", out var nps)) p.NominalSize = nps;
|
||||
if (values.TryGetValue("Wall", out var pWall)) p.Wall = pWall;
|
||||
if (values.TryGetValue("Schedule", out var schedule)) p.Schedule = schedule.ToString();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user