using GB5Shared.DTO.Framework.AutoNumber; using GB5Shared.DTO.Framework.Criteria; using GB5Shared.DTO.Framework.Login; using GB5Shared.GenerateAutoNumber; using GB5Shared.Resource.Response; using System.ComponentModel.DataAnnotations; using TMSDAL.CustomCode.TrainingEnrollment; using TMSDAL.CustomCode.TrainingInstance; using TMSDAL.DTO.TrainingEnrollment; using TMSDAL.Enums; namespace TMSBLL.TrainingEnrollment { public class TrainingEnrollmentBLL : ITrainingEnrollmentBLL { private readonly ITrainingEnrollmentDAL _TrainingEnrollmentDAL; private readonly ITrainingInstanceDAL _TrainingInstanceDAL; private readonly AutoNumber _AutoNumber; public TrainingEnrollmentBLL( ITrainingEnrollmentDAL trainingEnrollmentDAL, ITrainingInstanceDAL trainingInstanceDAL, AutoNumber autoNumber) { _TrainingEnrollmentDAL = trainingEnrollmentDAL; _TrainingInstanceDAL = trainingInstanceDAL; _AutoNumber = autoNumber; // FIX: IValidation removed — BLL was not using it; only DAL needs it } // ════════════════════════════════════════════════════════════════════ // READ // ════════════════════════════════════════════════════════════════════ public async Task GetTrainingEnrollment(int enrollmentId, LoginDTO loginDTO) { // FIX: Removed pointless catch(Exception){throw;} var result = await _TrainingEnrollmentDAL.GetTrainingEnrollment(enrollmentId, loginDTO); return System.Text.Json.JsonSerializer.Serialize(result); } public async Task GetEnrollmentsByInstance(int instanceId, LoginDTO loginDTO) => await _TrainingEnrollmentDAL.GetEnrollmentsByInstance(instanceId, loginDTO); public async Task GetEnrollmentsByEmployee(int employeeId, LoginDTO loginDTO) => await _TrainingEnrollmentDAL.GetEnrollmentsByEmployee(employeeId, loginDTO); public async Task GetEnrollmentList(LoginDTO loginDTO) => await _TrainingEnrollmentDAL.GetEnrollmentList(loginDTO); public async Task GetSelectList(CriteriaDTO criteriaDTO, LoginDTO loginDTO, CancellationToken ct) => await _TrainingEnrollmentDAL.GetSelectList(criteriaDTO, loginDTO, ct); // ════════════════════════════════════════════════════════════════════ // SAVE // ════════════════════════════════════════════════════════════════════ public async Task SaveTrainingEnrollment(TrainingEnrollmentDTO dto, LoginDTO loginDTO) { if (dto == null) throw new ArgumentNullException(nameof(dto)); AutoNumberDTO? autoNumberDTO = null; try { // ── Validation ─────────────────────────────────────────────── if (dto.InstanceId == 0) throw new ValidationException("InstanceId is required."); if (dto.EmployeeId == 0) throw new ValidationException("EmployeeId is required."); if (!Enum.IsDefined(typeof(NominationSource), dto.NominationSource)) throw new ValidationException($"Invalid NominationSource value: {dto.NominationSource}."); dto.TenantId = loginDTO.ClientId; dto.ModifiedById = loginDTO.UserId; dto.ModifiedOn = DateTime.UtcNow; // ── Duplicate guard ─────────────────────────────────────────── // Uses filtered index query — only active enrollments are checked, // so a previously Withdrawn/Rejected employee can re-enroll. var existing = await _TrainingEnrollmentDAL.GetEnrollment( dto.InstanceId, dto.EmployeeId, loginDTO); if (existing != null) throw new ValidationException( "Employee is already actively enrolled or waitlisted for this training instance."); // ── Instance / capacity check ───────────────────────────────── var instance = await _TrainingInstanceDAL.GetInstanceCapacity(dto.InstanceId, loginDTO); if (instance == null) throw new ValidationException("Training instance not found."); // FIX: WaitlistPosition = null (not 0) when not waitlisted — matches DB NULL column if (instance.MaxCapacity != -1 && instance.EnrolledCount >= instance.MaxCapacity) { dto.EnrollmentStatus = (byte)EnrollmentStatus.Waitlisted; dto.WaitlistPosition = await _TrainingEnrollmentDAL.GetNextWaitlistPosition( dto.InstanceId, loginDTO); } else { dto.EnrollmentStatus = (byte)EnrollmentStatus.Nominated; dto.WaitlistPosition = 0; } // ── AutoNumber ──────────────────────────────────────────────── autoNumberDTO = await _AutoNumber.GetAutoNumber(1, "TRAININGENROLLMENT", loginDTO); dto.EnrollmentId = autoNumberDTO.StartNumber; dto.CreatedById = loginDTO.UserId; dto.CreatedOn = DateTime.UtcNow; dto.NominatedOn = DateTime.UtcNow; // FIX: was never set // ── Insert ──────────────────────────────────────────────────── await _TrainingEnrollmentDAL.SaveTrainingEnrollment(dto, loginDTO); // Only increment enrolled count for directly nominated (not waitlisted) if (dto.EnrollmentStatus == (byte)EnrollmentStatus.Nominated) await _TrainingInstanceDAL.IncrementEnrolledCount(dto.InstanceId, loginDTO); return string.Format(SuccessResponse.SaveSuccessWithCodeAndName, dto.EnrollmentCode, dto.EnrollmentName); } catch (ValidationException) { // Re-throw as-is — preserve type for controller to distinguish from system errors throw; } catch (Exception) { // Rollback AutoNumber if it was already consumed if (autoNumberDTO != null && dto.EnrollmentId > 0) await _AutoNumber.RollbackAutoNumber("TRAININGENROLLMENT", dto.EnrollmentId, loginDTO); throw; } } // ════════════════════════════════════════════════════════════════════ // APPROVE / REJECT // ════════════════════════════════════════════════════════════════════ public async Task ApproveEnrollment(ApprovalActionDTO dto, LoginDTO loginDTO) { // FIX: Parameter changed from ApproveEnrollmentDTO to ApprovalActionDTO // FIX: Uses unified UpdateApproval — not split UpdateManagerApproval/UpdateHRApproval if (dto.EnrollmentId == 0) throw new ValidationException("Please select a valid Enrollment."); var enrollment = await _TrainingEnrollmentDAL.GetTrainingEnrollment(dto.EnrollmentId, loginDTO); if (enrollment == null) throw new ValidationException($"Enrollment {dto.EnrollmentId} not found."); var currentStatus = (EnrollmentStatus)enrollment.EnrollmentStatus; // ── Block terminal statuses ─────────────────────────────────────── if (currentStatus == EnrollmentStatus.Withdrawn) throw new ValidationException("This enrollment has been Withdrawn and cannot be approved."); if (currentStatus is EnrollmentStatus.Rejected or EnrollmentStatus.ManagerRejected or EnrollmentStatus.HRRejected) throw new ValidationException("This enrollment has been Rejected and cannot be approved."); if (currentStatus == EnrollmentStatus.Completed) throw new ValidationException("This enrollment is already Completed."); if (currentStatus == EnrollmentStatus.NoShow) throw new ValidationException("This enrollment is marked as No Show and cannot be approved."); // ── Build the DAL DTO ───────────────────────────────────────────── // ApprovalId = logged-in user (the person actioning the approval) var approvalDto = new ApprovalUpdateDTO { EnrollmentId = dto.EnrollmentId, ApprovalType = dto.ApprovalType, ApprovalId = loginDTO.UserId, Remarks = dto.Remarks }; // ── Route by approval tier ──────────────────────────────────────── if (dto.ApprovalType == ApprovalType.Manager) { if (currentStatus != EnrollmentStatus.Nominated) throw new ValidationException( $"Manager approval requires Nominated status. Current: '{currentStatus}'."); // FIX: Status derived from action — not passed raw from caller approvalDto.Status = dto.Status; // ManagerApproved or ManagerRejected await _TrainingEnrollmentDAL.UpdateApproval(approvalDto, loginDTO); bool approved = dto.Status == EnrollmentStatus.ManagerApproved; return approved ? $"Enrollment {dto.EnrollmentId} has been Manager Approved successfully." : $"Enrollment {dto.EnrollmentId} has been Manager Rejected."; } else // HR { if (currentStatus == EnrollmentStatus.Nominated) throw new ValidationException("Manager approval must be completed before HR approval."); if (currentStatus == EnrollmentStatus.Waitlisted) throw new ValidationException("Waitlisted enrollments cannot proceed to HR approval."); if (currentStatus == EnrollmentStatus.Enrolled) throw new ValidationException("This enrollment is already Enrolled."); if (currentStatus != EnrollmentStatus.ManagerApproved) throw new ValidationException( $"HR approval requires ManagerApproved status. Current: '{currentStatus}'."); approvalDto.Status = dto.Status; // HRApproved → Enrolled, or HRRejected await _TrainingEnrollmentDAL.UpdateApproval(approvalDto, loginDTO); bool approved = dto.Status == EnrollmentStatus.HRApproved; // When HR approves, transition status to Enrolled and update count if (approved) { approvalDto.Status = EnrollmentStatus.Enrolled; await _TrainingEnrollmentDAL.UpdateApproval(approvalDto, loginDTO); await _TrainingInstanceDAL.IncrementEnrolledCount(enrollment.InstanceId, loginDTO); return $"Enrollment {dto.EnrollmentId} has been HR Approved and is now Enrolled."; } return $"Enrollment {dto.EnrollmentId} has been HR Rejected."; } } // ════════════════════════════════════════════════════════════════════ // WITHDRAW // ════════════════════════════════════════════════════════════════════ public async Task WithdrawEnrollment(WithdrawEnrollmentDTO dto, LoginDTO loginDTO) { var enrollment = await _TrainingEnrollmentDAL.GetTrainingEnrollment(dto.EnrollmentId, loginDTO); // FIX: Uncommented null check — crashing with NullReferenceException was worse than a clear message if (enrollment == null) throw new ValidationException($"Enrollment {dto.EnrollmentId} not found."); var withdrawableStatuses = new[] { (byte)EnrollmentStatus.Nominated, (byte)EnrollmentStatus.ManagerApproved, (byte)EnrollmentStatus.HRApproved, (byte)EnrollmentStatus.Waitlisted, (byte)EnrollmentStatus.Enrolled // FIX: Added HRApproved — was missing from original list }; if (!withdrawableStatuses.Contains(enrollment.EnrollmentStatus)) throw new ValidationException( $"Enrollment cannot be withdrawn in its current status: '{(EnrollmentStatus)enrollment.EnrollmentStatus}'."); await _TrainingEnrollmentDAL.WithdrawEnrollment( dto.EnrollmentId, (byte)EnrollmentStatus.Withdrawn, dto.WithdrawalReason, loginDTO); // Decrement count only if a confirmed seat was held // FIX: Added HRApproved — seat is confirmed at that stage too if (enrollment.EnrollmentStatus == (byte)EnrollmentStatus.Enrolled || enrollment.EnrollmentStatus == (byte)EnrollmentStatus.Nominated || enrollment.EnrollmentStatus == (byte)EnrollmentStatus.HRApproved) await _TrainingInstanceDAL.DecrementEnrolledCount(enrollment.InstanceId, loginDTO); return $"{SuccessResponse.UpdateSuccessMessage} {dto.EnrollmentId}"; } // ════════════════════════════════════════════════════════════════════ // DELETE // ════════════════════════════════════════════════════════════════════ public async Task DeleteTrainingEnrollment(int enrollmentId, LoginDTO loginDTO) { int result = await _TrainingEnrollmentDAL.DeleteTrainingEnrollment(enrollmentId, loginDTO); return result > 0 ? SuccessResponse.DeleteSuccessMessage : ErrorResponse.DeleteNotFoundMessage; } } }