Replace direct EF Core/DbContext usage in MCP tools with HTTP calls to the CutList.Web REST API via new ApiClient. Removes CutList.Web project reference from MCP, adds Microsoft.Extensions.Http instead. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
817 lines
31 KiB
C#
817 lines
31 KiB
C#
using System.ComponentModel;
|
|
using CutList.Core.Formatting;
|
|
using ModelContextProtocol.Server;
|
|
|
|
namespace CutList.Mcp;
|
|
|
|
/// <summary>
|
|
/// MCP tools for inventory management - suppliers, materials, stock items, and offerings.
|
|
/// All calls go through the CutList.Web REST API via ApiClient.
|
|
/// </summary>
|
|
[McpServerToolType]
|
|
public class InventoryTools
|
|
{
|
|
private readonly ApiClient _api;
|
|
|
|
public InventoryTools(ApiClient api)
|
|
{
|
|
_api = api;
|
|
}
|
|
|
|
#region Suppliers
|
|
|
|
[McpServerTool(Name = "list_suppliers"), Description("Lists all suppliers in the system.")]
|
|
public async Task<SupplierListResult> ListSuppliers(
|
|
[Description("Include inactive suppliers (default false)")]
|
|
bool includeInactive = false)
|
|
{
|
|
var suppliers = await _api.GetSuppliersAsync(includeInactive);
|
|
|
|
return new SupplierListResult
|
|
{
|
|
Success = true,
|
|
Suppliers = suppliers.Select(s => new SupplierDto
|
|
{
|
|
Id = s.Id,
|
|
Name = s.Name,
|
|
ContactInfo = s.ContactInfo,
|
|
Notes = s.Notes,
|
|
IsActive = s.IsActive
|
|
}).ToList()
|
|
};
|
|
}
|
|
|
|
[McpServerTool(Name = "add_supplier"), Description("Adds a new supplier to the system.")]
|
|
public async Task<SupplierResult> AddSupplier(
|
|
[Description("Supplier name (e.g., 'O'Neal Steel')")]
|
|
string name,
|
|
[Description("Contact info - website, phone, email, etc.")]
|
|
string? contactInfo = null,
|
|
[Description("Notes about the supplier")]
|
|
string? notes = null)
|
|
{
|
|
var supplier = await _api.CreateSupplierAsync(name, contactInfo, notes);
|
|
|
|
return new SupplierResult
|
|
{
|
|
Success = true,
|
|
Supplier = supplier != null ? new SupplierDto
|
|
{
|
|
Id = supplier.Id,
|
|
Name = supplier.Name,
|
|
ContactInfo = supplier.ContactInfo,
|
|
Notes = supplier.Notes,
|
|
IsActive = supplier.IsActive
|
|
} : null
|
|
};
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Materials
|
|
|
|
[McpServerTool(Name = "list_materials"), Description("Lists all materials (shape/size combinations) in the system.")]
|
|
public async Task<MaterialListResult> ListMaterials(
|
|
[Description("Filter by shape (e.g., 'Angle', 'FlatBar', 'RoundTube')")]
|
|
string? shape = null,
|
|
[Description("Include inactive materials (default false)")]
|
|
bool includeInactive = false)
|
|
{
|
|
var materials = await _api.GetMaterialsAsync(shape, includeInactive);
|
|
|
|
return new MaterialListResult
|
|
{
|
|
Success = true,
|
|
Materials = materials.Select(MapMaterial).ToList()
|
|
};
|
|
}
|
|
|
|
[McpServerTool(Name = "add_material"), Description("Adds a new material (shape/size combination) to the system with optional parsed dimensions.")]
|
|
public async Task<MaterialResult> AddMaterial(
|
|
[Description("Material shape (e.g., 'Angle', 'FlatBar', 'RoundTube', 'SquareTube', 'Channel', 'IBeam', 'Pipe')")]
|
|
string shape,
|
|
[Description("Material size string (e.g., '2 x 2 x 1/4'). If not provided, will be auto-generated from dimensions.")]
|
|
string? size = null,
|
|
[Description("Optional description")]
|
|
string? description = null,
|
|
[Description("Diameter in inches (for Round Bar)")]
|
|
double? diameter = null,
|
|
[Description("Outer diameter in inches (for Round Tube)")]
|
|
double? outerDiameter = null,
|
|
[Description("Width in inches (for Flat Bar, Rectangular Tube)")]
|
|
double? width = null,
|
|
[Description("Height in inches (for Rectangular Tube, Channel, I-Beam)")]
|
|
double? height = null,
|
|
[Description("Size in inches (for Square Bar, Square Tube - the side length)")]
|
|
double? squareSize = null,
|
|
[Description("Thickness in inches (for Flat Bar, Angle)")]
|
|
double? thickness = null,
|
|
[Description("Wall thickness in inches (for tubes, pipe)")]
|
|
double? wall = null,
|
|
[Description("Leg 1 length in inches (for Angle)")]
|
|
double? leg1 = null,
|
|
[Description("Leg 2 length in inches (for Angle)")]
|
|
double? leg2 = null,
|
|
[Description("Flange width in inches (for Channel)")]
|
|
double? flange = null,
|
|
[Description("Web thickness in inches (for Channel)")]
|
|
double? web = null,
|
|
[Description("Weight per foot in lbs (for I-Beam)")]
|
|
double? weightPerFoot = null,
|
|
[Description("Nominal pipe size in inches (for Pipe)")]
|
|
double? nominalSize = null,
|
|
[Description("Schedule (for Pipe, e.g., '40', '80', 'STD')")]
|
|
string? schedule = null)
|
|
{
|
|
// Build dimensions dictionary from individual parameters
|
|
var dimensions = new Dictionary<string, decimal>();
|
|
if (diameter.HasValue) dimensions["Diameter"] = (decimal)diameter.Value;
|
|
if (outerDiameter.HasValue) dimensions["OuterDiameter"] = (decimal)outerDiameter.Value;
|
|
if (width.HasValue) dimensions["Width"] = (decimal)width.Value;
|
|
if (height.HasValue) dimensions["Height"] = (decimal)height.Value;
|
|
if (squareSize.HasValue) dimensions["Size"] = (decimal)squareSize.Value;
|
|
if (thickness.HasValue) dimensions["Thickness"] = (decimal)thickness.Value;
|
|
if (wall.HasValue) dimensions["Wall"] = (decimal)wall.Value;
|
|
if (leg1.HasValue) dimensions["Leg1"] = (decimal)leg1.Value;
|
|
if (leg2.HasValue) dimensions["Leg2"] = (decimal)leg2.Value;
|
|
if (flange.HasValue) dimensions["Flange"] = (decimal)flange.Value;
|
|
if (web.HasValue) dimensions["Web"] = (decimal)web.Value;
|
|
if (weightPerFoot.HasValue) dimensions["WeightPerFoot"] = (decimal)weightPerFoot.Value;
|
|
if (nominalSize.HasValue) dimensions["NominalSize"] = (decimal)nominalSize.Value;
|
|
|
|
try
|
|
{
|
|
var material = await _api.CreateMaterialAsync(
|
|
shape, size, description, null, null,
|
|
dimensions.Count > 0 ? dimensions : null);
|
|
|
|
if (material == null)
|
|
return new MaterialResult { Success = false, Error = "Failed to create material" };
|
|
|
|
return new MaterialResult
|
|
{
|
|
Success = true,
|
|
Material = MapMaterial(material)
|
|
};
|
|
}
|
|
catch (ApiConflictException ex)
|
|
{
|
|
return new MaterialResult { Success = false, Error = ex.Message };
|
|
}
|
|
catch (HttpRequestException ex)
|
|
{
|
|
return new MaterialResult { Success = false, Error = ex.Message };
|
|
}
|
|
}
|
|
|
|
[McpServerTool(Name = "search_materials"), Description("Search for materials by shape with a target dimension value and tolerance. The primary dimension for each shape is searched (e.g., diameter for RoundBar, leg size for Angle).")]
|
|
public async Task<MaterialListResult> SearchMaterials(
|
|
[Description("Shape to search (e.g., 'RoundBar', 'Angle', 'FlatBar')")]
|
|
string shape,
|
|
[Description("Target dimension value in inches (or lbs for weightPerFoot)")]
|
|
double targetValue,
|
|
[Description("Tolerance value - returns results within +/- this amount (default 0.1)")]
|
|
double tolerance = 0.1)
|
|
{
|
|
var materials = await _api.SearchMaterialsAsync(shape, (decimal)targetValue, (decimal)tolerance);
|
|
|
|
return new MaterialListResult
|
|
{
|
|
Success = true,
|
|
Materials = materials.Select(MapMaterial).ToList()
|
|
};
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Stock Items
|
|
|
|
[McpServerTool(Name = "list_stock_items"), Description("Lists stock items (material lengths available in inventory).")]
|
|
public async Task<StockItemListResult> ListStockItems(
|
|
[Description("Filter by material ID")]
|
|
int? materialId = null,
|
|
[Description("Filter by shape (e.g., 'Angle')")]
|
|
string? shape = null,
|
|
[Description("Include inactive stock items (default false)")]
|
|
bool includeInactive = false)
|
|
{
|
|
List<ApiStockItemDto> items;
|
|
|
|
if (materialId.HasValue)
|
|
{
|
|
items = await _api.GetStockItemsAsync(materialId, includeInactive);
|
|
}
|
|
else if (!string.IsNullOrEmpty(shape))
|
|
{
|
|
// Get materials for this shape, then get stock items for each
|
|
var materials = await _api.GetMaterialsAsync(shape, includeInactive);
|
|
var allItems = new List<ApiStockItemDto>();
|
|
foreach (var mat in materials)
|
|
{
|
|
var matItems = await _api.GetStockItemsAsync(mat.Id, includeInactive);
|
|
allItems.AddRange(matItems);
|
|
}
|
|
items = allItems;
|
|
}
|
|
else
|
|
{
|
|
items = await _api.GetStockItemsAsync(includeInactive: includeInactive);
|
|
}
|
|
|
|
return new StockItemListResult
|
|
{
|
|
Success = true,
|
|
StockItems = items.Select(s => new StockItemDto
|
|
{
|
|
Id = s.Id,
|
|
MaterialId = s.MaterialId,
|
|
MaterialName = s.MaterialName,
|
|
LengthInches = s.LengthInches,
|
|
LengthFormatted = s.LengthFormatted,
|
|
Name = s.Name,
|
|
QuantityOnHand = s.QuantityOnHand,
|
|
Notes = s.Notes,
|
|
IsActive = s.IsActive
|
|
}).ToList()
|
|
};
|
|
}
|
|
|
|
[McpServerTool(Name = "add_stock_item"), Description("Adds a new stock item (a specific length of material that can be stocked).")]
|
|
public async Task<StockItemResult> AddStockItem(
|
|
[Description("Material ID (use list_materials to find IDs)")]
|
|
int materialId,
|
|
[Description("Stock length (e.g., '20'', '240', '20 ft')")]
|
|
string length,
|
|
[Description("Optional name/label for this stock item")]
|
|
string? name = null,
|
|
[Description("Initial quantity on hand (default 0)")]
|
|
int quantityOnHand = 0,
|
|
[Description("Notes")]
|
|
string? notes = null)
|
|
{
|
|
try
|
|
{
|
|
var stockItem = await _api.CreateStockItemAsync(materialId, length, name, quantityOnHand, notes);
|
|
|
|
if (stockItem == null)
|
|
return new StockItemResult { Success = false, Error = "Failed to create stock item" };
|
|
|
|
return new StockItemResult
|
|
{
|
|
Success = true,
|
|
StockItem = new StockItemDto
|
|
{
|
|
Id = stockItem.Id,
|
|
MaterialId = stockItem.MaterialId,
|
|
MaterialName = stockItem.MaterialName,
|
|
LengthInches = stockItem.LengthInches,
|
|
LengthFormatted = stockItem.LengthFormatted,
|
|
Name = stockItem.Name,
|
|
QuantityOnHand = stockItem.QuantityOnHand,
|
|
Notes = stockItem.Notes,
|
|
IsActive = stockItem.IsActive
|
|
}
|
|
};
|
|
}
|
|
catch (ApiConflictException ex)
|
|
{
|
|
return new StockItemResult { Success = false, Error = ex.Message };
|
|
}
|
|
catch (HttpRequestException ex)
|
|
{
|
|
return new StockItemResult { Success = false, Error = ex.Message };
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Supplier Offerings
|
|
|
|
[McpServerTool(Name = "list_supplier_offerings"), Description("Lists supplier offerings (what suppliers sell for each stock item).")]
|
|
public async Task<SupplierOfferingListResult> ListSupplierOfferings(
|
|
[Description("Filter by supplier ID")]
|
|
int? supplierId = null,
|
|
[Description("Filter by stock item ID")]
|
|
int? stockItemId = null,
|
|
[Description("Filter by material ID")]
|
|
int? materialId = null)
|
|
{
|
|
List<ApiOfferingDto> offerings;
|
|
|
|
if (supplierId.HasValue)
|
|
{
|
|
offerings = await _api.GetOfferingsForSupplierAsync(supplierId.Value);
|
|
// Apply additional filters client-side
|
|
if (stockItemId.HasValue)
|
|
offerings = offerings.Where(o => o.StockItemId == stockItemId.Value).ToList();
|
|
if (materialId.HasValue)
|
|
{
|
|
// Need to get stock items for this material to filter
|
|
var stockItems = await _api.GetStockItemsAsync(materialId);
|
|
var stockItemIds = stockItems.Select(s => s.Id).ToHashSet();
|
|
offerings = offerings.Where(o => stockItemIds.Contains(o.StockItemId)).ToList();
|
|
}
|
|
}
|
|
else if (stockItemId.HasValue)
|
|
{
|
|
offerings = await _api.GetOfferingsForStockItemAsync(stockItemId.Value);
|
|
}
|
|
else if (materialId.HasValue)
|
|
{
|
|
// Get stock items for this material, then aggregate offerings
|
|
var stockItems = await _api.GetStockItemsAsync(materialId);
|
|
var allOfferings = new List<ApiOfferingDto>();
|
|
foreach (var si in stockItems)
|
|
{
|
|
var siOfferings = await _api.GetOfferingsForStockItemAsync(si.Id);
|
|
allOfferings.AddRange(siOfferings);
|
|
}
|
|
offerings = allOfferings;
|
|
}
|
|
else
|
|
{
|
|
// No filter - get all suppliers then aggregate
|
|
var suppliers = await _api.GetSuppliersAsync();
|
|
var allOfferings = new List<ApiOfferingDto>();
|
|
foreach (var s in suppliers)
|
|
{
|
|
var sOfferings = await _api.GetOfferingsForSupplierAsync(s.Id);
|
|
allOfferings.AddRange(sOfferings);
|
|
}
|
|
offerings = allOfferings;
|
|
}
|
|
|
|
return new SupplierOfferingListResult
|
|
{
|
|
Success = true,
|
|
Offerings = offerings.Select(o => new SupplierOfferingDto
|
|
{
|
|
Id = o.Id,
|
|
SupplierId = o.SupplierId,
|
|
SupplierName = o.SupplierName ?? string.Empty,
|
|
StockItemId = o.StockItemId,
|
|
MaterialName = o.MaterialName ?? string.Empty,
|
|
LengthFormatted = o.LengthFormatted ?? string.Empty,
|
|
PartNumber = o.PartNumber,
|
|
SupplierDescription = o.SupplierDescription,
|
|
Price = o.Price,
|
|
Notes = o.Notes
|
|
}).ToList()
|
|
};
|
|
}
|
|
|
|
[McpServerTool(Name = "add_supplier_offering"), Description("Adds a supplier offering - links a supplier to a stock item with their part number and pricing.")]
|
|
public async Task<SupplierOfferingResult> AddSupplierOffering(
|
|
[Description("Supplier ID (use list_suppliers to find)")]
|
|
int supplierId,
|
|
[Description("Stock item ID (use list_stock_items to find)")]
|
|
int stockItemId,
|
|
[Description("Supplier's part number")]
|
|
string? partNumber = null,
|
|
[Description("Supplier's description of the item")]
|
|
string? supplierDescription = null,
|
|
[Description("Price per unit")]
|
|
decimal? price = null,
|
|
[Description("Notes")]
|
|
string? notes = null)
|
|
{
|
|
try
|
|
{
|
|
var offering = await _api.CreateOfferingAsync(supplierId, stockItemId, partNumber, supplierDescription, price, notes);
|
|
|
|
if (offering == null)
|
|
return new SupplierOfferingResult { Success = false, Error = "Failed to create offering" };
|
|
|
|
return new SupplierOfferingResult
|
|
{
|
|
Success = true,
|
|
Offering = new SupplierOfferingDto
|
|
{
|
|
Id = offering.Id,
|
|
SupplierId = offering.SupplierId,
|
|
SupplierName = offering.SupplierName ?? string.Empty,
|
|
StockItemId = offering.StockItemId,
|
|
MaterialName = offering.MaterialName ?? string.Empty,
|
|
LengthFormatted = offering.LengthFormatted ?? string.Empty,
|
|
PartNumber = offering.PartNumber,
|
|
SupplierDescription = offering.SupplierDescription,
|
|
Price = offering.Price,
|
|
Notes = offering.Notes
|
|
}
|
|
};
|
|
}
|
|
catch (ApiConflictException ex)
|
|
{
|
|
return new SupplierOfferingResult { Success = false, Error = ex.Message };
|
|
}
|
|
catch (HttpRequestException ex)
|
|
{
|
|
return new SupplierOfferingResult { Success = false, Error = ex.Message };
|
|
}
|
|
}
|
|
|
|
[McpServerTool(Name = "add_stock_with_offering"), Description("Convenience method: adds a material (if needed), stock item (if needed), and supplier offering all at once.")]
|
|
public async Task<AddStockWithOfferingResult> AddStockWithOffering(
|
|
[Description("Supplier ID (use list_suppliers or add_supplier first)")]
|
|
int supplierId,
|
|
[Description("Material shape (e.g., 'Angle', 'FlatBar')")]
|
|
string shape,
|
|
[Description("Material size (e.g., '2 x 2 x 1/4')")]
|
|
string size,
|
|
[Description("Stock length (e.g., '20'', '240')")]
|
|
string length,
|
|
[Description("Material type: Steel, Aluminum, Stainless, Brass, Copper (default: Steel)")]
|
|
string type = "Steel",
|
|
[Description("Grade or specification (e.g., 'A36', 'Hot Roll', '304', '6061-T6')")]
|
|
string? grade = null,
|
|
[Description("Supplier's part number")]
|
|
string? partNumber = null,
|
|
[Description("Supplier's description")]
|
|
string? supplierDescription = null,
|
|
[Description("Price per unit")]
|
|
decimal? price = null)
|
|
{
|
|
// Parse length for formatted display
|
|
double lengthInches;
|
|
try
|
|
{
|
|
lengthInches = double.TryParse(length.Trim(), out var plain)
|
|
? plain
|
|
: ArchUnits.ParseToInches(length);
|
|
}
|
|
catch
|
|
{
|
|
return new AddStockWithOfferingResult
|
|
{
|
|
Success = false,
|
|
Error = $"Could not parse length: {length}"
|
|
};
|
|
}
|
|
|
|
// Step 1: Find or create material
|
|
bool materialCreated = false;
|
|
ApiMaterialDto? material = null;
|
|
|
|
// Search for existing material by shape and size
|
|
var materials = await _api.GetMaterialsAsync(shape);
|
|
material = materials.FirstOrDefault(m =>
|
|
m.Size.Equals(size, StringComparison.OrdinalIgnoreCase) &&
|
|
m.Type.Equals(type, StringComparison.OrdinalIgnoreCase) &&
|
|
string.Equals(m.Grade, grade, StringComparison.OrdinalIgnoreCase));
|
|
|
|
if (material == null)
|
|
{
|
|
// Parse dimensions from size string for the API
|
|
var dimensions = ParseSizeStringToDimensions(shape, size);
|
|
|
|
try
|
|
{
|
|
material = await _api.CreateMaterialAsync(shape, size, null, type, grade, dimensions);
|
|
materialCreated = true;
|
|
}
|
|
catch (ApiConflictException)
|
|
{
|
|
// Race condition - material was created between check and create, re-fetch
|
|
materials = await _api.GetMaterialsAsync(shape);
|
|
material = materials.FirstOrDefault(m =>
|
|
m.Size.Equals(size, StringComparison.OrdinalIgnoreCase));
|
|
}
|
|
catch (HttpRequestException ex)
|
|
{
|
|
return new AddStockWithOfferingResult { Success = false, Error = $"Failed to create material: {ex.Message}" };
|
|
}
|
|
}
|
|
|
|
if (material == null)
|
|
return new AddStockWithOfferingResult { Success = false, Error = "Failed to find or create material" };
|
|
|
|
// Step 2: Find or create stock item
|
|
bool stockItemCreated = false;
|
|
var stockItems = await _api.GetStockItemsAsync(material.Id);
|
|
var stockItem = stockItems.FirstOrDefault(s => Math.Abs((double)s.LengthInches - lengthInches) < 0.01);
|
|
|
|
if (stockItem == null)
|
|
{
|
|
try
|
|
{
|
|
stockItem = await _api.CreateStockItemAsync(material.Id, length, null, 0, null);
|
|
stockItemCreated = true;
|
|
}
|
|
catch (ApiConflictException)
|
|
{
|
|
// Race condition - re-fetch
|
|
stockItems = await _api.GetStockItemsAsync(material.Id);
|
|
stockItem = stockItems.FirstOrDefault(s => Math.Abs((double)s.LengthInches - lengthInches) < 0.01);
|
|
}
|
|
catch (HttpRequestException ex)
|
|
{
|
|
return new AddStockWithOfferingResult
|
|
{
|
|
Success = false,
|
|
Error = $"Failed to create stock item: {ex.Message}",
|
|
MaterialCreated = materialCreated
|
|
};
|
|
}
|
|
}
|
|
|
|
if (stockItem == null)
|
|
return new AddStockWithOfferingResult
|
|
{
|
|
Success = false,
|
|
Error = "Failed to find or create stock item",
|
|
MaterialCreated = materialCreated
|
|
};
|
|
|
|
// Step 3: Create offering
|
|
try
|
|
{
|
|
var offering = await _api.CreateOfferingAsync(supplierId, stockItem.Id, partNumber, supplierDescription, price, null);
|
|
|
|
return new AddStockWithOfferingResult
|
|
{
|
|
Success = true,
|
|
MaterialId = material.Id,
|
|
MaterialName = $"{material.Shape} - {material.Size}",
|
|
MaterialCreated = materialCreated,
|
|
StockItemId = stockItem.Id,
|
|
StockItemCreated = stockItemCreated,
|
|
LengthFormatted = ArchUnits.FormatFromInches(lengthInches),
|
|
OfferingId = offering?.Id ?? 0,
|
|
PartNumber = partNumber,
|
|
SupplierDescription = supplierDescription,
|
|
Price = price
|
|
};
|
|
}
|
|
catch (ApiConflictException)
|
|
{
|
|
return new AddStockWithOfferingResult
|
|
{
|
|
Success = false,
|
|
Error = $"Offering for this supplier and stock item already exists",
|
|
MaterialCreated = materialCreated,
|
|
StockItemCreated = stockItemCreated
|
|
};
|
|
}
|
|
catch (HttpRequestException ex)
|
|
{
|
|
return new AddStockWithOfferingResult
|
|
{
|
|
Success = false,
|
|
Error = $"Failed to create offering: {ex.Message}",
|
|
MaterialCreated = materialCreated,
|
|
StockItemCreated = stockItemCreated
|
|
};
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Helpers
|
|
|
|
private static MaterialDto MapMaterial(ApiMaterialDto m)
|
|
{
|
|
var dto = new MaterialDto
|
|
{
|
|
Id = m.Id,
|
|
Shape = m.Shape,
|
|
Size = m.Size,
|
|
Description = m.Description,
|
|
DisplayName = $"{m.Shape} - {m.Size}",
|
|
IsActive = m.IsActive,
|
|
};
|
|
|
|
if (m.Dimensions != null)
|
|
{
|
|
dto.Dimensions = MapDimensions(m.Dimensions);
|
|
}
|
|
|
|
return dto;
|
|
}
|
|
|
|
private static MaterialDimensionsDto MapDimensions(ApiMaterialDimensionsDto d)
|
|
{
|
|
var dto = new MaterialDimensionsDto();
|
|
var v = d.Values;
|
|
|
|
if (v.TryGetValue("Diameter", out var diameter)) dto.Diameter = (double)diameter;
|
|
if (v.TryGetValue("OuterDiameter", out var od)) dto.OuterDiameter = (double)od;
|
|
if (v.TryGetValue("Width", out var width)) dto.Width = (double)width;
|
|
if (v.TryGetValue("Height", out var height)) dto.Height = (double)height;
|
|
if (v.TryGetValue("Size", out var size)) dto.Size = (double)size;
|
|
if (v.TryGetValue("Thickness", out var thickness)) dto.Thickness = (double)thickness;
|
|
if (v.TryGetValue("Wall", out var wall)) dto.Wall = (double)wall;
|
|
if (v.TryGetValue("Leg1", out var leg1)) dto.Leg1 = (double)leg1;
|
|
if (v.TryGetValue("Leg2", out var leg2)) dto.Leg2 = (double)leg2;
|
|
if (v.TryGetValue("Flange", out var flange)) dto.Flange = (double)flange;
|
|
if (v.TryGetValue("Web", out var web)) dto.Web = (double)web;
|
|
if (v.TryGetValue("WeightPerFoot", out var wpf)) dto.WeightPerFoot = (double)wpf;
|
|
if (v.TryGetValue("NominalSize", out var ns)) dto.NominalSize = (double)ns;
|
|
|
|
return dto;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Parses a size string into a dimensions dictionary based on shape.
|
|
/// Format: values separated by 'x' (e.g., "1 1/2 x 1/8", "2 x 2 x 1/4")
|
|
/// </summary>
|
|
private static Dictionary<string, decimal>? ParseSizeStringToDimensions(string shape, string sizeString)
|
|
{
|
|
var parts = sizeString.Split('x', StringSplitOptions.TrimEntries)
|
|
.Select(ParseDimension)
|
|
.ToArray();
|
|
|
|
if (parts.Length == 0 || parts.All(p => !p.HasValue))
|
|
return null;
|
|
|
|
decimal D(int i) => (decimal)parts[i]!.Value;
|
|
|
|
var shapeLower = shape.ToLowerInvariant().Replace(" ", "");
|
|
|
|
return shapeLower switch
|
|
{
|
|
"roundbar" when parts.Length >= 1 && parts[0].HasValue
|
|
=> new Dictionary<string, decimal> { ["Diameter"] = D(0) },
|
|
|
|
"roundtube" when parts.Length >= 2 && parts[0].HasValue && parts[1].HasValue
|
|
=> new Dictionary<string, decimal> { ["OuterDiameter"] = D(0), ["Wall"] = D(1) },
|
|
|
|
"flatbar" when parts.Length >= 2 && parts[0].HasValue && parts[1].HasValue
|
|
=> new Dictionary<string, decimal> { ["Width"] = D(0), ["Thickness"] = D(1) },
|
|
|
|
"squarebar" when parts.Length >= 1 && parts[0].HasValue
|
|
=> new Dictionary<string, decimal> { ["Size"] = D(0) },
|
|
|
|
"squaretube" when parts.Length >= 2 && parts[0].HasValue && parts[1].HasValue
|
|
=> new Dictionary<string, decimal> { ["Size"] = D(0), ["Wall"] = D(1) },
|
|
|
|
"rectangulartube" or "recttube" when parts.Length >= 3 && parts[0].HasValue && parts[1].HasValue && parts[2].HasValue
|
|
=> new Dictionary<string, decimal> { ["Width"] = D(0), ["Height"] = D(1), ["Wall"] = D(2) },
|
|
|
|
"angle" when parts.Length >= 3 && parts[0].HasValue && parts[1].HasValue && parts[2].HasValue
|
|
=> new Dictionary<string, decimal> { ["Leg1"] = D(0), ["Leg2"] = D(1), ["Thickness"] = D(2) },
|
|
|
|
"channel" when parts.Length >= 3 && parts[0].HasValue && parts[1].HasValue && parts[2].HasValue
|
|
=> new Dictionary<string, decimal> { ["Height"] = D(0), ["Flange"] = D(1), ["Web"] = D(2) },
|
|
|
|
"ibeam" when parts.Length >= 2 && parts[0].HasValue && parts[1].HasValue
|
|
=> new Dictionary<string, decimal> { ["Height"] = D(0), ["WeightPerFoot"] = D(1) },
|
|
|
|
"pipe" when parts.Length >= 1 && parts[0].HasValue
|
|
=> new Dictionary<string, decimal> { ["NominalSize"] = D(0) },
|
|
|
|
_ => null
|
|
};
|
|
|
|
static double? ParseDimension(string value)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(value)) return null;
|
|
var processed = Fraction.ReplaceFractionsWithDecimals(value.Trim());
|
|
return processed.Split(' ', StringSplitOptions.RemoveEmptyEntries)
|
|
.Sum(part => double.TryParse(part, out var d) ? d : 0) is > 0 and var total ? total : null;
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
}
|
|
|
|
#region DTOs
|
|
|
|
public class SupplierDto
|
|
{
|
|
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 SupplierListResult
|
|
{
|
|
public bool Success { get; set; }
|
|
public string? Error { get; set; }
|
|
public List<SupplierDto> Suppliers { get; set; } = new();
|
|
}
|
|
|
|
public class SupplierResult
|
|
{
|
|
public bool Success { get; set; }
|
|
public string? Error { get; set; }
|
|
public SupplierDto? Supplier { get; set; }
|
|
}
|
|
|
|
public class MaterialDimensionsDto
|
|
{
|
|
public double? Diameter { get; set; }
|
|
public double? OuterDiameter { get; set; }
|
|
public double? Width { get; set; }
|
|
public double? Height { get; set; }
|
|
public double? Size { get; set; }
|
|
public double? Thickness { get; set; }
|
|
public double? Wall { get; set; }
|
|
public double? Leg1 { get; set; }
|
|
public double? Leg2 { get; set; }
|
|
public double? Flange { get; set; }
|
|
public double? Web { get; set; }
|
|
public double? WeightPerFoot { get; set; }
|
|
public double? NominalSize { get; set; }
|
|
public string? Schedule { get; set; }
|
|
}
|
|
|
|
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; }
|
|
public string DisplayName { get; set; } = string.Empty;
|
|
public bool IsActive { get; set; }
|
|
public MaterialDimensionsDto? Dimensions { get; set; }
|
|
}
|
|
|
|
public class MaterialListResult
|
|
{
|
|
public bool Success { get; set; }
|
|
public string? Error { get; set; }
|
|
public List<MaterialDto> Materials { get; set; } = new();
|
|
}
|
|
|
|
public class MaterialResult
|
|
{
|
|
public bool Success { get; set; }
|
|
public string? Error { get; set; }
|
|
public MaterialDto? Material { get; set; }
|
|
}
|
|
|
|
public class StockItemDto
|
|
{
|
|
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 StockItemListResult
|
|
{
|
|
public bool Success { get; set; }
|
|
public string? Error { get; set; }
|
|
public List<StockItemDto> StockItems { get; set; } = new();
|
|
}
|
|
|
|
public class StockItemResult
|
|
{
|
|
public bool Success { get; set; }
|
|
public string? Error { get; set; }
|
|
public StockItemDto? StockItem { get; set; }
|
|
}
|
|
|
|
public class SupplierOfferingDto
|
|
{
|
|
public int Id { get; set; }
|
|
public int SupplierId { get; set; }
|
|
public string SupplierName { get; set; } = string.Empty;
|
|
public int StockItemId { get; set; }
|
|
public string MaterialName { get; set; } = string.Empty;
|
|
public string LengthFormatted { get; set; } = string.Empty;
|
|
public string? PartNumber { get; set; }
|
|
public string? SupplierDescription { get; set; }
|
|
public decimal? Price { get; set; }
|
|
public string? Notes { get; set; }
|
|
}
|
|
|
|
public class SupplierOfferingListResult
|
|
{
|
|
public bool Success { get; set; }
|
|
public string? Error { get; set; }
|
|
public List<SupplierOfferingDto> Offerings { get; set; } = new();
|
|
}
|
|
|
|
public class SupplierOfferingResult
|
|
{
|
|
public bool Success { get; set; }
|
|
public string? Error { get; set; }
|
|
public SupplierOfferingDto? Offering { get; set; }
|
|
}
|
|
|
|
public class AddStockWithOfferingResult
|
|
{
|
|
public bool Success { get; set; }
|
|
public string? Error { get; set; }
|
|
public int MaterialId { get; set; }
|
|
public string MaterialName { get; set; } = string.Empty;
|
|
public bool MaterialCreated { get; set; }
|
|
public int StockItemId { get; set; }
|
|
public bool StockItemCreated { get; set; }
|
|
public string LengthFormatted { get; set; } = string.Empty;
|
|
public int OfferingId { get; set; }
|
|
public string? PartNumber { get; set; }
|
|
public string? SupplierDescription { get; set; }
|
|
public decimal? Price { get; set; }
|
|
}
|
|
|
|
#endregion
|