804 lines
30 KiB
C#
804 lines
30 KiB
C#
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<List<BillOfMaterialsDto>> 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<BillOfMaterialsDto?> 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<BillOfMaterialsDto> 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<BillOfMaterialsDto> 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<List<ProductionOrderDto>> 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<ProductionOrderDto?> 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<ProductionOrderDto> 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<ProductionOrderDto> 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<ProductionOrderDto> 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<ProductionOrderDto> 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<int> GetDefaultWarehouseIdAsync()
|
|
{
|
|
var wh = await _warehouseService.GetDefaultWarehouseAsync();
|
|
return wh?.Id ?? throw new InvalidOperationException("No default warehouse found");
|
|
}
|
|
|
|
// ===============================================
|
|
// WORK CENTERS
|
|
// ===============================================
|
|
|
|
public async Task<List<WorkCenterDto>> 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<WorkCenterDto?> 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<WorkCenterDto> 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<WorkCenterDto> 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<List<ProductionCycleDto>> 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<ProductionCycleDto?> 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<ProductionCycleDto> 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<ProductionCycleDto> 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();
|
|
}
|
|
}
|
|
}
|