Compare commits

...
7 Commits
Author SHA1 Message Date
ajandClaude Opus 4.6 48b8936266 feat: remove source column from cut list and increase cut length text size
Drop the Stock/Purchase source badge column from the results cut list
table (purchase list card remains). Bump cut length badges to fs-6 for
better readability.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 14:08:26 -04:00
ajandClaude Opus 4.6 700aa726fc fix: update MCP server base URL to port 5270 and suppress logging
Point API client at the deployed service port and clear logging
providers to prevent interference with MCP stdio transport.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 14:04:32 -04:00
ajandClaude Opus 4.6 2f2b9f71e1 feat: show inches value alongside formatted length in job editor
Display raw inches in parentheses next to the architectural format
in both Parts and Stock tables for easier reference.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 14:04:31 -04:00
ajandClaude Opus 4.6 65e0247a7d chore: default OpenFirewall to true in deploy script
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 13:48:41 -04:00
ajandClaude Opus 4.6 545678c30f feat: add error handling to job editor and Error page
Wrap job editor initialization in try/catch to display errors instead of
crashing the circuit. Make LoadSavedResults async. Add a simple Error
page for the production exception handler.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 13:48:36 -04:00
ajandClaude Opus 4.6 b5bef23c1d refactor: migrate to IDbContextFactory and Windows Service hosting
Replace AddDbContext with AddDbContextFactory for Blazor Server circuit
safety — each service method now creates a short-lived DbContext via the
factory. Configure Windows Service hosting with ContentRootPath and
remove HSTS/HTTPS redirect for reverse-proxy deployment.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 13:48:30 -04:00
ajandClaude Opus 4.6 f66fd3eaa7 chore: upgrade solution from .NET 8 to .NET 10
Update all project TFMs to net10.0, EF Core to 10.0.4, add
Microsoft.Extensions.Hosting.WindowsServices, and suppress WFO1000
warning for WinForms project.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 13:48:23 -04:00
16 changed files with 426 additions and 273 deletions
+1 -1
View File
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<LangVersion>latest</LangVersion>
+1 -1
View File
@@ -12,7 +12,7 @@
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
+5 -1
View File
@@ -1,6 +1,7 @@
using CutList.Mcp;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using ModelContextProtocol.Server;
var builder = Host.CreateApplicationBuilder(args);
@@ -8,9 +9,12 @@ var builder = Host.CreateApplicationBuilder(args);
// Register HttpClient for API calls to CutList.Web
builder.Services.AddHttpClient<ApiClient>(client =>
{
client.BaseAddress = new Uri("http://localhost:5009");
client.BaseAddress = new Uri("http://localhost:5270");
});
// Suppress all logging so it doesn't interfere with MCP stdio transport
builder.Logging.ClearProviders();
builder.Services
.AddMcpServer()
.WithStdioServerTransport()
+17
View File
@@ -0,0 +1,17 @@
@page "/Error"
<PageTitle>Error</PageTitle>
<div class="container mt-5">
<div class="row justify-content-center">
<div class="col-md-8">
<div class="card border-danger">
<div class="card-body text-center">
<h1 class="text-danger mb-3">Something went wrong</h1>
<p class="lead">An unexpected error occurred. Please try again later.</p>
<a href="/" class="btn btn-primary mt-3">Return to home page</a>
</div>
</div>
</div>
</div>
</div>
+43 -39
View File
@@ -32,7 +32,14 @@
</div>
}
@if (loading)
@if (!string.IsNullOrEmpty(loadError))
{
<div class="alert alert-danger">
<h5>Error loading job</h5>
<pre class="mb-0">@loadError</pre>
</div>
}
else if (loading)
{
<p><em>Loading...</em></p>
}
@@ -331,6 +338,7 @@ else
private List<CuttingTool> cuttingTools = new();
private bool loading = true;
private string? loadError;
private bool savingJob;
private string? jobErrorMessage;
private Tab activeTab = Tab.Details;
@@ -388,43 +396,50 @@ else
protected override async Task OnInitializedAsync()
{
materials = await MaterialService.GetAllAsync();
cuttingTools = await JobService.GetCuttingToolsAsync();
if (Id.HasValue)
try
{
var existing = await JobService.GetByIdAsync(Id.Value);
if (existing == null)
{
Navigation.NavigateTo("jobs");
return;
}
job = existing;
materials = await MaterialService.GetAllAsync();
cuttingTools = await JobService.GetCuttingToolsAsync();
// Load saved optimization results if available
if (job.OptimizationResultJson != null)
if (Id.HasValue)
{
LoadSavedResults();
var existing = await JobService.GetByIdAsync(Id.Value);
if (existing == null)
{
Navigation.NavigateTo("jobs");
return;
}
job = existing;
// Load saved optimization results if available
if (job.OptimizationResultJson != null)
{
await LoadSavedResultsAsync();
}
}
else
{
// Set default cutting tool for new jobs
var defaultTool = await JobService.GetDefaultCuttingToolAsync();
if (defaultTool != null)
{
job.CuttingToolId = defaultTool.Id;
}
}
}
else
catch (Exception ex)
{
// Set default cutting tool for new jobs
var defaultTool = await JobService.GetDefaultCuttingToolAsync();
if (defaultTool != null)
{
job.CuttingToolId = defaultTool.Id;
}
loadError = ex.ToString();
}
loading = false;
}
private void LoadSavedResults()
private async Task LoadSavedResultsAsync()
{
try
{
packResult = PackingService.LoadSavedResult(job.OptimizationResultJson!);
packResult = await PackingService.LoadSavedResultAsync(job.OptimizationResultJson!);
if (packResult != null)
{
summary = PackingService.GetSummary(packResult);
@@ -539,7 +554,7 @@ else
{
<tr>
<td>@part.Material.DisplayName</td>
<td>@ArchUnits.FormatFromInches((double)part.LengthInches)</td>
<td>@ArchUnits.FormatFromInches((double)part.LengthInches) <span class="text-muted">(@(part.LengthInches)")</span></td>
<td>@part.Quantity</td>
<td>@(string.IsNullOrWhiteSpace(part.Name) ? "-" : part.Name)</td>
<td>
@@ -902,7 +917,7 @@ else
{
<tr>
<td>@stock.Material?.DisplayName</td>
<td>@ArchUnits.FormatFromInches((double)stock.LengthInches)</td>
<td>@ArchUnits.FormatFromInches((double)stock.LengthInches) <span class="text-muted">(@(stock.LengthInches)")</span></td>
<td>@(stock.Quantity == -1 ? "Unlimited" : stock.Quantity.ToString())</td>
<td>@stock.Priority</td>
<td>
@@ -1131,7 +1146,7 @@ else
<table class="table table-sm table-striped mb-0">
<thead>
<tr class="cutlist-material-print-header">
<th colspan="5">
<th colspan="4">
<span class="cutlist-material-name">@materialResult.Material.DisplayName</span>
<span class="cutlist-material-stats">
@(materialSummary.InStockBins + materialSummary.ToBePurchasedBins) bars
@@ -1142,7 +1157,6 @@ else
</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>
@@ -1154,21 +1168,11 @@ else
{
<tr>
<td>@binNum</td>
<td>
@if (entry.Source == "Stock")
{
<span class="badge bg-success">Stock</span>
}
else
{
<span class="badge bg-warning text-dark">Purchase</span>
}
</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">
<span class="badge bg-primary me-1 fs-6">
@(string.IsNullOrWhiteSpace(item.Name) ? ArchUnits.FormatFromInches(item.Length) : $"{item.Name} ({ArchUnits.FormatFromInches(item.Length)})")
</span>
}
+4 -3
View File
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
@@ -11,11 +11,12 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="8.0.11" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="8.0.11">
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="10.0.4" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="10.0.4">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="10.0.4" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="10.1.3" />
</ItemGroup>
+9 -5
View File
@@ -3,7 +3,13 @@ using CutList.Web.Data;
using CutList.Web.Services;
using Microsoft.EntityFrameworkCore;
var builder = WebApplication.CreateBuilder(args);
var builder = WebApplication.CreateBuilder(new WebApplicationOptions
{
Args = args,
ContentRootPath = AppContext.BaseDirectory
});
builder.Host.UseWindowsService();
// Add services to the container.
builder.Services.AddControllers();
@@ -13,8 +19,8 @@ builder.Services.AddRazorComponents()
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
// Add Entity Framework
builder.Services.AddDbContext<ApplicationDbContext>(options =>
// Add Entity Framework (factory pattern for Blazor Server circuit safety)
builder.Services.AddDbContextFactory<ApplicationDbContext>(options =>
options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));
// Add application services
@@ -38,10 +44,8 @@ if (app.Environment.IsDevelopment())
else
{
app.UseExceptionHandler("/Error", createScopeForErrors: true);
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseAntiforgery();
+43 -40
View File
@@ -7,30 +7,32 @@ namespace CutList.Web.Services;
public class CatalogService
{
private readonly ApplicationDbContext _context;
private readonly IDbContextFactory<ApplicationDbContext> _factory;
private readonly MaterialService _materialService;
public CatalogService(ApplicationDbContext context, MaterialService materialService)
public CatalogService(IDbContextFactory<ApplicationDbContext> factory, MaterialService materialService)
{
_context = context;
_factory = factory;
_materialService = materialService;
}
public async Task<CatalogData> ExportAsync()
{
var suppliers = await _context.Suppliers
await using var context = _factory.CreateDbContext();
var suppliers = await context.Suppliers
.Where(s => s.IsActive)
.OrderBy(s => s.Name)
.AsNoTracking()
.ToListAsync();
var cuttingTools = await _context.CuttingTools
var cuttingTools = await context.CuttingTools
.Where(t => t.IsActive)
.OrderBy(t => t.Name)
.AsNoTracking()
.ToListAsync();
var materials = await _context.Materials
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))
@@ -157,18 +159,19 @@ public class CatalogService
{
var result = new ImportResultDto();
await using var transaction = await _context.Database.BeginTransactionAsync();
await using var context = _factory.CreateDbContext();
await using var transaction = await context.Database.BeginTransactionAsync();
try
{
// 1. Suppliers - upsert by name
var supplierMap = await ImportSuppliersAsync(data.Suppliers, result);
var supplierMap = await ImportSuppliersAsync(context, data.Suppliers, result);
// 2. Cutting tools - upsert by name
await ImportCuttingToolsAsync(data.CuttingTools, result);
await ImportCuttingToolsAsync(context, data.CuttingTools, result);
// 3. Materials + stock items + offerings
await ImportAllMaterialsAsync(data.Materials, supplierMap, result);
await ImportAllMaterialsAsync(context, data.Materials, supplierMap, result);
await transaction.CommitAsync();
}
@@ -182,11 +185,11 @@ public class CatalogService
}
private async Task<Dictionary<string, int>> ImportSuppliersAsync(
List<CatalogSupplierDto> suppliers, ImportResultDto result)
ApplicationDbContext context, List<CatalogSupplierDto> suppliers, ImportResultDto result)
{
var map = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
var existingSuppliers = await _context.Suppliers.ToListAsync();
var existingSuppliers = await context.Suppliers.ToListAsync();
foreach (var dto in suppliers)
{
@@ -212,8 +215,8 @@ public class CatalogService
Notes = dto.Notes,
CreatedAt = DateTime.UtcNow
};
_context.Suppliers.Add(supplier);
await _context.SaveChangesAsync();
context.Suppliers.Add(supplier);
await context.SaveChangesAsync();
existingSuppliers.Add(supplier);
map[dto.Name] = supplier.Id;
result.SuppliersCreated++;
@@ -225,14 +228,14 @@ public class CatalogService
}
}
await _context.SaveChangesAsync();
await context.SaveChangesAsync();
return map;
}
private async Task ImportCuttingToolsAsync(
List<CatalogCuttingToolDto> tools, ImportResultDto result)
ApplicationDbContext context, List<CatalogCuttingToolDto> tools, ImportResultDto result)
{
var existingTools = await _context.CuttingTools.ToListAsync();
var existingTools = await context.CuttingTools.ToListAsync();
foreach (var dto in tools)
{
@@ -255,7 +258,7 @@ public class CatalogService
KerfInches = dto.KerfInches,
IsDefault = false
};
_context.CuttingTools.Add(tool);
context.CuttingTools.Add(tool);
existingTools.Add(tool);
result.CuttingToolsCreated++;
}
@@ -266,71 +269,71 @@ public class CatalogService
}
}
await _context.SaveChangesAsync();
await context.SaveChangesAsync();
}
private async Task ImportAllMaterialsAsync(
CatalogMaterialsDto materials, Dictionary<string, int> supplierMap, ImportResultDto result)
ApplicationDbContext context, CatalogMaterialsDto materials, Dictionary<string, int> supplierMap, ImportResultDto result)
{
var existingMaterials = await _context.Materials
var existingMaterials = await context.Materials
.Include(m => m.Dimensions)
.Include(m => m.StockItems)
.ThenInclude(s => s.SupplierOfferings)
.ToListAsync();
foreach (var dto in materials.Angles)
await ImportMaterialAsync(dto, MaterialShape.Angle, existingMaterials, supplierMap, result,
await ImportMaterialAsync(context, dto, MaterialShape.Angle, existingMaterials, supplierMap, result,
() => new AngleDimensions { Leg1 = dto.Leg1, Leg2 = dto.Leg2, Thickness = dto.Thickness },
dim => { var d = (AngleDimensions)dim; d.Leg1 = dto.Leg1; d.Leg2 = dto.Leg2; d.Thickness = dto.Thickness; });
foreach (var dto in materials.Channels)
await ImportMaterialAsync(dto, MaterialShape.Channel, existingMaterials, supplierMap, result,
await ImportMaterialAsync(context, dto, MaterialShape.Channel, existingMaterials, supplierMap, result,
() => new ChannelDimensions { Height = dto.Height, Flange = dto.Flange, Web = dto.Web },
dim => { var d = (ChannelDimensions)dim; d.Height = dto.Height; d.Flange = dto.Flange; d.Web = dto.Web; });
foreach (var dto in materials.FlatBars)
await ImportMaterialAsync(dto, MaterialShape.FlatBar, existingMaterials, supplierMap, result,
await ImportMaterialAsync(context, dto, MaterialShape.FlatBar, existingMaterials, supplierMap, result,
() => new FlatBarDimensions { Width = dto.Width, Thickness = dto.Thickness },
dim => { var d = (FlatBarDimensions)dim; d.Width = dto.Width; d.Thickness = dto.Thickness; });
foreach (var dto in materials.IBeams)
await ImportMaterialAsync(dto, MaterialShape.IBeam, existingMaterials, supplierMap, result,
await ImportMaterialAsync(context, dto, MaterialShape.IBeam, existingMaterials, supplierMap, result,
() => new IBeamDimensions { Height = dto.Height, WeightPerFoot = dto.WeightPerFoot },
dim => { var d = (IBeamDimensions)dim; d.Height = dto.Height; d.WeightPerFoot = dto.WeightPerFoot; });
foreach (var dto in materials.Pipes)
await ImportMaterialAsync(dto, MaterialShape.Pipe, existingMaterials, supplierMap, result,
await ImportMaterialAsync(context, dto, MaterialShape.Pipe, existingMaterials, supplierMap, result,
() => new PipeDimensions { NominalSize = dto.NominalSize, Wall = dto.Wall, Schedule = dto.Schedule },
dim => { var d = (PipeDimensions)dim; d.NominalSize = dto.NominalSize; d.Wall = (decimal?)dto.Wall; d.Schedule = dto.Schedule; });
foreach (var dto in materials.RectangularTubes)
await ImportMaterialAsync(dto, MaterialShape.RectangularTube, existingMaterials, supplierMap, result,
await ImportMaterialAsync(context, dto, MaterialShape.RectangularTube, existingMaterials, supplierMap, result,
() => new RectangularTubeDimensions { Width = dto.Width, Height = dto.Height, Wall = dto.Wall },
dim => { var d = (RectangularTubeDimensions)dim; d.Width = dto.Width; d.Height = dto.Height; d.Wall = dto.Wall; });
foreach (var dto in materials.RoundBars)
await ImportMaterialAsync(dto, MaterialShape.RoundBar, existingMaterials, supplierMap, result,
await ImportMaterialAsync(context, dto, MaterialShape.RoundBar, existingMaterials, supplierMap, result,
() => new RoundBarDimensions { Diameter = dto.Diameter },
dim => { var d = (RoundBarDimensions)dim; d.Diameter = dto.Diameter; });
foreach (var dto in materials.RoundTubes)
await ImportMaterialAsync(dto, MaterialShape.RoundTube, existingMaterials, supplierMap, result,
await ImportMaterialAsync(context, dto, MaterialShape.RoundTube, existingMaterials, supplierMap, result,
() => new RoundTubeDimensions { OuterDiameter = dto.OuterDiameter, Wall = dto.Wall },
dim => { var d = (RoundTubeDimensions)dim; d.OuterDiameter = dto.OuterDiameter; d.Wall = dto.Wall; });
foreach (var dto in materials.SquareBars)
await ImportMaterialAsync(dto, MaterialShape.SquareBar, existingMaterials, supplierMap, result,
await ImportMaterialAsync(context, dto, MaterialShape.SquareBar, existingMaterials, supplierMap, result,
() => new SquareBarDimensions { Size = dto.SideLength },
dim => { var d = (SquareBarDimensions)dim; d.Size = dto.SideLength; });
foreach (var dto in materials.SquareTubes)
await ImportMaterialAsync(dto, MaterialShape.SquareTube, existingMaterials, supplierMap, result,
await ImportMaterialAsync(context, dto, MaterialShape.SquareTube, existingMaterials, supplierMap, result,
() => new SquareTubeDimensions { Size = dto.SideLength, Wall = dto.Wall },
dim => { var d = (SquareTubeDimensions)dim; d.Size = dto.SideLength; d.Wall = dto.Wall; });
}
private async Task ImportMaterialAsync(
CatalogMaterialBaseDto dto, MaterialShape shape,
ApplicationDbContext context, CatalogMaterialBaseDto dto, MaterialShape shape,
List<Material> existingMaterials, Dictionary<string, int> supplierMap,
ImportResultDto result,
Func<MaterialDimensions> createDimensions,
@@ -384,9 +387,9 @@ public class CatalogService
result.MaterialsCreated++;
}
await _context.SaveChangesAsync();
await context.SaveChangesAsync();
await ImportStockItemsAsync(material, dto.StockItems, supplierMap, result);
await ImportStockItemsAsync(context, material, dto.StockItems, supplierMap, result);
}
catch (Exception ex)
{
@@ -395,10 +398,10 @@ public class CatalogService
}
private async Task ImportStockItemsAsync(
Material material, List<CatalogStockItemDto> stockItems,
ApplicationDbContext context, Material material, List<CatalogStockItemDto> stockItems,
Dictionary<string, int> supplierMap, ImportResultDto result)
{
var existingStockItems = await _context.StockItems
var existingStockItems = await context.StockItems
.Include(s => s.SupplierOfferings)
.Where(s => s.MaterialId == material.Id)
.ToListAsync();
@@ -432,8 +435,8 @@ public class CatalogService
Notes = dto.Notes,
CreatedAt = DateTime.UtcNow
};
_context.StockItems.Add(stockItem);
await _context.SaveChangesAsync();
context.StockItems.Add(stockItem);
await context.SaveChangesAsync();
existingStockItems.Add(stockItem);
result.StockItemsCreated++;
}
@@ -473,7 +476,7 @@ public class CatalogService
Price = offeringDto.Price,
Notes = offeringDto.Notes
};
_context.SupplierOfferings.Add(offering);
context.SupplierOfferings.Add(offering);
stockItem.SupplierOfferings.Add(offering);
result.OfferingsCreated++;
}
@@ -486,7 +489,7 @@ public class CatalogService
}
}
await _context.SaveChangesAsync();
await context.SaveChangesAsync();
}
catch (Exception ex)
{
+12 -9
View File
@@ -8,11 +8,11 @@ namespace CutList.Web.Services;
public class CutListPackingService
{
private readonly ApplicationDbContext _context;
private readonly IDbContextFactory<ApplicationDbContext> _factory;
public CutListPackingService(ApplicationDbContext context)
public CutListPackingService(IDbContextFactory<ApplicationDbContext> factory)
{
_context = context;
_factory = factory;
}
public async Task<MultiMaterialPackResult> PackAsync(IEnumerable<JobPart> parts, decimal kerfInches)
@@ -22,6 +22,7 @@ public class CutListPackingService
public async Task<MultiMaterialPackResult> PackAsync(IEnumerable<JobPart> parts, decimal kerfInches, IEnumerable<JobStock>? jobStock)
{
await using var context = _factory.CreateDbContext();
var result = new MultiMaterialPackResult();
// Group parts by material
@@ -37,7 +38,7 @@ public class CutListPackingService
var materialParts = group.ToList();
// Get the material
var material = await _context.Materials
var material = await context.Materials
.FirstOrDefaultAsync(m => m.Id == materialId);
if (material == null) continue;
@@ -63,7 +64,7 @@ public class CutListPackingService
else
{
// No job-specific stock - use all available stock items for this material
var stockItems = await _context.StockItems
var stockItems = await context.StockItems
.Where(s => s.MaterialId == materialId && s.IsActive)
.ToListAsync();
@@ -172,10 +173,12 @@ public class CutListPackingService
return result;
}
public MultiMaterialPackResult? LoadSavedResult(string json)
public async Task<MultiMaterialPackResult?> LoadSavedResultAsync(string json)
{
var saved = System.Text.Json.JsonSerializer.Deserialize<SavedOptimizationResult>(json);
return saved?.ToPackResult(_context);
if (saved == null) return null;
await using var context = _factory.CreateDbContext();
return await saved.ToPackResultAsync(context);
}
public string SerializeResult(MultiMaterialPackResult result)
@@ -344,13 +347,13 @@ public class SavedOptimizationResult
return saved;
}
public MultiMaterialPackResult ToPackResult(ApplicationDbContext context)
public async Task<MultiMaterialPackResult> ToPackResultAsync(ApplicationDbContext context)
{
var result = new MultiMaterialPackResult();
foreach (var savedMr in MaterialResults)
{
var material = context.Materials.Find(savedMr.MaterialId);
var material = await context.Materials.FindAsync(savedMr.MaterialId);
if (material == null) continue;
var packResult = new PackResult();
+108 -69
View File
@@ -6,16 +6,17 @@ namespace CutList.Web.Services;
public class JobService
{
private readonly ApplicationDbContext _context;
private readonly IDbContextFactory<ApplicationDbContext> _factory;
public JobService(ApplicationDbContext context)
public JobService(IDbContextFactory<ApplicationDbContext> factory)
{
_context = context;
_factory = factory;
}
public async Task<List<Job>> GetAllAsync()
{
return await _context.Jobs
await using var context = _factory.CreateDbContext();
return await context.Jobs
.Include(p => p.CuttingTool)
.Include(p => p.Parts)
.ThenInclude(pt => pt.Material)
@@ -25,7 +26,8 @@ public class JobService
public async Task<Job?> GetByIdAsync(int id)
{
return await _context.Jobs
await using var context = _factory.CreateDbContext();
return await context.Jobs
.Include(p => p.CuttingTool)
.Include(p => p.Parts.OrderBy(pt => pt.SortOrder))
.ThenInclude(pt => pt.Material)
@@ -38,17 +40,24 @@ public class JobService
public async Task<Job> CreateAsync(Job? job = null)
{
await using var context = _factory.CreateDbContext();
job ??= new Job();
job.JobNumber = await GenerateJobNumberAsync();
job.JobNumber = await GenerateJobNumberAsync(context);
job.CreatedAt = DateTime.UtcNow;
_context.Jobs.Add(job);
await _context.SaveChangesAsync();
context.Jobs.Add(job);
await context.SaveChangesAsync();
return job;
}
public async Task<string> GenerateJobNumberAsync()
{
var maxNumber = await _context.Jobs
await using var context = _factory.CreateDbContext();
return await GenerateJobNumberAsync(context);
}
private static async Task<string> GenerateJobNumberAsync(ApplicationDbContext context)
{
var maxNumber = await context.Jobs
.Where(j => j.JobNumber.StartsWith("JOB-"))
.Select(j => j.JobNumber)
.MaxAsync() as string;
@@ -71,46 +80,61 @@ public class JobService
public async Task UpdateAsync(Job job)
{
await using var context = _factory.CreateDbContext();
job.UpdatedAt = DateTime.UtcNow;
job.OptimizationResultJson = null;
job.OptimizedAt = null;
_context.Jobs.Update(job);
await _context.SaveChangesAsync();
context.Jobs.Update(job);
await context.SaveChangesAsync();
}
public async Task LockAsync(int id)
{
var job = await _context.Jobs.FindAsync(id);
await using var context = _factory.CreateDbContext();
var job = await context.Jobs.FindAsync(id);
if (job != null)
{
job.LockedAt = DateTime.UtcNow;
await _context.SaveChangesAsync();
await context.SaveChangesAsync();
}
}
public async Task UnlockAsync(int id)
{
var job = await _context.Jobs.FindAsync(id);
await using var context = _factory.CreateDbContext();
var job = await context.Jobs.FindAsync(id);
if (job != null)
{
job.LockedAt = null;
await _context.SaveChangesAsync();
await context.SaveChangesAsync();
}
}
public async Task DeleteAsync(int id)
{
var job = await _context.Jobs.FindAsync(id);
await using var context = _factory.CreateDbContext();
var job = await context.Jobs.FindAsync(id);
if (job != null)
{
_context.Jobs.Remove(job);
await _context.SaveChangesAsync();
context.Jobs.Remove(job);
await context.SaveChangesAsync();
}
}
public async Task<Job> DuplicateAsync(int id)
{
var original = await GetByIdAsync(id);
await using var context = _factory.CreateDbContext();
var original = await context.Jobs
.Include(p => p.CuttingTool)
.Include(p => p.Parts.OrderBy(pt => pt.SortOrder))
.ThenInclude(pt => pt.Material)
.Include(p => p.Stock.OrderBy(s => s.SortOrder))
.ThenInclude(s => s.Material)
.Include(p => p.Stock)
.ThenInclude(s => s.StockItem)
.FirstOrDefaultAsync(p => p.Id == id);
if (original == null)
{
throw new ArgumentException("Job not found", nameof(id));
@@ -118,7 +142,7 @@ public class JobService
var duplicate = new Job
{
JobNumber = await GenerateJobNumberAsync(),
JobNumber = await GenerateJobNumberAsync(context),
Name = string.IsNullOrWhiteSpace(original.Name) ? null : $"{original.Name} (Copy)",
Customer = original.Customer,
CuttingToolId = original.CuttingToolId,
@@ -126,13 +150,13 @@ public class JobService
CreatedAt = DateTime.UtcNow
};
_context.Jobs.Add(duplicate);
await _context.SaveChangesAsync();
context.Jobs.Add(duplicate);
await context.SaveChangesAsync();
// Copy parts
foreach (var part in original.Parts)
{
_context.JobParts.Add(new JobPart
context.JobParts.Add(new JobPart
{
JobId = duplicate.Id,
MaterialId = part.MaterialId,
@@ -146,7 +170,7 @@ public class JobService
// Copy stock selections
foreach (var stock in original.Stock)
{
_context.JobStocks.Add(new JobStock
context.JobStocks.Add(new JobStock
{
JobId = duplicate.Id,
MaterialId = stock.MaterialId,
@@ -159,52 +183,55 @@ public class JobService
});
}
await _context.SaveChangesAsync();
await context.SaveChangesAsync();
return duplicate;
}
// Optimization result persistence
public async Task SaveOptimizationResultAsync(int jobId, string resultJson, DateTime optimizedAt)
{
var job = await _context.Jobs.FindAsync(jobId);
await using var context = _factory.CreateDbContext();
var job = await context.Jobs.FindAsync(jobId);
if (job != null)
{
job.OptimizationResultJson = resultJson;
job.OptimizedAt = optimizedAt;
await _context.SaveChangesAsync();
await context.SaveChangesAsync();
}
}
public async Task ClearOptimizationResultAsync(int jobId)
{
var job = await _context.Jobs.FindAsync(jobId);
await using var context = _factory.CreateDbContext();
var job = await context.Jobs.FindAsync(jobId);
if (job != null && job.OptimizationResultJson != null)
{
job.OptimizationResultJson = null;
job.OptimizedAt = null;
await _context.SaveChangesAsync();
await context.SaveChangesAsync();
}
}
// Parts management
public async Task<JobPart> AddPartAsync(JobPart part)
{
var maxOrder = await _context.JobParts
await using var context = _factory.CreateDbContext();
var maxOrder = await context.JobParts
.Where(p => p.JobId == part.JobId)
.MaxAsync(p => (int?)p.SortOrder) ?? -1;
part.SortOrder = maxOrder + 1;
_context.JobParts.Add(part);
await _context.SaveChangesAsync();
context.JobParts.Add(part);
await context.SaveChangesAsync();
// Update job timestamp and clear stale results
var job = await _context.Jobs.FindAsync(part.JobId);
var job = await context.Jobs.FindAsync(part.JobId);
if (job != null)
{
job.UpdatedAt = DateTime.UtcNow;
job.OptimizationResultJson = null;
job.OptimizedAt = null;
await _context.SaveChangesAsync();
await context.SaveChangesAsync();
}
return part;
@@ -212,35 +239,37 @@ public class JobService
public async Task UpdatePartAsync(JobPart part)
{
_context.JobParts.Update(part);
await _context.SaveChangesAsync();
await using var context = _factory.CreateDbContext();
context.JobParts.Update(part);
await context.SaveChangesAsync();
var job = await _context.Jobs.FindAsync(part.JobId);
var job = await context.Jobs.FindAsync(part.JobId);
if (job != null)
{
job.UpdatedAt = DateTime.UtcNow;
job.OptimizationResultJson = null;
job.OptimizedAt = null;
await _context.SaveChangesAsync();
await context.SaveChangesAsync();
}
}
public async Task DeletePartAsync(int id)
{
var part = await _context.JobParts.FindAsync(id);
await using var context = _factory.CreateDbContext();
var part = await context.JobParts.FindAsync(id);
if (part != null)
{
var jobId = part.JobId;
_context.JobParts.Remove(part);
await _context.SaveChangesAsync();
context.JobParts.Remove(part);
await context.SaveChangesAsync();
var job = await _context.Jobs.FindAsync(jobId);
var job = await context.Jobs.FindAsync(jobId);
if (job != null)
{
job.UpdatedAt = DateTime.UtcNow;
job.OptimizationResultJson = null;
job.OptimizedAt = null;
await _context.SaveChangesAsync();
await context.SaveChangesAsync();
}
}
}
@@ -248,21 +277,22 @@ public class JobService
// Stock management
public async Task<JobStock> AddStockAsync(JobStock stock)
{
var maxOrder = await _context.JobStocks
await using var context = _factory.CreateDbContext();
var maxOrder = await context.JobStocks
.Where(s => s.JobId == stock.JobId)
.MaxAsync(s => (int?)s.SortOrder) ?? -1;
stock.SortOrder = maxOrder + 1;
_context.JobStocks.Add(stock);
await _context.SaveChangesAsync();
context.JobStocks.Add(stock);
await context.SaveChangesAsync();
var job = await _context.Jobs.FindAsync(stock.JobId);
var job = await context.Jobs.FindAsync(stock.JobId);
if (job != null)
{
job.UpdatedAt = DateTime.UtcNow;
job.OptimizationResultJson = null;
job.OptimizedAt = null;
await _context.SaveChangesAsync();
await context.SaveChangesAsync();
}
return stock;
@@ -270,42 +300,45 @@ public class JobService
public async Task UpdateStockAsync(JobStock stock)
{
_context.JobStocks.Update(stock);
await _context.SaveChangesAsync();
await using var context = _factory.CreateDbContext();
context.JobStocks.Update(stock);
await context.SaveChangesAsync();
var job = await _context.Jobs.FindAsync(stock.JobId);
var job = await context.Jobs.FindAsync(stock.JobId);
if (job != null)
{
job.UpdatedAt = DateTime.UtcNow;
job.OptimizationResultJson = null;
job.OptimizedAt = null;
await _context.SaveChangesAsync();
await context.SaveChangesAsync();
}
}
public async Task DeleteStockAsync(int id)
{
var stock = await _context.JobStocks.FindAsync(id);
await using var context = _factory.CreateDbContext();
var stock = await context.JobStocks.FindAsync(id);
if (stock != null)
{
var jobId = stock.JobId;
_context.JobStocks.Remove(stock);
await _context.SaveChangesAsync();
context.JobStocks.Remove(stock);
await context.SaveChangesAsync();
var job = await _context.Jobs.FindAsync(jobId);
var job = await context.Jobs.FindAsync(jobId);
if (job != null)
{
job.UpdatedAt = DateTime.UtcNow;
job.OptimizationResultJson = null;
job.OptimizedAt = null;
await _context.SaveChangesAsync();
await context.SaveChangesAsync();
}
}
}
public async Task<List<StockItem>> GetAvailableStockForMaterialAsync(int materialId)
{
return await _context.StockItems
await using var context = _factory.CreateDbContext();
return await context.StockItems
.Include(s => s.Material)
.Where(s => s.MaterialId == materialId && s.IsActive)
.OrderBy(s => s.LengthInches)
@@ -315,7 +348,8 @@ public class JobService
// Cutting tools
public async Task<List<CuttingTool>> GetCuttingToolsAsync(bool includeInactive = false)
{
var query = _context.CuttingTools.AsQueryable();
await using var context = _factory.CreateDbContext();
var query = context.CuttingTools.AsQueryable();
if (!includeInactive)
{
query = query.Where(t => t.IsActive);
@@ -325,53 +359,58 @@ public class JobService
public async Task<CuttingTool?> GetCuttingToolByIdAsync(int id)
{
return await _context.CuttingTools.FindAsync(id);
await using var context = _factory.CreateDbContext();
return await context.CuttingTools.FindAsync(id);
}
public async Task<CuttingTool?> GetDefaultCuttingToolAsync()
{
return await _context.CuttingTools.FirstOrDefaultAsync(t => t.IsDefault && t.IsActive);
await using var context = _factory.CreateDbContext();
return await context.CuttingTools.FirstOrDefaultAsync(t => t.IsDefault && t.IsActive);
}
public async Task<CuttingTool> CreateCuttingToolAsync(CuttingTool tool)
{
await using var context = _factory.CreateDbContext();
if (tool.IsDefault)
{
// Clear other defaults
var others = await _context.CuttingTools.Where(t => t.IsDefault).ToListAsync();
var others = await context.CuttingTools.Where(t => t.IsDefault).ToListAsync();
foreach (var other in others)
{
other.IsDefault = false;
}
}
_context.CuttingTools.Add(tool);
await _context.SaveChangesAsync();
context.CuttingTools.Add(tool);
await context.SaveChangesAsync();
return tool;
}
public async Task UpdateCuttingToolAsync(CuttingTool tool)
{
await using var context = _factory.CreateDbContext();
if (tool.IsDefault)
{
var others = await _context.CuttingTools.Where(t => t.IsDefault && t.Id != tool.Id).ToListAsync();
var others = await context.CuttingTools.Where(t => t.IsDefault && t.Id != tool.Id).ToListAsync();
foreach (var other in others)
{
other.IsDefault = false;
}
}
_context.CuttingTools.Update(tool);
await _context.SaveChangesAsync();
context.CuttingTools.Update(tool);
await context.SaveChangesAsync();
}
public async Task DeleteCuttingToolAsync(int id)
{
var tool = await _context.CuttingTools.FindAsync(id);
await using var context = _factory.CreateDbContext();
var tool = await context.CuttingTools.FindAsync(id);
if (tool != null)
{
tool.IsActive = false;
await _context.SaveChangesAsync();
await context.SaveChangesAsync();
}
}
}
+52 -33
View File
@@ -6,16 +6,17 @@ namespace CutList.Web.Services;
public class MaterialService
{
private readonly ApplicationDbContext _context;
private readonly IDbContextFactory<ApplicationDbContext> _factory;
public MaterialService(ApplicationDbContext context)
public MaterialService(IDbContextFactory<ApplicationDbContext> factory)
{
_context = context;
_factory = factory;
}
public async Task<List<Material>> GetAllAsync(bool includeInactive = false)
{
var query = _context.Materials
await using var context = _factory.CreateDbContext();
var query = context.Materials
.Include(m => m.Dimensions)
.AsQueryable();
if (!includeInactive)
@@ -27,16 +28,18 @@ public class MaterialService
public async Task<Material?> GetByIdAsync(int id)
{
return await _context.Materials
await using var context = _factory.CreateDbContext();
return await context.Materials
.Include(m => m.Dimensions)
.FirstOrDefaultAsync(m => m.Id == id);
}
public async Task<Material> CreateAsync(Material material)
{
await using var context = _factory.CreateDbContext();
material.CreatedAt = DateTime.UtcNow;
_context.Materials.Add(material);
await _context.SaveChangesAsync();
context.Materials.Add(material);
await context.SaveChangesAsync();
return material;
}
@@ -45,6 +48,7 @@ public class MaterialService
/// </summary>
public async Task<Material> CreateWithDimensionsAsync(Material material, MaterialDimensions dimensions)
{
await using var context = _factory.CreateDbContext();
material.CreatedAt = DateTime.UtcNow;
// Auto-generate Size string from dimensions if not provided
@@ -56,13 +60,13 @@ public class MaterialService
// Set sort order from primary dimension
material.SortOrder = dimensions.GetSortOrder();
_context.Materials.Add(material);
await _context.SaveChangesAsync();
context.Materials.Add(material);
await context.SaveChangesAsync();
// Link dimensions to the created material
dimensions.MaterialId = material.Id;
_context.MaterialDimensions.Add(dimensions);
await _context.SaveChangesAsync();
context.MaterialDimensions.Add(dimensions);
await context.SaveChangesAsync();
material.Dimensions = dimensions;
return material;
@@ -70,9 +74,10 @@ public class MaterialService
public async Task UpdateAsync(Material material)
{
await using var context = _factory.CreateDbContext();
material.UpdatedAt = DateTime.UtcNow;
_context.Materials.Update(material);
await _context.SaveChangesAsync();
context.Materials.Update(material);
await context.SaveChangesAsync();
}
/// <summary>
@@ -80,6 +85,7 @@ public class MaterialService
/// </summary>
public async Task UpdateWithDimensionsAsync(Material material, MaterialDimensions dimensions, bool regenerateSize = false)
{
await using var context = _factory.CreateDbContext();
material.UpdatedAt = DateTime.UtcNow;
if (regenerateSize)
@@ -98,14 +104,14 @@ public class MaterialService
if (dimensions.Id > 0)
{
// Already tracked, just save
_context.Entry(material).State = EntityState.Modified;
context.Entry(material).State = EntityState.Modified;
}
else
{
_context.Materials.Update(material);
context.Materials.Update(material);
// Check if dimensions already exist for this material
var existingDimensions = await _context.MaterialDimensions
var existingDimensions = await context.MaterialDimensions
.AsNoTracking()
.FirstOrDefaultAsync(d => d.MaterialId == material.Id);
@@ -113,30 +119,32 @@ public class MaterialService
{
// Copy the existing Id to update in place
dimensions.Id = existingDimensions.Id;
_context.MaterialDimensions.Update(dimensions);
context.MaterialDimensions.Update(dimensions);
}
else
{
_context.MaterialDimensions.Add(dimensions);
context.MaterialDimensions.Add(dimensions);
}
}
await _context.SaveChangesAsync();
await context.SaveChangesAsync();
}
public async Task DeleteAsync(int id)
{
var material = await _context.Materials.FindAsync(id);
await using var context = _factory.CreateDbContext();
var material = await context.Materials.FindAsync(id);
if (material != null)
{
material.IsActive = false;
await _context.SaveChangesAsync();
await context.SaveChangesAsync();
}
}
public async Task<bool> ExistsAsync(MaterialShape shape, string size, int? excludeId = null)
{
var query = _context.Materials.Where(m => m.Shape == shape && m.Size == size && m.IsActive);
await using var context = _factory.CreateDbContext();
var query = context.Materials.Where(m => m.Shape == shape && m.Size == size && m.IsActive);
if (excludeId.HasValue)
{
query = query.Where(m => m.Id != excludeId.Value);
@@ -149,10 +157,11 @@ public class MaterialService
/// </summary>
public async Task<List<Material>> SearchRoundBarByDiameterAsync(decimal targetDiameter, decimal tolerance)
{
await using var context = _factory.CreateDbContext();
var minValue = targetDiameter - tolerance;
var maxValue = targetDiameter + tolerance;
return await _context.Set<RoundBarDimensions>()
return await context.Set<RoundBarDimensions>()
.Include(d => d.Material)
.Where(d => d.Material.IsActive)
.Where(d => d.Diameter >= minValue && d.Diameter <= maxValue)
@@ -166,10 +175,11 @@ public class MaterialService
/// </summary>
public async Task<List<Material>> SearchRoundTubeByODAsync(decimal targetOD, decimal tolerance)
{
await using var context = _factory.CreateDbContext();
var minValue = targetOD - tolerance;
var maxValue = targetOD + tolerance;
return await _context.Set<RoundTubeDimensions>()
return await context.Set<RoundTubeDimensions>()
.Include(d => d.Material)
.Where(d => d.Material.IsActive)
.Where(d => d.OuterDiameter >= minValue && d.OuterDiameter <= maxValue)
@@ -183,10 +193,11 @@ public class MaterialService
/// </summary>
public async Task<List<Material>> SearchFlatBarByWidthAsync(decimal targetWidth, decimal tolerance)
{
await using var context = _factory.CreateDbContext();
var minValue = targetWidth - tolerance;
var maxValue = targetWidth + tolerance;
return await _context.Set<FlatBarDimensions>()
return await context.Set<FlatBarDimensions>()
.Include(d => d.Material)
.Where(d => d.Material.IsActive)
.Where(d => d.Width >= minValue && d.Width <= maxValue)
@@ -200,10 +211,11 @@ public class MaterialService
/// </summary>
public async Task<List<Material>> SearchSquareBarBySizeAsync(decimal targetSize, decimal tolerance)
{
await using var context = _factory.CreateDbContext();
var minValue = targetSize - tolerance;
var maxValue = targetSize + tolerance;
return await _context.Set<SquareBarDimensions>()
return await context.Set<SquareBarDimensions>()
.Include(d => d.Material)
.Where(d => d.Material.IsActive)
.Where(d => d.Size >= minValue && d.Size <= maxValue)
@@ -217,10 +229,11 @@ public class MaterialService
/// </summary>
public async Task<List<Material>> SearchSquareTubeBySizeAsync(decimal targetSize, decimal tolerance)
{
await using var context = _factory.CreateDbContext();
var minValue = targetSize - tolerance;
var maxValue = targetSize + tolerance;
return await _context.Set<SquareTubeDimensions>()
return await context.Set<SquareTubeDimensions>()
.Include(d => d.Material)
.Where(d => d.Material.IsActive)
.Where(d => d.Size >= minValue && d.Size <= maxValue)
@@ -234,10 +247,11 @@ public class MaterialService
/// </summary>
public async Task<List<Material>> SearchRectangularTubeByWidthAsync(decimal targetWidth, decimal tolerance)
{
await using var context = _factory.CreateDbContext();
var minValue = targetWidth - tolerance;
var maxValue = targetWidth + tolerance;
return await _context.Set<RectangularTubeDimensions>()
return await context.Set<RectangularTubeDimensions>()
.Include(d => d.Material)
.Where(d => d.Material.IsActive)
.Where(d => d.Width >= minValue && d.Width <= maxValue)
@@ -251,10 +265,11 @@ public class MaterialService
/// </summary>
public async Task<List<Material>> SearchAngleByLegAsync(decimal targetLeg, decimal tolerance)
{
await using var context = _factory.CreateDbContext();
var minValue = targetLeg - tolerance;
var maxValue = targetLeg + tolerance;
return await _context.Set<AngleDimensions>()
return await context.Set<AngleDimensions>()
.Include(d => d.Material)
.Where(d => d.Material.IsActive)
.Where(d => d.Leg1 >= minValue && d.Leg1 <= maxValue)
@@ -268,10 +283,11 @@ public class MaterialService
/// </summary>
public async Task<List<Material>> SearchChannelByHeightAsync(decimal targetHeight, decimal tolerance)
{
await using var context = _factory.CreateDbContext();
var minValue = targetHeight - tolerance;
var maxValue = targetHeight + tolerance;
return await _context.Set<ChannelDimensions>()
return await context.Set<ChannelDimensions>()
.Include(d => d.Material)
.Where(d => d.Material.IsActive)
.Where(d => d.Height >= minValue && d.Height <= maxValue)
@@ -285,10 +301,11 @@ public class MaterialService
/// </summary>
public async Task<List<Material>> SearchIBeamByHeightAsync(decimal targetHeight, decimal tolerance)
{
await using var context = _factory.CreateDbContext();
var minValue = targetHeight - tolerance;
var maxValue = targetHeight + tolerance;
return await _context.Set<IBeamDimensions>()
return await context.Set<IBeamDimensions>()
.Include(d => d.Material)
.Where(d => d.Material.IsActive)
.Where(d => d.Height >= minValue && d.Height <= maxValue)
@@ -302,10 +319,11 @@ public class MaterialService
/// </summary>
public async Task<List<Material>> SearchPipeByNominalSizeAsync(decimal targetNPS, decimal tolerance)
{
await using var context = _factory.CreateDbContext();
var minValue = targetNPS - tolerance;
var maxValue = targetNPS + tolerance;
return await _context.Set<PipeDimensions>()
return await context.Set<PipeDimensions>()
.Include(d => d.Material)
.Where(d => d.Material.IsActive)
.Where(d => d.NominalSize >= minValue && d.NominalSize <= maxValue)
@@ -319,7 +337,8 @@ public class MaterialService
/// </summary>
public async Task<List<Material>> GetByShapeAsync(MaterialShape shape, bool includeInactive = false)
{
var query = _context.Materials
await using var context = _factory.CreateDbContext();
var query = context.Materials
.Include(m => m.Dimensions)
.Where(m => m.Shape == shape);
+34 -18
View File
@@ -6,16 +6,18 @@ namespace CutList.Web.Services;
public class PurchaseItemService
{
private readonly ApplicationDbContext _context;
private readonly IDbContextFactory<ApplicationDbContext> _factory;
public PurchaseItemService(ApplicationDbContext context)
public PurchaseItemService(IDbContextFactory<ApplicationDbContext> factory)
{
_context = context;
_factory = factory;
}
public async Task<List<PurchaseItem>> GetAllAsync(PurchaseItemStatus? status = null)
{
var query = _context.PurchaseItems
await using var context = _factory.CreateDbContext();
var query = context.PurchaseItems
.Include(p => p.StockItem)
.ThenInclude(s => s.Material)
.Include(p => p.Supplier)
@@ -35,7 +37,9 @@ public class PurchaseItemService
public async Task<PurchaseItem?> GetByIdAsync(int id)
{
return await _context.PurchaseItems
await using var context = _factory.CreateDbContext();
return await context.PurchaseItems
.Include(p => p.StockItem)
.ThenInclude(s => s.Material)
.Include(p => p.Supplier)
@@ -45,59 +49,71 @@ public class PurchaseItemService
public async Task<PurchaseItem> CreateAsync(PurchaseItem item)
{
await using var context = _factory.CreateDbContext();
item.CreatedAt = DateTime.UtcNow;
_context.PurchaseItems.Add(item);
await _context.SaveChangesAsync();
context.PurchaseItems.Add(item);
await context.SaveChangesAsync();
return item;
}
public async Task CreateBulkAsync(List<PurchaseItem> items)
{
await using var context = _factory.CreateDbContext();
var now = DateTime.UtcNow;
foreach (var item in items)
{
item.CreatedAt = now;
}
_context.PurchaseItems.AddRange(items);
await _context.SaveChangesAsync();
context.PurchaseItems.AddRange(items);
await context.SaveChangesAsync();
}
public async Task UpdateAsync(PurchaseItem item)
{
await using var context = _factory.CreateDbContext();
item.UpdatedAt = DateTime.UtcNow;
_context.PurchaseItems.Update(item);
await _context.SaveChangesAsync();
context.PurchaseItems.Update(item);
await context.SaveChangesAsync();
}
public async Task UpdateStatusAsync(int id, PurchaseItemStatus status)
{
var item = await _context.PurchaseItems.FindAsync(id);
await using var context = _factory.CreateDbContext();
var item = await context.PurchaseItems.FindAsync(id);
if (item != null)
{
item.Status = status;
item.UpdatedAt = DateTime.UtcNow;
await _context.SaveChangesAsync();
await context.SaveChangesAsync();
}
}
public async Task UpdateSupplierAsync(int id, int? supplierId)
{
var item = await _context.PurchaseItems.FindAsync(id);
await using var context = _factory.CreateDbContext();
var item = await context.PurchaseItems.FindAsync(id);
if (item != null)
{
item.SupplierId = supplierId;
item.UpdatedAt = DateTime.UtcNow;
await _context.SaveChangesAsync();
await context.SaveChangesAsync();
}
}
public async Task DeleteAsync(int id)
{
var item = await _context.PurchaseItems.FindAsync(id);
await using var context = _factory.CreateDbContext();
var item = await context.PurchaseItems.FindAsync(id);
if (item != null)
{
_context.PurchaseItems.Remove(item);
await _context.SaveChangesAsync();
context.PurchaseItems.Remove(item);
await context.SaveChangesAsync();
}
}
}
+61 -31
View File
@@ -6,16 +6,18 @@ namespace CutList.Web.Services;
public class StockItemService
{
private readonly ApplicationDbContext _context;
private readonly IDbContextFactory<ApplicationDbContext> _factory;
public StockItemService(ApplicationDbContext context)
public StockItemService(IDbContextFactory<ApplicationDbContext> factory)
{
_context = context;
_factory = factory;
}
public async Task<List<StockItem>> GetAllAsync(bool includeInactive = false)
{
var query = _context.StockItems
await using var context = _factory.CreateDbContext();
var query = context.StockItems
.Include(s => s.Material)
.AsQueryable();
@@ -33,7 +35,9 @@ public class StockItemService
public async Task<List<StockItem>> GetByMaterialAsync(int materialId, bool includeInactive = false)
{
var query = _context.StockItems
await using var context = _factory.CreateDbContext();
var query = context.StockItems
.Include(s => s.Material)
.Where(s => s.MaterialId == materialId);
@@ -49,7 +53,9 @@ public class StockItemService
public async Task<StockItem?> GetByIdAsync(int id)
{
return await _context.StockItems
await using var context = _factory.CreateDbContext();
return await context.StockItems
.Include(s => s.Material)
.Include(s => s.SupplierOfferings)
.ThenInclude(o => o.Supplier)
@@ -58,33 +64,41 @@ public class StockItemService
public async Task<StockItem> CreateAsync(StockItem stockItem)
{
await using var context = _factory.CreateDbContext();
stockItem.CreatedAt = DateTime.UtcNow;
_context.StockItems.Add(stockItem);
await _context.SaveChangesAsync();
context.StockItems.Add(stockItem);
await context.SaveChangesAsync();
return stockItem;
}
public async Task UpdateAsync(StockItem stockItem)
{
await using var context = _factory.CreateDbContext();
stockItem.UpdatedAt = DateTime.UtcNow;
_context.StockItems.Update(stockItem);
await _context.SaveChangesAsync();
context.StockItems.Update(stockItem);
await context.SaveChangesAsync();
}
public async Task DeleteAsync(int id)
{
var stockItem = await _context.StockItems.FindAsync(id);
await using var context = _factory.CreateDbContext();
var stockItem = await context.StockItems.FindAsync(id);
if (stockItem != null)
{
stockItem.IsActive = false;
stockItem.UpdatedAt = DateTime.UtcNow;
await _context.SaveChangesAsync();
await context.SaveChangesAsync();
}
}
public async Task<bool> ExistsAsync(int materialId, decimal lengthInches, int? excludeId = null)
{
var query = _context.StockItems.Where(s =>
await using var context = _factory.CreateDbContext();
var query = context.StockItems.Where(s =>
s.MaterialId == materialId &&
s.LengthInches == lengthInches &&
s.IsActive);
@@ -100,7 +114,9 @@ public class StockItemService
// Stock transaction methods
public async Task<StockTransaction> AddStockAsync(int stockItemId, int quantity, int? supplierId = null, decimal? unitPrice = null, string? notes = null)
{
var stockItem = await _context.StockItems.FindAsync(stockItemId)
await using var context = _factory.CreateDbContext();
var stockItem = await context.StockItems.FindAsync(stockItemId)
?? throw new InvalidOperationException($"Stock item {stockItemId} not found");
var transaction = new StockTransaction
@@ -117,15 +133,17 @@ public class StockItemService
stockItem.QuantityOnHand += quantity;
stockItem.UpdatedAt = DateTime.UtcNow;
_context.StockTransactions.Add(transaction);
await _context.SaveChangesAsync();
context.StockTransactions.Add(transaction);
await context.SaveChangesAsync();
return transaction;
}
public async Task<decimal?> GetAverageCostAsync(int stockItemId)
{
var transactions = await _context.StockTransactions
await using var context = _factory.CreateDbContext();
var transactions = await context.StockTransactions
.Where(t => t.StockItemId == stockItemId && t.Type == StockTransactionType.Received && t.UnitPrice.HasValue)
.ToListAsync();
@@ -140,7 +158,9 @@ public class StockItemService
public async Task<decimal?> GetLastPurchasePriceAsync(int stockItemId)
{
return await _context.StockTransactions
await using var context = _factory.CreateDbContext();
return await context.StockTransactions
.Where(t => t.StockItemId == stockItemId && t.Type == StockTransactionType.Received && t.UnitPrice.HasValue)
.OrderByDescending(t => t.CreatedAt)
.Select(t => t.UnitPrice)
@@ -149,7 +169,9 @@ public class StockItemService
public async Task<StockTransaction> UseStockAsync(int stockItemId, int quantity, int? jobId = null, string? notes = null)
{
var stockItem = await _context.StockItems.FindAsync(stockItemId)
await using var context = _factory.CreateDbContext();
var stockItem = await context.StockItems.FindAsync(stockItemId)
?? throw new InvalidOperationException($"Stock item {stockItemId} not found");
var transaction = new StockTransaction
@@ -165,15 +187,17 @@ public class StockItemService
stockItem.QuantityOnHand -= quantity;
stockItem.UpdatedAt = DateTime.UtcNow;
_context.StockTransactions.Add(transaction);
await _context.SaveChangesAsync();
context.StockTransactions.Add(transaction);
await context.SaveChangesAsync();
return transaction;
}
public async Task<StockTransaction> AdjustStockAsync(int stockItemId, int newQuantity, string? notes = null)
{
var stockItem = await _context.StockItems.FindAsync(stockItemId)
await using var context = _factory.CreateDbContext();
var stockItem = await context.StockItems.FindAsync(stockItemId)
?? throw new InvalidOperationException($"Stock item {stockItemId} not found");
var difference = newQuantity - stockItem.QuantityOnHand;
@@ -190,15 +214,17 @@ public class StockItemService
stockItem.QuantityOnHand = newQuantity;
stockItem.UpdatedAt = DateTime.UtcNow;
_context.StockTransactions.Add(transaction);
await _context.SaveChangesAsync();
context.StockTransactions.Add(transaction);
await context.SaveChangesAsync();
return transaction;
}
public async Task<StockTransaction> ScrapStockAsync(int stockItemId, int quantity, string? notes = null)
{
var stockItem = await _context.StockItems.FindAsync(stockItemId)
await using var context = _factory.CreateDbContext();
var stockItem = await context.StockItems.FindAsync(stockItemId)
?? throw new InvalidOperationException($"Stock item {stockItemId} not found");
var transaction = new StockTransaction
@@ -213,15 +239,17 @@ public class StockItemService
stockItem.QuantityOnHand -= quantity;
stockItem.UpdatedAt = DateTime.UtcNow;
_context.StockTransactions.Add(transaction);
await _context.SaveChangesAsync();
context.StockTransactions.Add(transaction);
await context.SaveChangesAsync();
return transaction;
}
public async Task<List<StockTransaction>> GetTransactionHistoryAsync(int stockItemId, int? limit = null)
{
var query = _context.StockTransactions
await using var context = _factory.CreateDbContext();
var query = context.StockTransactions
.Include(t => t.Job)
.Include(t => t.Supplier)
.Where(t => t.StockItemId == stockItemId)
@@ -238,10 +266,12 @@ public class StockItemService
public async Task<int> RecalculateQuantityAsync(int stockItemId)
{
var stockItem = await _context.StockItems.FindAsync(stockItemId)
await using var context = _factory.CreateDbContext();
var stockItem = await context.StockItems.FindAsync(stockItemId)
?? throw new InvalidOperationException($"Stock item {stockItemId} not found");
var calculatedQuantity = await _context.StockTransactions
var calculatedQuantity = await context.StockTransactions
.Where(t => t.StockItemId == stockItemId)
.SumAsync(t => t.Quantity);
@@ -249,7 +279,7 @@ public class StockItemService
{
stockItem.QuantityOnHand = calculatedQuantity;
stockItem.UpdatedAt = DateTime.UtcNow;
await _context.SaveChangesAsync();
await context.SaveChangesAsync();
}
return calculatedQuantity;
+33 -21
View File
@@ -6,16 +6,17 @@ namespace CutList.Web.Services;
public class SupplierService
{
private readonly ApplicationDbContext _context;
private readonly IDbContextFactory<ApplicationDbContext> _factory;
public SupplierService(ApplicationDbContext context)
public SupplierService(IDbContextFactory<ApplicationDbContext> factory)
{
_context = context;
_factory = factory;
}
public async Task<List<Supplier>> GetAllAsync(bool includeInactive = false)
{
var query = _context.Suppliers.AsQueryable();
await using var context = _factory.CreateDbContext();
var query = context.Suppliers.AsQueryable();
if (!includeInactive)
{
query = query.Where(s => s.IsActive);
@@ -25,7 +26,8 @@ public class SupplierService
public async Task<Supplier?> GetByIdAsync(int id)
{
return await _context.Suppliers
await using var context = _factory.CreateDbContext();
return await context.Suppliers
.Include(s => s.Offerings)
.ThenInclude(o => o.StockItem)
.ThenInclude(si => si.Material)
@@ -34,32 +36,36 @@ public class SupplierService
public async Task<Supplier> CreateAsync(Supplier supplier)
{
await using var context = _factory.CreateDbContext();
supplier.CreatedAt = DateTime.UtcNow;
_context.Suppliers.Add(supplier);
await _context.SaveChangesAsync();
context.Suppliers.Add(supplier);
await context.SaveChangesAsync();
return supplier;
}
public async Task UpdateAsync(Supplier supplier)
{
_context.Suppliers.Update(supplier);
await _context.SaveChangesAsync();
await using var context = _factory.CreateDbContext();
context.Suppliers.Update(supplier);
await context.SaveChangesAsync();
}
public async Task DeleteAsync(int id)
{
var supplier = await _context.Suppliers.FindAsync(id);
await using var context = _factory.CreateDbContext();
var supplier = await context.Suppliers.FindAsync(id);
if (supplier != null)
{
supplier.IsActive = false;
await _context.SaveChangesAsync();
await context.SaveChangesAsync();
}
}
// Offering management
public async Task<List<SupplierOffering>> GetOfferingsForSupplierAsync(int supplierId)
{
return await _context.SupplierOfferings
await using var context = _factory.CreateDbContext();
return await context.SupplierOfferings
.Include(o => o.StockItem)
.ThenInclude(si => si.Material)
.Where(o => o.SupplierId == supplierId && o.IsActive)
@@ -71,7 +77,8 @@ public class SupplierService
public async Task<List<SupplierOffering>> GetOfferingsForStockItemAsync(int stockItemId)
{
return await _context.SupplierOfferings
await using var context = _factory.CreateDbContext();
return await context.SupplierOfferings
.Include(o => o.Supplier)
.Where(o => o.StockItemId == stockItemId && o.IsActive && o.Supplier.IsActive)
.OrderBy(o => o.Supplier.Name)
@@ -80,7 +87,8 @@ public class SupplierService
public async Task<SupplierOffering?> GetOfferingByIdAsync(int id)
{
return await _context.SupplierOfferings
await using var context = _factory.CreateDbContext();
return await context.SupplierOfferings
.Include(o => o.StockItem)
.ThenInclude(si => si.Material)
.Include(o => o.Supplier)
@@ -89,30 +97,34 @@ public class SupplierService
public async Task<SupplierOffering> AddOfferingAsync(SupplierOffering offering)
{
_context.SupplierOfferings.Add(offering);
await _context.SaveChangesAsync();
await using var context = _factory.CreateDbContext();
context.SupplierOfferings.Add(offering);
await context.SaveChangesAsync();
return offering;
}
public async Task UpdateOfferingAsync(SupplierOffering offering)
{
_context.SupplierOfferings.Update(offering);
await _context.SaveChangesAsync();
await using var context = _factory.CreateDbContext();
context.SupplierOfferings.Update(offering);
await context.SaveChangesAsync();
}
public async Task DeleteOfferingAsync(int id)
{
var offering = await _context.SupplierOfferings.FindAsync(id);
await using var context = _factory.CreateDbContext();
var offering = await context.SupplierOfferings.FindAsync(id);
if (offering != null)
{
offering.IsActive = false;
await _context.SaveChangesAsync();
await context.SaveChangesAsync();
}
}
public async Task<bool> OfferingExistsAsync(int supplierId, int stockItemId, int? excludeId = null)
{
var query = _context.SupplierOfferings.Where(o =>
await using var context = _factory.CreateDbContext();
var query = context.SupplierOfferings.Where(o =>
o.SupplierId == supplierId &&
o.StockItemId == stockItemId &&
o.IsActive);
+2 -1
View File
@@ -2,11 +2,12 @@
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net8.0-windows</TargetFramework>
<TargetFramework>net10.0-windows</TargetFramework>
<UseWindowsForms>true</UseWindowsForms>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<LangVersion>latest</LangVersion>
<NoWarn>WFO1000</NoWarn>
<ApplicationIcon />
<StartupObject />
+1 -1
View File
@@ -16,7 +16,7 @@ Param(
[string]$PublishConfiguration = "Release",
[string]$InstallDir = "C:\Services\CutListWeb",
[string]$Urls = "http://*:5270",
[switch]$OpenFirewall,
[bool]$OpenFirewall = $true,
[int]$ServiceStopTimeoutSeconds = 30,
[int]$ServiceStartTimeoutSeconds = 30
)