using GB5Shared.DaprCache; using GB5Shared.DTO.Framework.Criteria; using GB5Shared.DTO.Framework.Login; using GB5Shared.EntityHandler; using GB5Shared.GenerateAutoNumber; using GB5Shared.QueryExecutor; using QMSDAL.CustomCode.Checklist; using QMSDAL.CustomCode.ChecklistLog; using QMSDAL.DTO; using QMSDAL.Interfaces; using static GB5Shared.GB5Constant.Constant; namespace QMSBLL.ChecklistLog { public class ChecklistLogBLL : IChecklistLogBLL { private readonly IChecklistLogDAL _ChecklistLogDAL; private readonly IChecklistDAL _ChecklistDAL; private readonly AutoNumber _AutoNumber; private readonly IQueryExecutor _QueryExecutor; private readonly KeyInvalidate _KeyInvalidate; private readonly BaseEntityAppService _BaseEntityAppService; public ChecklistLogBLL( IChecklistLogDAL checklistLogDAL, IChecklistDAL checklistDAL, AutoNumber autoNumber, IQueryExecutor queryExecutor, KeyInvalidate keyInvalidate, BaseEntityAppService baseEntityAppService) { _ChecklistLogDAL = checklistLogDAL; _ChecklistDAL = checklistDAL; _AutoNumber = autoNumber; _QueryExecutor = queryExecutor; _KeyInvalidate = keyInvalidate; _BaseEntityAppService = baseEntityAppService; } // ── Get single log header ───────────────────────────────────────────── public async Task GetChecklistLogAsync( int checklistLogId, LoginDTO login, CancellationToken ct = default) { try { var header = await _ChecklistLogDAL .GetChecklistLogAsync(checklistLogId, login, ct).ConfigureAwait(false); if (header != null) { var details = await _ChecklistLogDAL .GetChecklistLogDetailAsync(checklistLogId, login, ct).ConfigureAwait(false); header.ChecklistLogDetailArray = details?.ToList() ?? new List(); } return header; } catch (Exception) { throw; } } // ── Get paged log list ──────────────────────────────────────────────── public async Task> GetChecklistLogListAsync( int checklistId, int overallResult, DateTime? fromDate, DateTime? toDate, int pageOffset, int pageSize, LoginDTO login, CancellationToken ct = default) { try { return await _ChecklistLogDAL .GetChecklistLogListAsync( checklistId, overallResult, fromDate, toDate, pageOffset, pageSize, login, ct) .ConfigureAwait(false); } catch (Exception) { throw; } } // ── Get detail rows for a log ───────────────────────────────────────── public async Task> GetChecklistLogDetailAsync( int checklistLogId, LoginDTO login, CancellationToken ct = default) { try { return await _ChecklistLogDAL .GetChecklistLogDetailAsync(checklistLogId, login, ct).ConfigureAwait(false); } catch (Exception) { throw; } } // ── Save checklist log ──────────────────────────────────────────────── public async Task SaveChecklistLogAsync( ChecklistLogDTO dto, LoginDTO login, CancellationToken ct = default) { if (dto == null) throw new ArgumentNullException(nameof(dto)); // Validate: checklist must exist and be active var checklist = await _ChecklistDAL .GetChecklistAsync(dto.ChecklistId, login, ct).ConfigureAwait(false); if (checklist == null || checklist.Status != 1) return $"Checklist {dto.ChecklistId} does not exist or is not active."; // Validate: skipped log must have a reason if (dto.OverallResult == 4 && string.IsNullOrWhiteSpace(dto.SkippedReason)) return "A reason is required when skipping a checklist."; var details = dto.ChecklistLogDetailArray; // Compute server-side RESULT per item (override client values) var checklistWithDetail = await GetChecklistAsync(dto.ChecklistId, login, ct) .ConfigureAwait(false); var definitionMap = (checklistWithDetail?.ChecklistDetailArray ?? Enumerable.Empty()) .ToDictionary(d => d.ChecklistDetailId); foreach (var detail in details) { if (definitionMap.TryGetValue(detail.ChecklistDetailId, out var def)) { detail.ActionType = def.ActionType; detail.FailAction = def.FailAction; detail.IsMandatory = def.IsMandatory; detail.Result = ComputeItemResult(def, detail); } else { // Handle missing definition - set default values detail.ActionType = 1; detail.FailAction = 0; detail.IsMandatory = 0; detail.Result = 1; // Pass by default } } // Recompute OVERALLRESULT server-side — always override client submission dto.OverallResult = dto.OverallResult == 4 ? 4 // Skipped — preserve skip intent : ComputeOverallResult(details, definitionMap); // FAILACTION=2 (Block) check — refuse save if any blocking item fails foreach (var detail in details) { if (detail.Result == 2 && detail.FailAction == 2) { var defText = definitionMap.TryGetValue(detail.ChecklistDetailId, out var bd) ? bd.DetailText : detail.ChecklistDetailId.ToString(); return $"Checklist blocked: '{defText}' failed and is set to Block."; } } // ─── SET AUDIT FIELDS ─── DateTime now = DateTime.UtcNow; int userId = login.UserId; bool isNew = dto.ChecklistLogId == 0; // Set header audit fields if (isNew) { dto.CreatedById = userId; dto.CreatedOn = now; dto.ModifiedById = userId; dto.ModifiedOn = now; dto.SortOrder = 9999; dto.Version = 1; dto.SourceType = 5; } else { dto.ModifiedById = userId; dto.ModifiedOn = now; } // Ensure TenantId is set if (dto.TenantId == 0 || dto.TenantId == -1) dto.TenantId = login.ClientId; // Set detail audit fields foreach (var detail in details) { detail.ModifiedById = userId; detail.ModifiedOn = now; detail.CreatedById = userId; detail.CreatedOn = now; detail.SortOrder = 9999; detail.Status = 1; detail.Version = 1; detail.SourceType = 5; if (detail.TenantId == 0 || detail.TenantId == -1) detail.TenantId = login.ClientId; } var Trans = await _QueryExecutor.BeginTransactionAsync(login); try { // ─── DIRECT SAVE (Bypass BaseEntityAppService) ─── if (isNew) { // Save header and get generated ID int generatedId = await _ChecklistLogDAL.SaveChecklistLogAndGetIdAsync( dto, login, Trans, ct).ConfigureAwait(false); dto.ChecklistLogId = generatedId; // Assign detail IDs int offset = 1; foreach (var detail in details) { detail.ChecklistLogDetailId = 0; // IDENTITY will generate detail.ChecklistLogId = generatedId; detail.TenantId = login.ClientId; offset++; } } else { // Update existing await _ChecklistLogDAL.UpdateChecklistLogAsync(dto, login, Trans, ct) .ConfigureAwait(false); } // Save details await _ChecklistLogDAL.SaveChecklistLogDetailsAsync( details, login, Trans, ct).ConfigureAwait(false); await _QueryExecutor.CommitAsync(Trans); // ─── CACHE INVALIDATION (with error handling) ─── try { var cacheKey = new CacheKeyGeneration().KeyGeneration( dto.ChecklistLogId, EntityConstant.OBJECTCHECKLISTLOG, CacheKeyLevel.NOT_REQUIRED, login); await _KeyInvalidate.AllInvalidateCache(cacheKey); } catch (Exception ex) { // Cache error - log but don't fail // _logger.LogWarning($"Cache invalidation failed: {ex.Message}"); // Continue - cache is not critical for the save operation } // Post-save actions _ = HandlePostSaveActionsAsync(details, dto, login); return $"Details saved successfully with id = {dto.ChecklistLogId}"; } catch (Exception) { await _QueryExecutor.RollbackAsync(Trans); throw; } } // ── Private helpers ─────────────────────────────────────────────────── private async Task GetChecklistAsync( int checklistId, LoginDTO login, CancellationToken ct) { try { var checklist = await _ChecklistDAL .GetChecklistAsync(checklistId, login, ct).ConfigureAwait(false); return checklist; } catch { return null; } } /// /// Computes the RESULT for a single checklist log detail item. /// 0 = Pending (mandatory, no value supplied) /// 1 = Pass /// 2 = Fail /// 3 = NA (optional, no value supplied) /// private static int ComputeItemResult( ChecklistDetailDTO def, ChecklistLogDetailDTO response) { bool hasValue = def.ActionType switch { 1 => response.InputValue.HasValue, 2 => response.InputValue.HasValue, 3 => !string.IsNullOrWhiteSpace(response.InputText), 4 => response.InputId != -1, 5 => response.AttachmentId != -1, 6 => !string.IsNullOrWhiteSpace(response.InputText), _ => false }; if (!hasValue) return def.IsMandatory == 1 ? 0 : 3; return def.ActionType switch { 1 => response.InputValue == 1.0m ? 1 : 2, 2 => // Numeric range check IsOutOfRange(response.InputValue.Value, (decimal?)def.MinVal, (decimal?)def.MaxVal) ? 2 : 1, _ => 1 // Text, Select, Photo, Signature — any value = Pass }; } private static bool IsOutOfRange(decimal value, decimal? minVal, decimal? maxVal) { bool hasMin = minVal.HasValue; bool hasMax = maxVal.HasValue; return (hasMin && value < minVal.Value) || (hasMax && value > maxVal.Value); } /// /// Recomputes OVERALLRESULT from all detail results. /// 0 = Pending (any mandatory item unanswered) /// 1 = Pass (all mandatory answered, no fails) /// 2 = Fail (any item failed) /// 3 = Partial (all mandatory answered, some optional pending) /// private static int ComputeOverallResult( IEnumerable details, Dictionary definitionMap) { var detailList = details.ToList(); // Any mandatory item still pending → Pending(0) bool anyMandatoryPending = detailList.Any(d => d.Result == 0 && definitionMap.TryGetValue(d.ChecklistDetailId, out var def) && def.IsMandatory == 1); if (anyMandatoryPending) return 0; // Any fail → Fail(2) if (detailList.Any(d => d.Result == 2)) return 2; // All mandatory answered; any optional still pending → Partial(3) bool anyOptionalPending = detailList.Any(d => d.Result == 0 && definitionMap.TryGetValue(d.ChecklistDetailId, out var def) && def.IsMandatory == 0); if (anyOptionalPending) return 3; // All answered, no fails → Pass(1) return 1; } /// /// Fire-and-forget post-save actions for FAILACTION=3 (alert) and FAILACTION=4 (NCR). /// Exceptions are swallowed to avoid disrupting the caller — these are advisory only. /// private static Task HandlePostSaveActionsAsync( IEnumerable details, ChecklistLogDTO log, LoginDTO login) { // Identify items requiring post-save actions var alertItems = details .Where(d => d.Result == 2 && d.FailAction == 3) .ToList(); var ncrItems = details .Where(d => d.Result == 2 && d.FailAction == 4) .ToList(); // TODO: inject ISupervisorAlertService and INcrService when those modules exist. // For now these are no-ops — the pattern is correct. // _ = supervisorAlertService.NotifyAsync(alertItems, log, login); // _ = ncrService.EnqueueAsync(ncrItems, log, login); return Task.CompletedTask; } public async Task GetSelectListChecklistLog(CriteriaDTO CriteriaDTO, LoginDTO LoginDTO) { try { return await _ChecklistLogDAL.GetSelectListChecklistLog(CriteriaDTO, LoginDTO); } catch (Exception) { throw; } } } }