using GB5Shared.DTO.Framework.Login; using Microsoft.AspNetCore.SignalR; using MMBLL.Production; using MMDAL.DTO.Production; namespace MMSL.Hubs { // Group naming: // Operators: "production:wc:{workCenterId}" // Supervisors: "production:supervisor:ou:{workOUId}" public class OperatorConsoleHub : Hub { private readonly IProductionBLL _productionBll; private readonly IResourcePunchBLL _resourcePunchBll; private readonly ILogger _logger; public OperatorConsoleHub( IProductionBLL productionBll, IResourcePunchBLL resourcePunchBll, ILogger logger) { _productionBll = productionBll; _resourcePunchBll = resourcePunchBll; _logger = logger; } // ── Connection lifecycle ────────────────────────────────────────────── public override async Task OnConnectedAsync() { var login = GetLoginDTO(); var workCenterId = GetWorkCenterIdFromQuery(); if (workCenterId > 0) await Groups.AddToGroupAsync(Context.ConnectionId, OperatorGroup(workCenterId)); await Groups.AddToGroupAsync(Context.ConnectionId, SupervisorGroup(login.WorkOUId)); _logger.LogInformation("OperatorConsoleHub connected: {ConnectionId} user {UserId} WC {WorkCenterId}", Context.ConnectionId, login.UserId, workCenterId); await base.OnConnectedAsync(); } public override async Task OnDisconnectedAsync(Exception? exception) { var login = GetLoginDTO(); var workCenterId = GetWorkCenterIdFromQuery(); if (workCenterId > 0) await Groups.RemoveFromGroupAsync(Context.ConnectionId, OperatorGroup(workCenterId)); await Groups.RemoveFromGroupAsync(Context.ConnectionId, SupervisorGroup(login.WorkOUId)); if (exception is not null) _logger.LogWarning(exception, "OperatorConsoleHub disconnected with error: {ConnectionId}", Context.ConnectionId); await base.OnDisconnectedAsync(exception); } // ── Hub methods ─────────────────────────────────────────────────────── public async Task JoinWorkCenter(int workCenterId, CancellationToken ct) { var login = GetLoginDTO(); try { await Groups.RemoveFromGroupAsync(Context.ConnectionId, GetWorkCenterIdFromQuery() > 0 ? OperatorGroup(GetWorkCenterIdFromQuery()) : string.Empty); await Groups.AddToGroupAsync(Context.ConnectionId, OperatorGroup(workCenterId), ct); } catch (Exception ex) { _logger.LogError(ex, "JoinWorkCenter failed for user {UserId} WC {WorkCenterId}", login.UserId, workCenterId); await Clients.Caller.ReceiveError("Failed to join work center group."); } } public async Task LogStoppage(StoppageAlertDTO dto, CancellationToken ct) { var login = GetLoginDTO(); try { await Clients.Group(OperatorGroup(dto.WorkCenterId)).ReceiveStoppageAlert(dto); await Clients.Group(SupervisorGroup(login.WorkOUId)).ReceiveStoppageAlert(dto); } catch (Exception ex) { _logger.LogError(ex, "LogStoppage failed for user {UserId}", login.UserId); await Clients.Caller.ReceiveError("Failed to broadcast stoppage alert."); } } public async Task CompleteStage(StageCompleteDTO dto, CancellationToken ct) { var login = GetLoginDTO(); try { await Clients.Group(SupervisorGroup(login.WorkOUId)).ReceiveStageComplete(dto); } catch (Exception ex) { _logger.LogError(ex, "CompleteStage failed for user {UserId}", login.UserId); await Clients.Caller.ReceiveError("Failed to broadcast stage completion."); } } // ── Helpers ─────────────────────────────────────────────────────────── private LoginDTO GetLoginDTO() { var httpContext = Context.GetHttpContext() ?? throw new InvalidOperationException("Hub invoked outside HTTP context."); return MRPRunHub.FromHttpContext(httpContext); } private int GetWorkCenterIdFromQuery() { var qs = Context.GetHttpContext()?.Request.Query["workCenterId"]; return int.TryParse(qs, out var id) ? id : 0; } public static string OperatorGroup(int workCenterId) => $"production:wc:{workCenterId}"; public static string SupervisorGroup(int workOUId) => $"production:supervisor:ou:{workOUId}"; } }