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