using GB5Shared.DTO.Framework.Login; using MMDAL.CustomCode.Scheduling; using MMDAL.DTO.Scheduling; namespace MMBLL.Scheduling.Engine { // Rough-Cut Capacity Planning: aggregate demand minutes per WorkCenter vs available shift minutes. // Overload threshold: >90% load emits a warning; does NOT prevent scheduling. // Capacity is computed via a single set-based DATETALLY query per run — not a per-machine day loop. public class RCCPService : IRCCPService { private const double OverloadThreshold = 0.90; private readonly ICandidateDAL _CandidateDAL; public RCCPService(ICandidateDAL candidateDAL) { _CandidateDAL = candidateDAL; } public async Task> CheckAsync( IReadOnlyList eus, DateTime horizonFrom, DateTime horizonTo, SchedulingContext context, LoginDTO login, CancellationToken ct) { // Sum required minutes per WorkCenter (IsMonitorOnly: 0=Yes — exclude monitor-only) var requiredByWC = eus .Where(e => e.IsMonitorOnly != 0) .GroupBy(e => e.WorkCenterId) .ToDictionary(g => g.Key, g => g.Sum(e => e.DurationMinutes)); if (requiredByWC.Count == 0) return Array.Empty(); // Single set-based query: net shift minutes × machine count per WC across the full horizon var capacityRows = (await _CandidateDAL .GetRCCPCapacityAsync(requiredByWC.Keys, horizonFrom, horizonTo, login.ClientId, login, ct) .ConfigureAwait(false)) .ToDictionary(r => r.WorkCenterId); var results = new List(); foreach (var (workCenterId, requiredMinutes) in requiredByWC) { capacityRows.TryGetValue(workCenterId, out var cap); int capacityMinutes = cap?.CapacityMinutes ?? 0; decimal overloadPct = capacityMinutes > 0 ? Math.Round((decimal)requiredMinutes / capacityMinutes * 100, 2) : 100m; results.Add(new RCCPResultDTO { WorkCenterId = workCenterId, WorkCenterName = cap?.WorkCenterName ?? string.Empty, CapacityMinutes = capacityMinutes, RequiredMinutes = requiredMinutes, OverloadPct = overloadPct, IsOverloaded = overloadPct > (decimal)(OverloadThreshold * 100) }); } return results; } } }