using GB5Shared.DTO.Framework.AutoNumber; using GB5Shared.DTO.Framework.Criteria; using GB5Shared.DTO.Framework.Login; using GB5Shared.GenerateAutoNumber; using GB5Shared.Resource.Response; using GB5Shared.Validation; using System.ComponentModel.DataAnnotations; using TMSBLL.TrainingNeed; using TMSDAL.CustomCode.SkillGapLink; using TMSDAL.DTO.SkillGapLink; using TMSDAL.DTO.TrainingNeed; using GapSeverityEnum = TMSDAL.Enums.GapSeverity; using GapStatusEnum = TMSDAL.Enums.GapStatus; using NeedSourceTypeEnum = TMSDAL.Enums.NeedSourceType; using PriorityEnum = TMSDAL.Enums.Priority; namespace TMSBLL.SkillGapLink { public class SkillGapLinkBLL : ISkillGapLinkBLL { private readonly ISkillGapLinkDAL _SkillGapLinkDAL; private readonly AutoNumber _AutoNumber; private readonly IValidation _Validation; private readonly ITrainingNeedBLL _TrainingNeedBLL; public SkillGapLinkBLL(ISkillGapLinkDAL skillGapLinkDAL, AutoNumber autoNumber, IValidation validation, ITrainingNeedBLL trainingNeedBLL) { _SkillGapLinkDAL = skillGapLinkDAL; _AutoNumber = autoNumber; _Validation = validation; _TrainingNeedBLL = trainingNeedBLL; } public async Task GetSkillGapLink(int skillGapLinkId, LoginDTO loginDTO) { try { return await _SkillGapLinkDAL.GetSkillGapLink(skillGapLinkId, loginDTO); } catch (Exception) { throw; } } public async Task GetSkillGapLinkList(CriteriaDTO criteriaDTO, LoginDTO loginDTO) { try { return await _SkillGapLinkDAL.GetSkillGapLinkList(criteriaDTO, loginDTO); } catch (Exception) { throw; } } public async Task GetSelectListSkillGapLink(CriteriaDTO criteriaDTO, LoginDTO loginDTO) { try { return await _SkillGapLinkDAL.GetSelectListSkillGapLink(criteriaDTO, loginDTO); } catch (Exception) { throw; } } public async Task SaveSkillGapLink(SkillGapLinkDTO dto, LoginDTO loginDTO, CancellationToken ct = default) { if (dto == null) throw new ArgumentNullException(nameof(dto)); AutoNumberDTO? autoNumberDTO = null; try { if (dto.SkillId == 0) throw new ValidationException("SkillId is required."); if (dto.EmployeeId == 0) throw new ValidationException("EmployeeId is required."); dto.TenantId = loginDTO.ClientId; dto.ModifiedById = loginDTO.UserId; dto.ModifiedOn = DateTime.UtcNow; if (dto.AutoRaisedNeedId == 0) dto.AutoRaisedNeedId = -1; if (dto.RecommendedProgrammeId == 0) dto.RecommendedProgrammeId = -1; if (dto.DetectedOn == null) dto.DetectedOn = DateTime.UtcNow; if (dto.SkillGapLinkId == 0) { autoNumberDTO = await _AutoNumber.GetAutoNumber(1, "SKILLGAPLINK", loginDTO); dto.SkillGapLinkId = autoNumberDTO.StartNumber; dto.CreatedById = loginDTO.UserId; dto.CreatedOn = DateTime.UtcNow; dto.Version = 1; await AutoRaiseTrainingNeedIfNeeded(dto, loginDTO, ct).ConfigureAwait(false); await _SkillGapLinkDAL.SaveSkillGapLink(dto, loginDTO); return string.Format(SuccessResponse.SaveSuccessWithCodeAndName, dto.SkillCode, dto.SkillName); } else { await AutoRaiseTrainingNeedIfNeeded(dto, loginDTO, ct).ConfigureAwait(false); await _SkillGapLinkDAL.UpdateSkillGapLink(dto, loginDTO); return string.Format(SuccessResponse.UpdateSuccessWithCodeAndName, dto.SkillCode, dto.SkillName); } } catch (ValidationException vex) { throw new Exception(vex.Message); } catch (Exception) { if (autoNumberDTO != null && dto.SkillGapLinkId > 0) await _AutoNumber.RollbackAutoNumber("SKILLGAPLINK", dto.SkillGapLinkId, loginDTO); throw; } } // Auto-raise a TMS Training Need for High/Critical-severity gaps still Open — mutates // dto.AutoRaisedNeedId/GapStatus in place before it's saved so the caller's single // insert/update already reflects the outcome (no extra round-trip). Non-fatal: a Training // Need failure must never block recording the skill gap itself — SaveSkillGapLink/ // UpdateSkillGapLink still proceeds with dto.GapStatus left at Open on failure. private async Task AutoRaiseTrainingNeedIfNeeded(SkillGapLinkDTO dto, LoginDTO loginDTO, CancellationToken ct) { if (dto.GapStatus != (byte)GapStatusEnum.Open) return; if (dto.GapSeverity < (byte)GapSeverityEnum.High) return; try { var proposeDto = new ProposeTrainingNeedDTO { NeedTitle = $"Skill Gap — {dto.SkillName ?? ("Skill #" + dto.SkillId)} ({dto.EmployeeName ?? ("Employee #" + dto.EmployeeId)})", NeedSourceType = (byte)NeedSourceTypeEnum.SkillGap, SourceRefEntity = "TSKILLGAPLINK", SourceRefId = dto.SkillGapLinkId, Priority = dto.GapSeverity == (byte)GapSeverityEnum.Critical ? (byte)PriorityEnum.Critical : (byte)PriorityEnum.High, EmployeeId = dto.EmployeeId, Remarks = $"Auto-raised from skill gap detection (current level {dto.CurrentLevelNo}, required {dto.RequiredLevelNo})." }; var trainingNeedId = await _TrainingNeedBLL.ProposeTrainingNeed(proposeDto, loginDTO, ct).ConfigureAwait(false); dto.AutoRaisedNeedId = trainingNeedId; dto.GapStatus = (byte)GapStatusEnum.NeedRaised; } catch (Exception) { // Swallow — the skill gap record itself must still save even if the Need // couldn't be raised (e.g. tenant hasn't configured a Training Domain yet). } } public async Task DeleteSkillGapLink(int skillGapLinkId, LoginDTO loginDTO) { try { return await _SkillGapLinkDAL.DeleteSkillGapLink(skillGapLinkId, loginDTO); } catch (Exception) { throw; } } public async Task FindActiveLinkId(int employeeId, int skillId, LoginDTO loginDTO, CancellationToken ct) { try { return await _SkillGapLinkDAL.FindActiveLinkId(employeeId, skillId, loginDTO, ct); } catch (Exception) { throw; } } } }