using Dapr; using GB5Shared.DTO.Framework.Login; using Microsoft.AspNetCore.Mvc; using Newtonsoft.Json; using TMSBLL.SkillGapLink; using TMSDAL.DTO.SkillGapLink; using GapSeverityEnum = TMSDAL.Enums.GapSeverity; namespace TMSSL.Subscriptions; // Cross-module producer: WI's WiSkillGapBLL.RaiseTrainingNeedsForRosterGaps publishes one // wi.skillgap.detected event per Warn/Block-enforcement skill gap it finds on a roster. This // subscriber upserts a TSKILLGAPLINK row (deduped by EmployeeId+SkillId against any still-active // link) via ISkillGapLinkBLL — which in turn auto-raises a TMS Training Need for High/Critical // severity gaps still Open (see SkillGapLinkBLL.AutoRaiseTrainingNeedIfNeeded). This is the // template other producer modules (PMS, OKR, Quality) can follow once they have their own // gap/need-detection logic: publish a "{module}.something.detected" event, add a small subscriber // here that maps into either SkillGapLink or directly into ITrainingNeedBLL.ProposeTrainingNeed. [ApiController] public class WiSkillGapSubscriber : ControllerBase { private readonly ISkillGapLinkBLL _skillGapLinkBll; private readonly ILogger _logger; public WiSkillGapSubscriber(ISkillGapLinkBLL skillGapLinkBll, ILogger logger) { _skillGapLinkBll = skillGapLinkBll; _logger = logger; } [Topic("pubsub", "wi.skillgap.detected")] [HttpPost("/TMS/Subscribe/WiSkillGapDetected")] public async Task HandleWiSkillGapDetectedAsync( [FromBody] WiSkillGapDetectedEvent evt, CancellationToken ct) { _logger.LogInformation( "wi.skillgap.detected received: EmployeeId {EmployeeId} SkillId {SkillId} Enforcement {Enforcement} GapStatus {GapStatus}", evt.EmployeeId, evt.SkillId, evt.Enforcement, evt.GapStatus); try { var login = new LoginDTO { ClientId = evt.ClientId, DatabaseName = evt.DatabaseName, UserId = evt.UserId }; // Enforcement: Log=0 (never published by WI), Warn=1, Block=2. byte severity = evt.Enforcement == 2 ? (byte)GapSeverityEnum.Critical : (byte)GapSeverityEnum.High; var existingId = await _skillGapLinkBll.FindActiveLinkId(evt.EmployeeId, evt.SkillId, login, ct); SkillGapLinkDTO dto; if (existingId != 0) { // Already tracked (and possibly already NeedRaised/TrainingScheduled) — refresh // the detected levels/severity but leave GapStatus/AutoRaisedNeedId untouched so // SaveSkillGapLink's auto-raise logic (which only fires when GapStatus == Open) // doesn't re-trigger a second Training Need for an already-raised gap. var existingJson = await _skillGapLinkBll.GetSkillGapLink(existingId, login); dto = JsonConvert.DeserializeObject(existingJson) ?? new SkillGapLinkDTO { SkillGapLinkId = existingId, EmployeeId = evt.EmployeeId, SkillId = evt.SkillId }; dto.CurrentLevelNo = evt.CurrentLevel ?? dto.CurrentLevelNo; dto.RequiredLevelNo = evt.RequiredLevel; dto.GapSeverity = severity; dto.DetectedOn = DateTime.UtcNow; } else { dto = new SkillGapLinkDTO { SkillGapLinkId = 0, EmployeeId = evt.EmployeeId, EmployeeName = evt.EmployeeName, SkillId = evt.SkillId, SkillName = evt.SkillName, CurrentLevelNo = evt.CurrentLevel ?? 0, RequiredLevelNo = evt.RequiredLevel, GapSeverity = severity, DetectedOn = DateTime.UtcNow, GapStatus = 0, // Open — SaveSkillGapLink will auto-raise if severity is High/Critical Remarks = $"Detected via Work Instruction WiId={evt.WiId} on ProductionLineId={evt.ProductionLineId}, roster {evt.RosterDate:yyyy-MM-dd} shift {evt.ShiftId}." }; } await _skillGapLinkBll.SaveSkillGapLink(dto, login, ct); return Ok(); } catch (Exception ex) { _logger.LogError(ex, "Failed to process wi.skillgap.detected for EmployeeId {EmployeeId} SkillId {SkillId}", evt.EmployeeId, evt.SkillId); return StatusCode(500, ex.Message); } } } public sealed class WiSkillGapDetectedEvent { public int EmployeeId { get; set; } public string? EmployeeName { get; set; } public int SkillId { get; set; } public string? SkillName { get; set; } public short RequiredLevel { get; set; } public short? CurrentLevel { get; set; } public byte Enforcement { get; set; } public string? GapStatus { get; set; } public int WiId { get; set; } public string? WiTitle { get; set; } public int ProductionLineId { get; set; } public DateOnly RosterDate { get; set; } public int ShiftId { get; set; } public int TenantId { get; set; } public string DatabaseName { get; set; } = string.Empty; public int ClientId { get; set; } public int UserId { get; set; } }