using Zentral.API.Modules.Production.Dtos; using Zentral.API.Modules.Warehouse.Services; using Zentral.Domain.Entities.Production; using Zentral.Domain.Entities.Warehouse; using Zentral.Infrastructure.Data; using Microsoft.EntityFrameworkCore; namespace Zentral.API.Modules.Production.Services; public class ProductionService : IProductionService { private readonly ZentralDbContext _context; private readonly IWarehouseService _warehouseService; public ProductionService(ZentralDbContext context, IWarehouseService warehouseService) { _context = context; _warehouseService = warehouseService; } // =============================================== // BILL OF MATERIALS // =============================================== public async Task> GetBillOfMaterialsAsync() { return await _context.BillOfMaterials .Include(b => b.Article) .Include(b => b.Components) .ThenInclude(c => c.ComponentArticle) .Where(b => b.IsActive) .Select(b => new BillOfMaterialsDto { Id = b.Id, Name = b.Name, Description = b.Description, ArticleId = b.ArticleId, ArticleName = b.Article.Description, Quantity = b.Quantity, IsActive = b.IsActive, Components = b.Components.Select(c => new BillOfMaterialsComponentDto { Id = c.Id, ComponentArticleId = c.ComponentArticleId, ComponentArticleName = c.ComponentArticle.Description, Quantity = c.Quantity, ScrapPercentage = c.ScrapPercentage }).ToList() }) .ToListAsync(); } public async Task GetBillOfMaterialsByIdAsync(int id) { var bom = await _context.BillOfMaterials .Include(b => b.Article) .Include(b => b.Components) .ThenInclude(c => c.ComponentArticle) .FirstOrDefaultAsync(b => b.Id == id); if (bom == null) return null; return new BillOfMaterialsDto { Id = bom.Id, Name = bom.Name, Description = bom.Description, ArticleId = bom.ArticleId, ArticleName = bom.Article.Description, Quantity = bom.Quantity, IsActive = bom.IsActive, Components = bom.Components.Select(c => new BillOfMaterialsComponentDto { Id = c.Id, ComponentArticleId = c.ComponentArticleId, ComponentArticleName = c.ComponentArticle.Description, Quantity = c.Quantity, ScrapPercentage = c.ScrapPercentage }).ToList() }; } public async Task CreateBillOfMaterialsAsync(CreateBillOfMaterialsDto dto) { var bom = new BillOfMaterials { Name = dto.Name, Description = dto.Description, ArticleId = dto.ArticleId, Quantity = dto.Quantity, IsActive = true }; foreach (var compDto in dto.Components) { bom.Components.Add(new BillOfMaterialsComponent { ComponentArticleId = compDto.ComponentArticleId, Quantity = compDto.Quantity, ScrapPercentage = compDto.ScrapPercentage }); } _context.BillOfMaterials.Add(bom); await _context.SaveChangesAsync(); return await GetBillOfMaterialsByIdAsync(bom.Id) ?? throw new InvalidOperationException("Failed to retrieve created BOM"); } public async Task UpdateBillOfMaterialsAsync(int id, UpdateBillOfMaterialsDto dto) { var bom = await _context.BillOfMaterials .Include(b => b.Components) .FirstOrDefaultAsync(b => b.Id == id); if (bom == null) throw new KeyNotFoundException($"BOM with ID {id} not found"); bom.Name = dto.Name; bom.Description = dto.Description; bom.Quantity = dto.Quantity; bom.IsActive = dto.IsActive; // Update components foreach (var compDto in dto.Components) { if (compDto.IsDeleted) { if (compDto.Id.HasValue) { var compToDelete = bom.Components.FirstOrDefault(c => c.Id == compDto.Id.Value); if (compToDelete != null) { _context.BillOfMaterialsComponents.Remove(compToDelete); } } } else if (compDto.Id.HasValue) { var compToUpdate = bom.Components.FirstOrDefault(c => c.Id == compDto.Id.Value); if (compToUpdate != null) { compToUpdate.ComponentArticleId = compDto.ComponentArticleId; compToUpdate.Quantity = compDto.Quantity; compToUpdate.ScrapPercentage = compDto.ScrapPercentage; } } else { // New component bom.Components.Add(new BillOfMaterialsComponent { ComponentArticleId = compDto.ComponentArticleId, Quantity = compDto.Quantity, ScrapPercentage = compDto.ScrapPercentage }); } } await _context.SaveChangesAsync(); return await GetBillOfMaterialsByIdAsync(id) ?? throw new InvalidOperationException("Failed to retrieve updated BOM"); } public async Task DeleteBillOfMaterialsAsync(int id) { var bom = await _context.BillOfMaterials.FindAsync(id); if (bom != null) { _context.BillOfMaterials.Remove(bom); await _context.SaveChangesAsync(); } } // =============================================== // PRODUCTION ORDERS // =============================================== public async Task> GetProductionOrdersAsync() { return await _context.ProductionOrders .Include(o => o.Article) .Include(o => o.Components) .ThenInclude(c => c.Article) .Include(o => o.Phases) .ThenInclude(p => p.WorkCenter) .OrderByDescending(o => o.StartDate) .Select(o => new ProductionOrderDto { Id = o.Id, Code = o.Code, ArticleId = o.ArticleId, ArticleName = o.Article.Description, Quantity = o.Quantity, StartDate = o.StartDate, EndDate = o.EndDate, DueDate = o.DueDate, Status = o.Status, Notes = o.Notes, Components = o.Components.Select(c => new ProductionOrderComponentDto { Id = c.Id, ArticleId = c.ArticleId, ArticleName = c.Article.Description, RequiredQuantity = c.RequiredQuantity, ConsumedQuantity = c.ConsumedQuantity }).ToList(), Phases = o.Phases.OrderBy(p => p.Sequence).Select(p => new ProductionOrderPhaseDto { Id = p.Id, Sequence = p.Sequence, Name = p.Name, WorkCenterId = p.WorkCenterId, WorkCenterName = p.WorkCenter.Name, Status = p.Status, StartDate = p.StartDate, EndDate = p.EndDate, QuantityCompleted = p.QuantityCompleted, QuantityScrapped = p.QuantityScrapped, EstimatedDurationMinutes = p.EstimatedDurationMinutes, ActualDurationMinutes = p.ActualDurationMinutes }).ToList(), ParentProductionOrderId = o.ParentProductionOrderId, ParentProductionOrderCode = o.ParentProductionOrder != null ? o.ParentProductionOrder.Code : null }) .ToListAsync(); } public async Task GetProductionOrderByIdAsync(int id) { var order = await _context.ProductionOrders .Include(o => o.Article) .Include(o => o.Components) .ThenInclude(c => c.Article) .Include(o => o.Phases) .ThenInclude(p => p.WorkCenter) .FirstOrDefaultAsync(o => o.Id == id); if (order == null) return null; return new ProductionOrderDto { Id = order.Id, Code = order.Code, ArticleId = order.ArticleId, ArticleName = order.Article.Description, Quantity = order.Quantity, StartDate = order.StartDate, EndDate = order.EndDate, DueDate = order.DueDate, Status = order.Status, Notes = order.Notes, Components = order.Components.Select(c => new ProductionOrderComponentDto { Id = c.Id, ArticleId = c.ArticleId, ArticleName = c.Article.Description, RequiredQuantity = c.RequiredQuantity, ConsumedQuantity = c.ConsumedQuantity }).ToList(), Phases = order.Phases.OrderBy(p => p.Sequence).Select(p => new ProductionOrderPhaseDto { Id = p.Id, Sequence = p.Sequence, Name = p.Name, WorkCenterId = p.WorkCenterId, WorkCenterName = p.WorkCenter.Name, Status = p.Status, StartDate = p.StartDate, EndDate = p.EndDate, QuantityCompleted = p.QuantityCompleted, QuantityScrapped = p.QuantityScrapped, EstimatedDurationMinutes = p.EstimatedDurationMinutes, ActualDurationMinutes = p.ActualDurationMinutes }).ToList(), ParentProductionOrderId = order.ParentProductionOrderId, ParentProductionOrderCode = order.ParentProductionOrder?.Code, ChildOrders = order.ChildProductionOrders.Select(c => new ProductionOrderDto { Id = c.Id, Code = c.Code, ArticleId = c.ArticleId, ArticleName = c.Article.Description, Quantity = c.Quantity, StartDate = c.StartDate, DueDate = c.DueDate, Status = c.Status }).ToList() }; } public async Task CreateProductionOrderAsync(CreateProductionOrderDto dto) { var order = new ProductionOrder { Code = $"PO-{DateTime.Now:yyyyMMdd}-{Guid.NewGuid().ToString().Substring(0, 4).ToUpper()}", // Simple code generation ArticleId = dto.ArticleId, Quantity = dto.Quantity, StartDate = dto.StartDate, DueDate = dto.DueDate, Status = ProductionOrderStatus.Draft, Notes = dto.Notes, ParentProductionOrderId = dto.ParentProductionOrderId }; // If BOM is provided, copy components if (dto.BillOfMaterialsId.HasValue) { var bom = await _context.BillOfMaterials .Include(b => b.Components) .FirstOrDefaultAsync(b => b.Id == dto.BillOfMaterialsId.Value); if (bom != null) { // Calculate ratio based on BOM quantity vs Order quantity var ratio = dto.Quantity / bom.Quantity; foreach (var comp in bom.Components) { order.Components.Add(new ProductionOrderComponent { ArticleId = comp.ComponentArticleId, RequiredQuantity = comp.Quantity * ratio, ConsumedQuantity = 0 }); } } } // Copy default production cycle phases var defaultCycle = await _context.ProductionCycles .Include(c => c.Phases) .FirstOrDefaultAsync(c => c.ArticleId == dto.ArticleId && c.IsDefault && c.IsActive); if (defaultCycle != null) { foreach (var phase in defaultCycle.Phases) { order.Phases.Add(new ProductionOrderPhase { Sequence = phase.Sequence, Name = phase.Name, WorkCenterId = phase.WorkCenterId, Status = ProductionPhaseStatus.Pending, EstimatedDurationMinutes = phase.SetupTimeMinutes + (phase.DurationPerUnitMinutes * (int)dto.Quantity), QuantityCompleted = 0, QuantityScrapped = 0 }); } } _context.ProductionOrders.Add(order); await _context.SaveChangesAsync(); // Recursively create child orders if requested if (dto.CreateChildOrders && order.Components.Any()) { foreach (var comp in order.Components) { // Check if component has a BOM (is manufactured) var compBom = await _context.BillOfMaterials .FirstOrDefaultAsync(b => b.ArticleId == comp.ArticleId && b.IsActive); if (compBom != null) { // Create child order var childDto = new CreateProductionOrderDto { ArticleId = comp.ArticleId, Quantity = comp.RequiredQuantity, StartDate = dto.StartDate.AddDays(-1), // Simple scheduling: start 1 day earlier DueDate = dto.StartDate, // Must be ready by parent start Notes = $"Auto-generated for Parent Order {order.Code}", BillOfMaterialsId = compBom.Id, CreateChildOrders = true, // Recursive ParentProductionOrderId = order.Id // Link to parent }; await CreateProductionOrderAsync(childDto); } } } return await GetProductionOrderByIdAsync(order.Id) ?? throw new InvalidOperationException("Failed to retrieve created order"); } public async Task UpdateProductionOrderAsync(int id, UpdateProductionOrderDto dto) { var order = await _context.ProductionOrders.FindAsync(id); if (order == null) throw new KeyNotFoundException($"Order with ID {id} not found"); if (order.Status == ProductionOrderStatus.Completed || order.Status == ProductionOrderStatus.Cancelled) { throw new InvalidOperationException("Cannot update completed or cancelled orders"); } order.Quantity = dto.Quantity; order.StartDate = dto.StartDate; order.DueDate = dto.DueDate; order.Notes = dto.Notes; // Status change logic could be complex, for now just allow simple updates if not final // Ideally status change should be a separate method (which it is) await _context.SaveChangesAsync(); return await GetProductionOrderByIdAsync(id) ?? throw new InvalidOperationException("Failed to retrieve updated order"); } public async Task ChangeProductionOrderStatusAsync(int id, ProductionOrderStatus status) { var order = await _context.ProductionOrders .Include(o => o.Components) .FirstOrDefaultAsync(o => o.Id == id); if (order == null) throw new KeyNotFoundException($"Order with ID {id} not found"); // Simple state machine check if (order.Status == ProductionOrderStatus.Completed) throw new InvalidOperationException("Order is already completed"); if (status == ProductionOrderStatus.Completed) { var defaultWarehouseId = await GetDefaultWarehouseIdAsync(); // Get reasons var prodReason = await _context.MovementReasons.FirstOrDefaultAsync(r => r.Code == "PROD"); var consReason = await _context.MovementReasons.FirstOrDefaultAsync(r => r.Code == "CONS"); // 1. Create Inbound Movement for Finished Good var inboundMovement = new StockMovement { MovementDate = DateTime.UtcNow, Type = MovementType.Production, // Inbound from Production Status = MovementStatus.Draft, // Must be Draft to be confirmed ReasonId = prodReason?.Id, ExternalReference = order.Code, ExternalDocumentType = ExternalDocumentType.ProductionOrder, Notes = $"Production Order {order.Code} Completed", DestinationWarehouseId = defaultWarehouseId }; inboundMovement.Lines.Add(new StockMovementLine { ArticleId = order.ArticleId, Quantity = order.Quantity, LineNumber = 1 }); await _warehouseService.CreateMovementAsync(inboundMovement); await _warehouseService.ConfirmMovementAsync(inboundMovement.Id); // 2. Create Outbound Movement for Components (Consumption) if (order.Components.Any()) { var outboundMovement = new StockMovement { MovementDate = DateTime.UtcNow, Type = MovementType.Consumption, // Outbound for Production Status = MovementStatus.Draft, // Must be Draft to be confirmed ReasonId = consReason?.Id, ExternalReference = order.Code, ExternalDocumentType = ExternalDocumentType.ProductionOrder, Notes = $"Consumption for Production Order {order.Code}", SourceWarehouseId = defaultWarehouseId }; int lineNum = 1; foreach (var comp in order.Components) { outboundMovement.Lines.Add(new StockMovementLine { ArticleId = comp.ArticleId, Quantity = comp.RequiredQuantity, // Consuming required quantity LineNumber = lineNum++ }); // Update consumed quantity on the order component comp.ConsumedQuantity = comp.RequiredQuantity; } await _warehouseService.CreateMovementAsync(outboundMovement); await _warehouseService.ConfirmMovementAsync(outboundMovement.Id); } order.EndDate = DateTime.Now; } order.Status = status; await _context.SaveChangesAsync(); return await GetProductionOrderByIdAsync(id) ?? throw new InvalidOperationException("Failed to retrieve updated order"); } public async Task UpdateProductionOrderPhaseAsync(int orderId, int phaseId, UpdateProductionOrderPhaseDto dto) { var phase = await _context.ProductionOrderPhases .FirstOrDefaultAsync(p => p.Id == phaseId && p.ProductionOrderId == orderId); if (phase == null) throw new KeyNotFoundException($"Phase with ID {phaseId} not found in order {orderId}"); phase.Status = dto.Status; phase.QuantityCompleted = dto.QuantityCompleted; phase.QuantityScrapped = dto.QuantityScrapped; phase.ActualDurationMinutes = dto.ActualDurationMinutes; if (dto.Status == ProductionPhaseStatus.InProgress && phase.StartDate == null) { phase.StartDate = DateTime.Now; } else if (dto.Status == ProductionPhaseStatus.Completed && phase.EndDate == null) { phase.EndDate = DateTime.Now; } await _context.SaveChangesAsync(); return await GetProductionOrderByIdAsync(orderId) ?? throw new InvalidOperationException("Failed to retrieve updated order"); } public async Task DeleteProductionOrderAsync(int id) { var order = await _context.ProductionOrders.FindAsync(id); if (order != null) { if (order.Status != ProductionOrderStatus.Draft) throw new InvalidOperationException("Only draft orders can be deleted"); _context.ProductionOrders.Remove(order); await _context.SaveChangesAsync(); } } private async Task GetDefaultWarehouseIdAsync() { var wh = await _warehouseService.GetDefaultWarehouseAsync(); return wh?.Id ?? throw new InvalidOperationException("No default warehouse found"); } // =============================================== // WORK CENTERS // =============================================== public async Task> GetWorkCentersAsync() { return await _context.WorkCenters .Where(w => w.IsActive) .Select(w => new WorkCenterDto { Id = w.Id, Code = w.Code, Name = w.Name, Description = w.Description, CostPerHour = w.CostPerHour, IsActive = w.IsActive }) .ToListAsync(); } public async Task GetWorkCenterByIdAsync(int id) { var w = await _context.WorkCenters.FindAsync(id); if (w == null) return null; return new WorkCenterDto { Id = w.Id, Code = w.Code, Name = w.Name, Description = w.Description, CostPerHour = w.CostPerHour, IsActive = w.IsActive }; } public async Task CreateWorkCenterAsync(CreateWorkCenterDto dto) { if (await _context.WorkCenters.AnyAsync(w => w.Code == dto.Code)) throw new InvalidOperationException($"Work center with code {dto.Code} already exists"); var workCenter = new WorkCenter { Code = dto.Code, Name = dto.Name, Description = dto.Description, CostPerHour = dto.CostPerHour, IsActive = true }; _context.WorkCenters.Add(workCenter); await _context.SaveChangesAsync(); return await GetWorkCenterByIdAsync(workCenter.Id) ?? throw new InvalidOperationException("Failed to retrieve created work center"); } public async Task UpdateWorkCenterAsync(int id, UpdateWorkCenterDto dto) { var workCenter = await _context.WorkCenters.FindAsync(id); if (workCenter == null) throw new KeyNotFoundException($"Work center with ID {id} not found"); workCenter.Name = dto.Name; workCenter.Description = dto.Description; workCenter.CostPerHour = dto.CostPerHour; workCenter.IsActive = dto.IsActive; await _context.SaveChangesAsync(); return await GetWorkCenterByIdAsync(id) ?? throw new InvalidOperationException("Failed to retrieve updated work center"); } public async Task DeleteWorkCenterAsync(int id) { var workCenter = await _context.WorkCenters.FindAsync(id); if (workCenter != null) { // Check if used in any cycle or order phase if (await _context.ProductionCyclePhases.AnyAsync(p => p.WorkCenterId == id)) throw new InvalidOperationException("Cannot delete work center used in production cycles"); if (await _context.ProductionOrderPhases.AnyAsync(p => p.WorkCenterId == id)) throw new InvalidOperationException("Cannot delete work center used in production orders"); _context.WorkCenters.Remove(workCenter); await _context.SaveChangesAsync(); } } // =============================================== // PRODUCTION CYCLES // =============================================== public async Task> GetProductionCyclesAsync() { return await _context.ProductionCycles .Include(c => c.Article) .Include(c => c.Phases) .ThenInclude(p => p.WorkCenter) .Where(c => c.IsActive) .Select(c => new ProductionCycleDto { Id = c.Id, Name = c.Name, Description = c.Description, ArticleId = c.ArticleId, ArticleName = c.Article.Description, IsDefault = c.IsDefault, IsActive = c.IsActive, Phases = c.Phases.OrderBy(p => p.Sequence).Select(p => new ProductionCyclePhaseDto { Id = p.Id, Sequence = p.Sequence, Name = p.Name, Description = p.Description, WorkCenterId = p.WorkCenterId, WorkCenterName = p.WorkCenter.Name, DurationPerUnitMinutes = p.DurationPerUnitMinutes, SetupTimeMinutes = p.SetupTimeMinutes }).ToList() }) .ToListAsync(); } public async Task GetProductionCycleByIdAsync(int id) { var c = await _context.ProductionCycles .Include(c => c.Article) .Include(c => c.Phases) .ThenInclude(p => p.WorkCenter) .FirstOrDefaultAsync(x => x.Id == id); if (c == null) return null; return new ProductionCycleDto { Id = c.Id, Name = c.Name, Description = c.Description, ArticleId = c.ArticleId, ArticleName = c.Article.Description, IsDefault = c.IsDefault, IsActive = c.IsActive, Phases = c.Phases.OrderBy(p => p.Sequence).Select(p => new ProductionCyclePhaseDto { Id = p.Id, Sequence = p.Sequence, Name = p.Name, Description = p.Description, WorkCenterId = p.WorkCenterId, WorkCenterName = p.WorkCenter.Name, DurationPerUnitMinutes = p.DurationPerUnitMinutes, SetupTimeMinutes = p.SetupTimeMinutes }).ToList() }; } public async Task CreateProductionCycleAsync(CreateProductionCycleDto dto) { var cycle = new ProductionCycle { Name = dto.Name, Description = dto.Description, ArticleId = dto.ArticleId, IsDefault = dto.IsDefault, IsActive = true }; if (dto.IsDefault) { // Unset other defaults for this article var defaults = await _context.ProductionCycles .Where(c => c.ArticleId == dto.ArticleId && c.IsDefault) .ToListAsync(); foreach (var d in defaults) d.IsDefault = false; } foreach (var phaseDto in dto.Phases) { cycle.Phases.Add(new ProductionCyclePhase { Sequence = phaseDto.Sequence, Name = phaseDto.Name, Description = phaseDto.Description, WorkCenterId = phaseDto.WorkCenterId, DurationPerUnitMinutes = phaseDto.DurationPerUnitMinutes, SetupTimeMinutes = phaseDto.SetupTimeMinutes }); } _context.ProductionCycles.Add(cycle); await _context.SaveChangesAsync(); return await GetProductionCycleByIdAsync(cycle.Id) ?? throw new InvalidOperationException("Failed to retrieve created cycle"); } public async Task UpdateProductionCycleAsync(int id, UpdateProductionCycleDto dto) { var cycle = await _context.ProductionCycles .Include(c => c.Phases) .FirstOrDefaultAsync(c => c.Id == id); if (cycle == null) throw new KeyNotFoundException($"Cycle with ID {id} not found"); cycle.Name = dto.Name; cycle.Description = dto.Description; cycle.IsActive = dto.IsActive; if (dto.IsDefault && !cycle.IsDefault) { // Unset other defaults var defaults = await _context.ProductionCycles .Where(c => c.ArticleId == cycle.ArticleId && c.IsDefault && c.Id != id) .ToListAsync(); foreach (var d in defaults) d.IsDefault = false; } cycle.IsDefault = dto.IsDefault; // Update phases foreach (var phaseDto in dto.Phases) { if (phaseDto.IsDeleted) { if (phaseDto.Id.HasValue) { var phaseToDelete = cycle.Phases.FirstOrDefault(p => p.Id == phaseDto.Id.Value); if (phaseToDelete != null) _context.ProductionCyclePhases.Remove(phaseToDelete); } } else if (phaseDto.Id.HasValue) { var phaseToUpdate = cycle.Phases.FirstOrDefault(p => p.Id == phaseDto.Id.Value); if (phaseToUpdate != null) { phaseToUpdate.Sequence = phaseDto.Sequence; phaseToUpdate.Name = phaseDto.Name; phaseToUpdate.Description = phaseDto.Description; phaseToUpdate.WorkCenterId = phaseDto.WorkCenterId; phaseToUpdate.DurationPerUnitMinutes = phaseDto.DurationPerUnitMinutes; phaseToUpdate.SetupTimeMinutes = phaseDto.SetupTimeMinutes; } } else { cycle.Phases.Add(new ProductionCyclePhase { Sequence = phaseDto.Sequence, Name = phaseDto.Name, Description = phaseDto.Description, WorkCenterId = phaseDto.WorkCenterId, DurationPerUnitMinutes = phaseDto.DurationPerUnitMinutes, SetupTimeMinutes = phaseDto.SetupTimeMinutes }); } } await _context.SaveChangesAsync(); return await GetProductionCycleByIdAsync(id) ?? throw new InvalidOperationException("Failed to retrieve updated cycle"); } public async Task DeleteProductionCycleAsync(int id) { var cycle = await _context.ProductionCycles.FindAsync(id); if (cycle != null) { _context.ProductionCycles.Remove(cycle); await _context.SaveChangesAsync(); } } }