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>
This commit is contained in:
@@ -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();
|
||||
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user