diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistBootstrap/HyperTwistContractLibrary.cpp b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistBootstrap/HyperTwistContractLibrary.cpp index 4d258d9..6aff754 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistBootstrap/HyperTwistContractLibrary.cpp +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistBootstrap/HyperTwistContractLibrary.cpp @@ -628,6 +628,16 @@ FHyperTwistTrainingRepositoryState UHyperTwistContractLibrary::MakeSampleTrainin ); } + const FHyperTwistTrainingCoachSessionQueueState CoachSessionQueueState = + MakeSampleTrainingCoachSessionQueueState(); + if (CoachSessionQueueState.IsStructurallyValid()) + { + RepositoryState = UHyperTwistTrainingRepositoryLibrary::UpsertCoachSessionQueueState( + RepositoryState, + CoachSessionQueueState + ); + } + return RepositoryState; } @@ -686,6 +696,15 @@ FHyperTwistTrainingRepositoryIntegrityReport UHyperTwistContractLibrary::MakeSam } RepositoryState.StoredCoachActionPlans.Add(FHyperTwistTrainingCoachActionPlan()); + const FHyperTwistTrainingCoachSessionQueueState CoachSessionQueueState = + MakeSampleTrainingCoachSessionQueueState(); + if (CoachSessionQueueState.IsStructurallyValid()) + { + RepositoryState.StoredCoachSessionQueues.Add(CoachSessionQueueState); + RepositoryState.StoredCoachSessionQueues.Add(CoachSessionQueueState); + } + RepositoryState.StoredCoachSessionQueues.Add(FHyperTwistTrainingCoachSessionQueueState()); + return UHyperTwistTrainingRepositoryLibrary::DeriveRepositoryIntegrityReport(RepositoryState); } @@ -1292,6 +1311,25 @@ FHyperTwistCoachBrief UHyperTwistContractLibrary::MakeSampleTrainingCoachFollowU ); } +FHyperTwistCoachSessionQueueSummary UHyperTwistContractLibrary::MakeSampleTrainingCoachSessionQueueSummary() +{ + return UHyperTwistTrainingCoachLibrary::DeriveCoachSessionQueueSummary( + MakeSampleTrainingCoachBrief(), + MakeSampleTrainingCoachFollowUpBrief(), + MakeSampleTrainingCoachArchivePolicySummary(), + MakeSampleTrainingReviewProgramSummary() + ); +} + +FHyperTwistTrainingCoachSessionQueueState UHyperTwistContractLibrary::MakeSampleTrainingCoachSessionQueueState() +{ + return UHyperTwistTrainingRepositoryLibrary::BuildCoachSessionQueueState( + MakeSampleTrainingCoachSessionQueueSummary(), + TEXT("coach_queue_active"), + TEXT("2026-04-28T12:12:00Z") + ); +} + bool UHyperTwistContractLibrary::IsPuzzleStateStructurallyValid(const FHyperTwistPuzzleState& State) { return State.IsStructurallyValid(); diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingCoachLibrary.cpp b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingCoachLibrary.cpp index 3049001..6149898 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingCoachLibrary.cpp +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingCoachLibrary.cpp @@ -569,3 +569,148 @@ FHyperTwistCoachBrief UHyperTwistTrainingCoachLibrary::DeriveCoachFollowUpBrief( return Brief; } + +FHyperTwistCoachSessionQueueSummary UHyperTwistTrainingCoachLibrary::DeriveCoachSessionQueueSummary( + const FHyperTwistCoachBrief& CurrentBrief, + const FHyperTwistCoachBrief& FollowUpBrief, + const FHyperTwistTrainingCoachArchivePolicySummary& ArchivePolicySummary, + const FHyperTwistTrainingReviewProgramSummary& ReviewProgramSummary) +{ + FHyperTwistCoachSessionQueueSummary Summary; + Summary.UserId = !CurrentBrief.UserId.IsEmpty() + ? CurrentBrief.UserId + : FollowUpBrief.UserId; + if (Summary.UserId.IsEmpty()) + { + return Summary; + } + + Summary.ReferenceUtc = !FollowUpBrief.ReferenceUtc.IsEmpty() + ? FollowUpBrief.ReferenceUtc + : (!CurrentBrief.ReferenceUtc.IsEmpty() ? CurrentBrief.ReferenceUtc : ArchivePolicySummary.ReferenceUtc); + Summary.FocusDeckId = !FollowUpBrief.FocusDeckId.IsEmpty() + ? FollowUpBrief.FocusDeckId + : (!CurrentBrief.FocusDeckId.IsEmpty() ? CurrentBrief.FocusDeckId : ArchivePolicySummary.FocusDeckId); + Summary.FocusMethodSegmentId = !FollowUpBrief.FocusMethodSegmentId.IsEmpty() + ? FollowUpBrief.FocusMethodSegmentId + : (!CurrentBrief.FocusMethodSegmentId.IsEmpty() + ? CurrentBrief.FocusMethodSegmentId + : ArchivePolicySummary.FocusMethodSegmentId); + + auto AddBriefEntry = [&Summary]( + const FString& EntryId, + const FString& SourceLabel, + const FHyperTwistCoachBrief& Brief, + const bool bRequiresFollowUp, + const bool bRequiresArchiveRetentionTuning) + { + if (Brief.UserId.IsEmpty()) + { + return; + } + + FHyperTwistCoachSessionQueueEntry Entry; + Entry.EntryId = EntryId; + Entry.SourceLabel = SourceLabel; + Entry.Priority = Brief.Priority; + Entry.PriorityScore = Brief.PriorityScore; + Entry.Headline = Brief.Headline; + Entry.SuggestedMode = Brief.SuggestedMode; + Entry.SuggestedSelectionPolicy = Brief.SuggestedSelectionPolicy; + Entry.FocusDeckId = Brief.FocusDeckId; + Entry.FocusMethodSegmentId = Brief.FocusMethodSegmentId; + Entry.FocusCaseIds = Brief.FocusCaseIds; + Entry.bRequiresFollowUp = bRequiresFollowUp; + Entry.bRequiresArchiveRetentionTuning = bRequiresArchiveRetentionTuning; + Summary.Entries.Add(Entry); + }; + + AddBriefEntry( + TEXT("coach_queue_primary"), + TEXT("primary-brief"), + CurrentBrief, + false, + false + ); + + if (FollowUpBrief.UserId == Summary.UserId && FollowUpBrief.FocusCaseIds.Num() > 0) + { + AddBriefEntry( + TEXT("coach_queue_follow_up"), + TEXT("follow-up-brief"), + FollowUpBrief, + true, + false + ); + } + + if (ArchivePolicySummary.IsStructurallyValid() + && (ArchivePolicySummary.bNeedsArchiveRetentionTuning + || ArchivePolicySummary.RetainedCarryForwardCaseCount > 0)) + { + FHyperTwistCoachBrief ArchiveBrief; + ArchiveBrief.UserId = Summary.UserId; + ArchiveBrief.ReferenceUtc = Summary.ReferenceUtc; + ArchiveBrief.PriorityScore = FMath::Clamp( + 55.0f + ArchivePolicySummary.ArchiveRetentionTuningScore, + 20.0f, + 100.0f + ); + ArchiveBrief.Priority = HyperTwistTrainingCoachLibraryInternal::PriorityFromScore(ArchiveBrief.PriorityScore); + ArchiveBrief.Headline = ArchivePolicySummary.bNeedsArchiveRetentionTuning + ? TEXT("Archive retention queue needs attention") + : TEXT("Carry-forward archive queue is still active"); + ArchiveBrief.SuggestedMode = ArchivePolicySummary.DominantSuggestedMode; + ArchiveBrief.SuggestedSelectionPolicy = ArchivePolicySummary.DominantSelectionPolicy; + ArchiveBrief.FocusDeckId = ArchivePolicySummary.FocusDeckId; + ArchiveBrief.FocusMethodSegmentId = ArchivePolicySummary.FocusMethodSegmentId; + ArchiveBrief.FocusCaseIds = ArchivePolicySummary.ReopenRiskArchivedCaseIds.Num() > 0 + ? ArchivePolicySummary.ReopenRiskArchivedCaseIds + : (ArchivePolicySummary.ProvisionalArchivedCaseIds.Num() > 0 + ? ArchivePolicySummary.ProvisionalArchivedCaseIds + : ArchivePolicySummary.RetainedCarryForwardCaseIds); + AddBriefEntry( + TEXT("coach_queue_archive"), + TEXT("archive-policy"), + ArchiveBrief, + false, + ArchivePolicySummary.bNeedsArchiveRetentionTuning + ); + } + + Summary.Entries.Sort([](const FHyperTwistCoachSessionQueueEntry& Left, const FHyperTwistCoachSessionQueueEntry& Right) + { + if (!FMath::IsNearlyEqual(Left.PriorityScore, Right.PriorityScore)) + { + return Left.PriorityScore > Right.PriorityScore; + } + return Left.EntryId < Right.EntryId; + }); + + for (int32 Index = 0; Index < Summary.Entries.Num(); ++Index) + { + Summary.Entries[Index].SlotIndex = Index; + Summary.FollowUpEntryCount += Summary.Entries[Index].bRequiresFollowUp ? 1 : 0; + Summary.ArchiveRetentionEntryCount += Summary.Entries[Index].bRequiresArchiveRetentionTuning ? 1 : 0; + Summary.ReviewBacklogEntryCount += Summary.Entries[Index].SourceLabel == TEXT("primary-brief") + && CurrentBrief.FocusLane == EHyperTwistCoachFocusLane::ReviewBacklog + ? 1 + : 0; + } + + Summary.TotalEntryCount = Summary.Entries.Num(); + Summary.PrimaryQueueLabel = Summary.Entries.Num() > 0 + ? Summary.Entries[0].SourceLabel + : FString(); + Summary.NextQueueLabel = Summary.Entries.Num() > 1 + ? Summary.Entries[1].SourceLabel + : FString(); + Summary.bHasImmediateQueue = Summary.TotalEntryCount > 0; + Summary.bNeedsArchiveAwareSequencing = + ArchivePolicySummary.bNeedsArchiveRetentionTuning + || Summary.ArchiveRetentionEntryCount > 0 + || ArchivePolicySummary.RetainedCarryForwardCaseCount > 0 + || ReviewProgramSummary.bNeedsCoachIntervention; + + return Summary; +} diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingRepositoryLibrary.cpp b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingRepositoryLibrary.cpp index 2223d37..bf5a26b 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingRepositoryLibrary.cpp +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingRepositoryLibrary.cpp @@ -301,6 +301,11 @@ namespace HyperTwistTrainingRepositoryLibraryInternal return UserId + CaseKeySeparator + PlanId; } + FString MakeCoachSessionQueueKey(const FString& UserId, const FString& QueueId) + { + return UserId + CaseKeySeparator + QueueId; + } + TArray DeduplicateCaseIds(const TArray& CaseIds) { TArray UniqueCaseIds; @@ -475,6 +480,223 @@ namespace HyperTwistTrainingRepositoryLibraryInternal return NormalizedPlan; } + bool IsTerminalCoachQueueEntryState(const EHyperTwistTrainingCoachSessionQueueEntryState EntryState) + { + return EntryState == EHyperTwistTrainingCoachSessionQueueEntryState::Completed + || EntryState == EHyperTwistTrainingCoachSessionQueueEntryState::Skipped + || EntryState == EHyperTwistTrainingCoachSessionQueueEntryState::Expired; + } + + FString MakeCoachSessionQueueEntryFingerprint( + const FHyperTwistTrainingCoachSessionQueueStateEntry& Entry + ) + { + const FString CaseFingerprint = FString::Join(Entry.FocusCaseIds, TEXT("|")); + return FString::Printf( + TEXT("%s|%s|%s|%s|%d|%d|%s"), + *Entry.SourceLabel, + *Entry.FocusDeckId, + *Entry.FocusMethodSegmentId, + *Entry.Headline, + static_cast(Entry.SuggestedMode), + static_cast(Entry.SuggestedSelectionPolicy), + *CaseFingerprint + ); + } + + FHyperTwistTrainingCoachSessionQueueState NormalizeCoachSessionQueueState( + const FHyperTwistTrainingCoachSessionQueueState& QueueState + ) + { + FHyperTwistTrainingCoachSessionQueueState NormalizedState = QueueState; + if (NormalizedState.QueueId.IsEmpty()) + { + NormalizedState.QueueId = TEXT("coach_queue_active"); + } + + NormalizedState.ReferenceUtc = ResolveReferenceUtc(NormalizedState.ReferenceUtc); + NormalizedState.LastUpdatedAtUtc = ResolveReferenceUtc( + NormalizedState.LastUpdatedAtUtc.IsEmpty() + ? NormalizedState.ReferenceUtc + : NormalizedState.LastUpdatedAtUtc + ); + + TMap EntryIndexById; + TArray NormalizedEntries; + for (const FHyperTwistTrainingCoachSessionQueueStateEntry& Entry : QueueState.Entries) + { + FHyperTwistTrainingCoachSessionQueueStateEntry NormalizedEntry = Entry; + NormalizedEntry.FocusCaseIds = DeduplicateCaseIds(Entry.FocusCaseIds); + if (NormalizedEntry.WorkFingerprint.IsEmpty()) + { + NormalizedEntry.WorkFingerprint = MakeCoachSessionQueueEntryFingerprint(NormalizedEntry); + } + NormalizedEntry.bPinned = NormalizedEntry.bPinned + || NormalizedEntry.bRequiresFollowUp + || NormalizedEntry.bRequiresArchiveRetentionTuning; + if (NormalizedEntry.EntryState == EHyperTwistTrainingCoachSessionQueueEntryState::Ready + || NormalizedEntry.EntryState == EHyperTwistTrainingCoachSessionQueueEntryState::InProgress) + { + NormalizedEntry.ScheduledAtUtc = !NormalizedEntry.ScheduledAtUtc.IsEmpty() + ? NormalizedEntry.ScheduledAtUtc + : NormalizedState.ReferenceUtc; + } + if (IsTerminalCoachQueueEntryState(NormalizedEntry.EntryState)) + { + NormalizedEntry.bCanStartImmediately = false; + } + if (!NormalizedEntry.IsStructurallyValid()) + { + continue; + } + + if (const int32* ExistingIndex = EntryIndexById.Find(NormalizedEntry.EntryId)) + { + NormalizedEntries[*ExistingIndex] = NormalizedEntry; + } + else + { + EntryIndexById.Add(NormalizedEntry.EntryId, NormalizedEntries.Add(NormalizedEntry)); + } + } + + NormalizedEntries.Sort([]( + const FHyperTwistTrainingCoachSessionQueueStateEntry& Left, + const FHyperTwistTrainingCoachSessionQueueStateEntry& Right + ) + { + if (Left.SlotIndex != Right.SlotIndex) + { + return Left.SlotIndex < Right.SlotIndex; + } + if (!FMath::IsNearlyEqual(Left.PriorityScore, Right.PriorityScore)) + { + return Left.PriorityScore > Right.PriorityScore; + } + + return Left.EntryId < Right.EntryId; + }); + + NormalizedState.ActiveEntryId.Reset(); + NormalizedState.ActiveTrainingSessionId.Reset(); + NormalizedState.PendingEntryCount = 0; + NormalizedState.ReadyEntryCount = 0; + NormalizedState.InProgressEntryCount = 0; + NormalizedState.CompletedEntryCount = 0; + NormalizedState.SkippedEntryCount = 0; + NormalizedState.ExpiredEntryCount = 0; + + bool bHasInProgressEntry = false; + bool bHasReadyEntry = false; + for (int32 EntryIndex = 0; EntryIndex < NormalizedEntries.Num(); ++EntryIndex) + { + FHyperTwistTrainingCoachSessionQueueStateEntry& Entry = NormalizedEntries[EntryIndex]; + Entry.SlotIndex = EntryIndex; + + if (Entry.EntryState == EHyperTwistTrainingCoachSessionQueueEntryState::InProgress) + { + if (bHasInProgressEntry) + { + Entry.EntryState = EHyperTwistTrainingCoachSessionQueueEntryState::Pending; + Entry.SourceTrainingSessionId.Reset(); + Entry.StartedAtUtc.Reset(); + } + else + { + bHasInProgressEntry = true; + Entry.ScheduledAtUtc = !Entry.ScheduledAtUtc.IsEmpty() ? Entry.ScheduledAtUtc : NormalizedState.ReferenceUtc; + Entry.bCanStartImmediately = false; + NormalizedState.ActiveEntryId = Entry.EntryId; + NormalizedState.ActiveTrainingSessionId = Entry.SourceTrainingSessionId; + } + } + + if (Entry.EntryState == EHyperTwistTrainingCoachSessionQueueEntryState::Ready) + { + if (bHasInProgressEntry || bHasReadyEntry) + { + Entry.EntryState = EHyperTwistTrainingCoachSessionQueueEntryState::Pending; + Entry.bCanStartImmediately = false; + } + else + { + bHasReadyEntry = true; + Entry.ScheduledAtUtc = !Entry.ScheduledAtUtc.IsEmpty() ? Entry.ScheduledAtUtc : NormalizedState.ReferenceUtc; + Entry.bCanStartImmediately = true; + NormalizedState.ActiveEntryId = Entry.EntryId; + } + } + + switch (Entry.EntryState) + { + case EHyperTwistTrainingCoachSessionQueueEntryState::Pending: + NormalizedState.PendingEntryCount += 1; + break; + case EHyperTwistTrainingCoachSessionQueueEntryState::Ready: + NormalizedState.ReadyEntryCount += 1; + break; + case EHyperTwistTrainingCoachSessionQueueEntryState::InProgress: + NormalizedState.InProgressEntryCount += 1; + break; + case EHyperTwistTrainingCoachSessionQueueEntryState::Completed: + NormalizedState.CompletedEntryCount += 1; + break; + case EHyperTwistTrainingCoachSessionQueueEntryState::Skipped: + NormalizedState.SkippedEntryCount += 1; + break; + case EHyperTwistTrainingCoachSessionQueueEntryState::Expired: + NormalizedState.ExpiredEntryCount += 1; + break; + default: + break; + } + } + + if (!bHasInProgressEntry && !bHasReadyEntry) + { + for (FHyperTwistTrainingCoachSessionQueueStateEntry& Entry : NormalizedEntries) + { + if (!IsTerminalCoachQueueEntryState(Entry.EntryState)) + { + if (Entry.EntryState != EHyperTwistTrainingCoachSessionQueueEntryState::Pending) + { + switch (Entry.EntryState) + { + case EHyperTwistTrainingCoachSessionQueueEntryState::Completed: + NormalizedState.CompletedEntryCount -= 1; + break; + case EHyperTwistTrainingCoachSessionQueueEntryState::Skipped: + NormalizedState.SkippedEntryCount -= 1; + break; + case EHyperTwistTrainingCoachSessionQueueEntryState::Expired: + NormalizedState.ExpiredEntryCount -= 1; + break; + default: + break; + } + } + else + { + NormalizedState.PendingEntryCount -= 1; + } + + Entry.EntryState = EHyperTwistTrainingCoachSessionQueueEntryState::Ready; + Entry.ScheduledAtUtc = !Entry.ScheduledAtUtc.IsEmpty() ? Entry.ScheduledAtUtc : NormalizedState.ReferenceUtc; + Entry.bCanStartImmediately = true; + NormalizedState.ReadyEntryCount += 1; + NormalizedState.ActiveEntryId = Entry.EntryId; + break; + } + } + } + + NormalizedState.TotalEntryCount = NormalizedEntries.Num(); + NormalizedState.bHasImmediateQueue = NormalizedState.ReadyEntryCount > 0 + || NormalizedState.InProgressEntryCount > 0; + NormalizedState.Entries = NormalizedEntries; + return NormalizedState; + } + FHyperTwistTrainingCaseLearnerState DeriveLearnerStateFromReviewHistory( const FString& UserId, const FString& DeckId, @@ -2680,6 +2902,29 @@ FHyperTwistTrainingRepositoryIntegrityReport UHyperTwistTrainingRepositoryLibrar SeenCoachActionPlanKeys.Add(ActionPlanKey); } + Report.TotalCoachSessionQueueCount = RepositoryState.StoredCoachSessionQueues.Num(); + TSet SeenCoachSessionQueueKeys; + for (const FHyperTwistTrainingCoachSessionQueueState& QueueState : RepositoryState.StoredCoachSessionQueues) + { + if (!QueueState.IsStructurallyValid()) + { + Report.InvalidCoachSessionQueueCount += 1; + continue; + } + + const FString QueueKey = HyperTwistTrainingRepositoryLibraryInternal::MakeCoachSessionQueueKey( + QueueState.UserId, + QueueState.QueueId + ); + if (SeenCoachSessionQueueKeys.Contains(QueueKey)) + { + Report.DuplicateCoachSessionQueueCount += 1; + continue; + } + + SeenCoachSessionQueueKeys.Add(QueueKey); + } + if (Report.InvalidRunRecordCount > 0) { HyperTwistTrainingRepositoryLibraryInternal::AppendRepairAction( @@ -2781,6 +3026,20 @@ FHyperTwistTrainingRepositoryIntegrityReport UHyperTwistTrainingRepositoryLibrar TEXT("normalize-and-deduplicate-coach-action-plans") ); } + if (Report.InvalidCoachSessionQueueCount > 0) + { + HyperTwistTrainingRepositoryLibraryInternal::AppendRepairAction( + Report.RepairActions, + TEXT("drop-invalid-coach-session-queues") + ); + } + if (Report.DuplicateCoachSessionQueueCount > 0) + { + HyperTwistTrainingRepositoryLibraryInternal::AppendRepairAction( + Report.RepairActions, + TEXT("normalize-and-deduplicate-coach-session-queues") + ); + } return Report; } @@ -3089,6 +3348,60 @@ FHyperTwistTrainingRepositoryState UHyperTwistTrainingRepositoryLibrary::RepairR return Left.PlanId < Right.PlanId; }); + TMap CoachSessionQueueIndexByKey; + for (const FHyperTwistTrainingCoachSessionQueueState& QueueState : RepositoryState.StoredCoachSessionQueues) + { + if (!QueueState.IsStructurallyValid()) + { + continue; + } + + const FHyperTwistTrainingCoachSessionQueueState NormalizedQueueState = + HyperTwistTrainingRepositoryLibraryInternal::NormalizeCoachSessionQueueState(QueueState); + if (!NormalizedQueueState.IsStructurallyValid()) + { + continue; + } + + const FString QueueKey = HyperTwistTrainingRepositoryLibraryInternal::MakeCoachSessionQueueKey( + NormalizedQueueState.UserId, + NormalizedQueueState.QueueId + ); + if (const int32* ExistingIndex = CoachSessionQueueIndexByKey.Find(QueueKey)) + { + RepairedState.StoredCoachSessionQueues[*ExistingIndex] = NormalizedQueueState; + } + else + { + CoachSessionQueueIndexByKey.Add( + QueueKey, + RepairedState.StoredCoachSessionQueues.Add(NormalizedQueueState) + ); + } + } + + RepairedState.StoredCoachSessionQueues.Sort([]( + const FHyperTwistTrainingCoachSessionQueueState& Left, + const FHyperTwistTrainingCoachSessionQueueState& Right + ) + { + if (Left.UserId != Right.UserId) + { + return Left.UserId < Right.UserId; + } + + const int32 UtcCompare = HyperTwistTrainingRepositoryLibraryInternal::CompareUtcStrings( + Left.LastUpdatedAtUtc, + Right.LastUpdatedAtUtc + ); + if (UtcCompare != 0) + { + return UtcCompare > 0; + } + + return Left.QueueId < Right.QueueId; + }); + return RepairedState; } @@ -4997,6 +5310,311 @@ FHyperTwistTrainingRepositoryState UHyperTwistTrainingRepositoryLibrary::UpsertC return UpdatedRepositoryState; } +FHyperTwistTrainingCoachSessionQueueState UHyperTwistTrainingRepositoryLibrary::BuildCoachSessionQueueState( + const FHyperTwistCoachSessionQueueSummary& QueueSummary, + const FString& QueueId, + const FString& ReferenceUtc +) +{ + FHyperTwistTrainingCoachSessionQueueState QueueState; + if (!QueueSummary.IsStructurallyValid()) + { + return QueueState; + } + + QueueState.QueueId = QueueId.IsEmpty() ? TEXT("coach_queue_active") : QueueId; + QueueState.UserId = QueueSummary.UserId; + QueueState.ReferenceUtc = HyperTwistTrainingRepositoryLibraryInternal::ResolveReferenceUtc( + ReferenceUtc.IsEmpty() ? QueueSummary.ReferenceUtc : ReferenceUtc + ); + QueueState.LastUpdatedAtUtc = QueueState.ReferenceUtc; + QueueState.FocusDeckId = QueueSummary.FocusDeckId; + QueueState.FocusMethodSegmentId = QueueSummary.FocusMethodSegmentId; + QueueState.PrimaryQueueLabel = QueueSummary.PrimaryQueueLabel; + QueueState.NextQueueLabel = QueueSummary.NextQueueLabel; + QueueState.bHasImmediateQueue = QueueSummary.bHasImmediateQueue; + QueueState.bNeedsArchiveAwareSequencing = QueueSummary.bNeedsArchiveAwareSequencing; + + for (const FHyperTwistCoachSessionQueueEntry& SummaryEntry : QueueSummary.Entries) + { + FHyperTwistTrainingCoachSessionQueueStateEntry QueueEntry; + QueueEntry.EntryId = SummaryEntry.EntryId; + QueueEntry.SourceLabel = SummaryEntry.SourceLabel; + QueueEntry.SlotIndex = SummaryEntry.SlotIndex; + QueueEntry.PriorityLabel = StaticEnum()->GetNameStringByValue( + static_cast(SummaryEntry.Priority) + ); + QueueEntry.PriorityScore = SummaryEntry.PriorityScore; + QueueEntry.Headline = SummaryEntry.Headline; + QueueEntry.SuggestedMode = SummaryEntry.SuggestedMode; + QueueEntry.SuggestedSelectionPolicy = SummaryEntry.SuggestedSelectionPolicy; + QueueEntry.FocusDeckId = SummaryEntry.FocusDeckId; + QueueEntry.FocusMethodSegmentId = SummaryEntry.FocusMethodSegmentId; + QueueEntry.FocusCaseIds = SummaryEntry.FocusCaseIds; + QueueEntry.bRequiresFollowUp = SummaryEntry.bRequiresFollowUp; + QueueEntry.bRequiresArchiveRetentionTuning = SummaryEntry.bRequiresArchiveRetentionTuning; + QueueEntry.bPinned = QueueEntry.bRequiresFollowUp || QueueEntry.bRequiresArchiveRetentionTuning; + QueueEntry.EntryState = EHyperTwistTrainingCoachSessionQueueEntryState::Pending; + QueueEntry.WorkFingerprint = + HyperTwistTrainingRepositoryLibraryInternal::MakeCoachSessionQueueEntryFingerprint(QueueEntry); + QueueState.Entries.Add(QueueEntry); + } + + return HyperTwistTrainingRepositoryLibraryInternal::NormalizeCoachSessionQueueState(QueueState); +} + +FHyperTwistTrainingCoachSessionQueueState UHyperTwistTrainingRepositoryLibrary::ResolveCoachSessionQueueState( + const FHyperTwistTrainingCoachSessionQueueState& ExistingQueueState, + const FHyperTwistCoachSessionQueueSummary& QueueSummary, + const FString& ReferenceUtc +) +{ + FHyperTwistTrainingCoachSessionQueueState ResolvedQueueState = BuildCoachSessionQueueState( + QueueSummary, + ExistingQueueState.QueueId, + ReferenceUtc + ); + if (!ResolvedQueueState.IsStructurallyValid() || !ExistingQueueState.IsStructurallyValid()) + { + return ResolvedQueueState; + } + + for (FHyperTwistTrainingCoachSessionQueueStateEntry& QueueEntry : ResolvedQueueState.Entries) + { + const FHyperTwistTrainingCoachSessionQueueStateEntry* ExistingEntry = + ExistingQueueState.Entries.FindByPredicate( + [&QueueEntry](const FHyperTwistTrainingCoachSessionQueueStateEntry& Candidate) + { + return Candidate.EntryId == QueueEntry.EntryId + && Candidate.WorkFingerprint == QueueEntry.WorkFingerprint + && Candidate.IsStructurallyValid(); + } + ); + if (ExistingEntry == nullptr) + { + continue; + } + + QueueEntry.EntryState = ExistingEntry->EntryState; + QueueEntry.SourceTrainingSessionId = ExistingEntry->SourceTrainingSessionId; + QueueEntry.ScheduledAtUtc = ExistingEntry->ScheduledAtUtc; + QueueEntry.StartedAtUtc = ExistingEntry->StartedAtUtc; + QueueEntry.CompletedAtUtc = ExistingEntry->CompletedAtUtc; + QueueEntry.bPinned = QueueEntry.bPinned || ExistingEntry->bPinned; + QueueEntry.bCanStartImmediately = ExistingEntry->bCanStartImmediately; + } + + ResolvedQueueState.LastUpdatedAtUtc = HyperTwistTrainingRepositoryLibraryInternal::ResolveReferenceUtc( + ReferenceUtc.IsEmpty() ? ResolvedQueueState.ReferenceUtc : ReferenceUtc + ); + return HyperTwistTrainingRepositoryLibraryInternal::NormalizeCoachSessionQueueState(ResolvedQueueState); +} + +FHyperTwistTrainingCoachSessionQueueState UHyperTwistTrainingRepositoryLibrary::StartCoachSessionQueueEntryBySourceLabel( + const FHyperTwistTrainingCoachSessionQueueState& QueueState, + const FString& SourceLabel, + const FString& TrainingSessionId, + const FString& StartedAtUtc +) +{ + FHyperTwistTrainingCoachSessionQueueState UpdatedQueueState = + HyperTwistTrainingRepositoryLibraryInternal::NormalizeCoachSessionQueueState(QueueState); + if (!UpdatedQueueState.IsStructurallyValid() || SourceLabel.IsEmpty()) + { + return UpdatedQueueState; + } + + const FString ResolvedStartedAtUtc = + HyperTwistTrainingRepositoryLibraryInternal::ResolveReferenceUtc(StartedAtUtc); + int32 ActivatedIndex = INDEX_NONE; + for (int32 EntryIndex = 0; EntryIndex < UpdatedQueueState.Entries.Num(); ++EntryIndex) + { + const FHyperTwistTrainingCoachSessionQueueStateEntry& Entry = UpdatedQueueState.Entries[EntryIndex]; + if (Entry.SourceLabel == SourceLabel + && !HyperTwistTrainingRepositoryLibraryInternal::IsTerminalCoachQueueEntryState(Entry.EntryState)) + { + ActivatedIndex = EntryIndex; + break; + } + } + if (ActivatedIndex == INDEX_NONE) + { + return UpdatedQueueState; + } + + for (FHyperTwistTrainingCoachSessionQueueStateEntry& Entry : UpdatedQueueState.Entries) + { + if (Entry.EntryState == EHyperTwistTrainingCoachSessionQueueEntryState::InProgress) + { + Entry.EntryState = EHyperTwistTrainingCoachSessionQueueEntryState::Pending; + Entry.SourceTrainingSessionId.Reset(); + Entry.StartedAtUtc.Reset(); + Entry.bCanStartImmediately = false; + } + } + + FHyperTwistTrainingCoachSessionQueueStateEntry& ActiveEntry = + UpdatedQueueState.Entries[ActivatedIndex]; + ActiveEntry.EntryState = EHyperTwistTrainingCoachSessionQueueEntryState::InProgress; + ActiveEntry.SourceTrainingSessionId = TrainingSessionId; + ActiveEntry.ScheduledAtUtc = !ActiveEntry.ScheduledAtUtc.IsEmpty() + ? ActiveEntry.ScheduledAtUtc + : ResolvedStartedAtUtc; + ActiveEntry.StartedAtUtc = ResolvedStartedAtUtc; + ActiveEntry.bCanStartImmediately = false; + UpdatedQueueState.LastUpdatedAtUtc = ResolvedStartedAtUtc; + return HyperTwistTrainingRepositoryLibraryInternal::NormalizeCoachSessionQueueState(UpdatedQueueState); +} + +FHyperTwistTrainingCoachSessionQueueState UHyperTwistTrainingRepositoryLibrary::FinalizeCoachSessionQueueEntry( + const FHyperTwistTrainingCoachSessionQueueState& QueueState, + const FString& TrainingSessionId, + const FString& CompletedAtUtc, + bool bAbortEntry +) +{ + FHyperTwistTrainingCoachSessionQueueState UpdatedQueueState = + HyperTwistTrainingRepositoryLibraryInternal::NormalizeCoachSessionQueueState(QueueState); + if (!UpdatedQueueState.IsStructurallyValid()) + { + return UpdatedQueueState; + } + + const FString ResolvedCompletedAtUtc = + HyperTwistTrainingRepositoryLibraryInternal::ResolveReferenceUtc( + CompletedAtUtc.IsEmpty() ? UpdatedQueueState.ReferenceUtc : CompletedAtUtc + ); + FHyperTwistTrainingCoachSessionQueueStateEntry* ActiveEntry = + UpdatedQueueState.Entries.FindByPredicate( + [&TrainingSessionId, &UpdatedQueueState](const FHyperTwistTrainingCoachSessionQueueStateEntry& Candidate) + { + if (!TrainingSessionId.IsEmpty() && Candidate.SourceTrainingSessionId == TrainingSessionId) + { + return true; + } + + return Candidate.EntryId == UpdatedQueueState.ActiveEntryId + && Candidate.EntryState == EHyperTwistTrainingCoachSessionQueueEntryState::InProgress; + } + ); + if (ActiveEntry == nullptr) + { + return UpdatedQueueState; + } + + ActiveEntry->EntryState = bAbortEntry + ? EHyperTwistTrainingCoachSessionQueueEntryState::Skipped + : EHyperTwistTrainingCoachSessionQueueEntryState::Completed; + ActiveEntry->CompletedAtUtc = ResolvedCompletedAtUtc; + ActiveEntry->bCanStartImmediately = false; + UpdatedQueueState.ActiveEntryId.Reset(); + UpdatedQueueState.ActiveTrainingSessionId.Reset(); + UpdatedQueueState.LastUpdatedAtUtc = ResolvedCompletedAtUtc; + return HyperTwistTrainingRepositoryLibraryInternal::NormalizeCoachSessionQueueState(UpdatedQueueState); +} + +TArray UHyperTwistTrainingRepositoryLibrary::ListCoachSessionQueueStatesForUser( + const FHyperTwistTrainingRepositoryState& RepositoryState, + const FString& UserId +) +{ + TArray QueueStates; + if (UserId.IsEmpty()) + { + return QueueStates; + } + + for (const FHyperTwistTrainingCoachSessionQueueState& QueueState : RepositoryState.StoredCoachSessionQueues) + { + if (QueueState.UserId == UserId && QueueState.IsStructurallyValid()) + { + QueueStates.Add(QueueState); + } + } + + QueueStates.Sort([]( + const FHyperTwistTrainingCoachSessionQueueState& Left, + const FHyperTwistTrainingCoachSessionQueueState& Right + ) + { + const int32 UtcCompare = HyperTwistTrainingRepositoryLibraryInternal::CompareUtcStrings( + Left.LastUpdatedAtUtc, + Right.LastUpdatedAtUtc + ); + if (UtcCompare != 0) + { + return UtcCompare > 0; + } + + return Left.QueueId < Right.QueueId; + }); + return QueueStates; +} + +bool UHyperTwistTrainingRepositoryLibrary::TryGetCoachSessionQueueState( + const FHyperTwistTrainingRepositoryState& RepositoryState, + const FString& UserId, + const FString& QueueId, + FHyperTwistTrainingCoachSessionQueueState& OutQueueState +) +{ + OutQueueState = FHyperTwistTrainingCoachSessionQueueState(); + if (UserId.IsEmpty() || QueueId.IsEmpty()) + { + return false; + } + + const FHyperTwistTrainingCoachSessionQueueState* MatchingQueueState = + RepositoryState.StoredCoachSessionQueues.FindByPredicate( + [&UserId, &QueueId](const FHyperTwistTrainingCoachSessionQueueState& Candidate) + { + return Candidate.UserId == UserId + && Candidate.QueueId == QueueId + && Candidate.IsStructurallyValid(); + } + ); + if (MatchingQueueState == nullptr) + { + return false; + } + + OutQueueState = *MatchingQueueState; + return true; +} + +FHyperTwistTrainingRepositoryState UHyperTwistTrainingRepositoryLibrary::UpsertCoachSessionQueueState( + const FHyperTwistTrainingRepositoryState& RepositoryState, + const FHyperTwistTrainingCoachSessionQueueState& QueueState +) +{ + FHyperTwistTrainingRepositoryState UpdatedRepositoryState = RepositoryState; + const FHyperTwistTrainingCoachSessionQueueState NormalizedQueueState = + HyperTwistTrainingRepositoryLibraryInternal::NormalizeCoachSessionQueueState(QueueState); + if (!NormalizedQueueState.IsStructurallyValid()) + { + return UpdatedRepositoryState; + } + + FHyperTwistTrainingCoachSessionQueueState* ExistingQueueState = + UpdatedRepositoryState.StoredCoachSessionQueues.FindByPredicate( + [&NormalizedQueueState](const FHyperTwistTrainingCoachSessionQueueState& Candidate) + { + return Candidate.UserId == NormalizedQueueState.UserId + && Candidate.QueueId == NormalizedQueueState.QueueId; + } + ); + if (ExistingQueueState != nullptr) + { + *ExistingQueueState = NormalizedQueueState; + } + else + { + UpdatedRepositoryState.StoredCoachSessionQueues.Add(NormalizedQueueState); + } + + return UpdatedRepositoryState; +} + FHyperTwistTrainingCoachActionPlanSummary UHyperTwistTrainingRepositoryLibrary::DeriveCoachActionPlanSummary( const FHyperTwistTrainingRepositoryState& RepositoryState, const FString& UserId, diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingRuntimeLibrary.cpp b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingRuntimeLibrary.cpp index 9ac0b93..923d749 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingRuntimeLibrary.cpp +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingRuntimeLibrary.cpp @@ -358,6 +358,29 @@ FHyperTwistCoachBrief UHyperTwistTrainingRuntimeLibrary::GetActiveTrainingCoachF return FHyperTwistCoachBrief(); } +FHyperTwistCoachSessionQueueSummary UHyperTwistTrainingRuntimeLibrary::GetActiveTrainingCoachSessionQueueSummary( + UObject* WorldContextObject) +{ + if (UHyperTwistTrainingSubsystem* TrainingSubsystem = GetTrainingSubsystem(WorldContextObject)) + { + return TrainingSubsystem->GetActiveCoachSessionQueueSummary(); + } + + return FHyperTwistCoachSessionQueueSummary(); +} + +FHyperTwistTrainingCoachSessionQueueState UHyperTwistTrainingRuntimeLibrary::GetActiveTrainingCoachSessionQueueState( + UObject* WorldContextObject +) +{ + if (UHyperTwistTrainingSubsystem* TrainingSubsystem = GetTrainingSubsystem(WorldContextObject)) + { + return TrainingSubsystem->GetActiveCoachSessionQueueState(); + } + + return FHyperTwistTrainingCoachSessionQueueState(); +} + TArray UHyperTwistTrainingRuntimeLibrary::GetActiveTrainingLearnerMethodSegments(UObject* WorldContextObject) { if (UHyperTwistTrainingSubsystem* TrainingSubsystem = GetTrainingSubsystem(WorldContextObject)) @@ -547,6 +570,22 @@ bool UHyperTwistTrainingRuntimeLibrary::TryGetStoredTrainingCoachActionPlan( return false; } +bool UHyperTwistTrainingRuntimeLibrary::TryGetStoredTrainingCoachSessionQueueState( + UObject* WorldContextObject, + const FString& UserId, + const FString& QueueId, + FHyperTwistTrainingCoachSessionQueueState& OutQueueState +) +{ + if (UHyperTwistTrainingSubsystem* TrainingSubsystem = GetTrainingSubsystem(WorldContextObject)) + { + return TrainingSubsystem->TryGetStoredCoachSessionQueueState(UserId, QueueId, OutQueueState); + } + + OutQueueState = FHyperTwistTrainingCoachSessionQueueState(); + return false; +} + bool UHyperTwistTrainingRuntimeLibrary::TryGetStoredTrainingSessionTemplate( UObject* WorldContextObject, const FString& UserId, @@ -633,6 +672,19 @@ bool UHyperTwistTrainingRuntimeLibrary::SaveMethodDrillBatch( return false; } +bool UHyperTwistTrainingRuntimeLibrary::SaveCoachSessionQueueState( + UObject* WorldContextObject, + const FHyperTwistTrainingCoachSessionQueueState& QueueState +) +{ + if (UHyperTwistTrainingSubsystem* TrainingSubsystem = GetTrainingSubsystem(WorldContextObject)) + { + return TrainingSubsystem->SaveCoachSessionQueueState(QueueState); + } + + return false; +} + bool UHyperTwistTrainingRuntimeLibrary::RemoveMethodDrillBatch( UObject* WorldContextObject, const FString& UserId, diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingSubsystem.cpp b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingSubsystem.cpp index 30b962b..71012d3 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingSubsystem.cpp +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingSubsystem.cpp @@ -8,6 +8,11 @@ #include "HyperTwistTraining/HyperTwistTrainingPersistenceLibrary.h" #include "HyperTwistTraining/HyperTwistTrainingRepositoryLibrary.h" +namespace HyperTwistTrainingSubsystemInternal +{ + const TCHAR* ActiveCoachSessionQueueId = TEXT("coach_queue_active"); +} + bool UHyperTwistTrainingSubsystem::HasActiveRun() const { return bHasActiveRun && ActiveRunState.IsStructurallyValid(); @@ -158,6 +163,16 @@ FHyperTwistCoachBrief UHyperTwistTrainingSubsystem::GetActiveCoachFollowUpBrief( return ActiveCoachFollowUpBrief; } +FHyperTwistCoachSessionQueueSummary UHyperTwistTrainingSubsystem::GetActiveCoachSessionQueueSummary() const +{ + return ActiveCoachSessionQueueSummary; +} + +FHyperTwistTrainingCoachSessionQueueState UHyperTwistTrainingSubsystem::GetActiveCoachSessionQueueState() const +{ + return ActiveCoachSessionQueueState; +} + TArray UHyperTwistTrainingSubsystem::GetActiveLearnerMethodSegments() const { return ActiveLearnerMethodSegments; @@ -293,6 +308,20 @@ bool UHyperTwistTrainingSubsystem::TryGetStoredCoachActionPlan( ); } +bool UHyperTwistTrainingSubsystem::TryGetStoredCoachSessionQueueState( + const FString& UserId, + const FString& QueueId, + FHyperTwistTrainingCoachSessionQueueState& OutQueueState +) const +{ + return UHyperTwistTrainingRepositoryLibrary::TryGetCoachSessionQueueState( + TrainingRepositoryState, + UserId, + QueueId, + OutQueueState + ); +} + bool UHyperTwistTrainingSubsystem::TryGetStoredTrainingSessionTemplate( const FString& UserId, const FString& TemplateId, @@ -394,6 +423,23 @@ bool UHyperTwistTrainingSubsystem::SaveMethodDrillBatch( return true; } +bool UHyperTwistTrainingSubsystem::SaveCoachSessionQueueState( + const FHyperTwistTrainingCoachSessionQueueState& QueueState +) +{ + if (!QueueState.IsStructurallyValid()) + { + return false; + } + + TrainingRepositoryState = UHyperTwistTrainingRepositoryLibrary::UpsertCoachSessionQueueState( + TrainingRepositoryState, + QueueState + ); + RefreshRepositoryViews(); + return true; +} + bool UHyperTwistTrainingSubsystem::RemoveMethodDrillBatch( const FString& UserId, const FString& BatchId @@ -819,6 +865,7 @@ FHyperTwistTrainingRunState UHyperTwistTrainingSubsystem::StartCoachRecommendedR } const FString UserId = ActiveRunState.Session.UserId.IsEmpty() ? TEXT("local-user") : ActiveRunState.Session.UserId; + const FHyperTwistTrainingCoachSessionQueueState QueueStateBeforeStart = ActiveCoachSessionQueueState; const FHyperTwistTrainingCoachActionPlan DraftActionPlan = UHyperTwistTrainingRepositoryLibrary::BuildCoachActionPlan( ActiveCoachBrief, @@ -845,6 +892,24 @@ FHyperTwistTrainingRunState UHyperTwistTrainingSubsystem::StartCoachRecommendedR TrainingRepositoryState, ActiveCoachActionPlan ); + if (QueueStateBeforeStart.IsStructurallyValid()) + { + const FHyperTwistTrainingCoachSessionQueueState UpdatedQueueState = + UHyperTwistTrainingRepositoryLibrary::StartCoachSessionQueueEntryBySourceLabel( + QueueStateBeforeStart, + TEXT("primary-brief"), + RunState.Session.TrainingSessionId, + RunState.Session.StartedAtUtc + ); + if (UpdatedQueueState.IsStructurallyValid()) + { + TrainingRepositoryState = UHyperTwistTrainingRepositoryLibrary::UpsertCoachSessionQueueState( + TrainingRepositoryState, + UpdatedQueueState + ); + ActiveCoachSessionQueueState = UpdatedQueueState; + } + } RefreshRepositoryViews(); return RunState; } @@ -861,6 +926,7 @@ FHyperTwistTrainingRunState UHyperTwistTrainingSubsystem::StartCoachFollowUpRun( } const FHyperTwistCoachBrief FollowUpBrief = ActiveCoachFollowUpBrief; + const FHyperTwistTrainingCoachSessionQueueState QueueStateBeforeStart = ActiveCoachSessionQueueState; const FHyperTwistTrainingCoachActionPlan DraftActionPlan = UHyperTwistTrainingRepositoryLibrary::BuildCoachActionPlan( FollowUpBrief, @@ -887,6 +953,24 @@ FHyperTwistTrainingRunState UHyperTwistTrainingSubsystem::StartCoachFollowUpRun( TrainingRepositoryState, ActiveCoachActionPlan ); + if (QueueStateBeforeStart.IsStructurallyValid()) + { + const FHyperTwistTrainingCoachSessionQueueState UpdatedQueueState = + UHyperTwistTrainingRepositoryLibrary::StartCoachSessionQueueEntryBySourceLabel( + QueueStateBeforeStart, + TEXT("follow-up-brief"), + RunState.Session.TrainingSessionId, + RunState.Session.StartedAtUtc + ); + if (UpdatedQueueState.IsStructurallyValid()) + { + TrainingRepositoryState = UHyperTwistTrainingRepositoryLibrary::UpsertCoachSessionQueueState( + TrainingRepositoryState, + UpdatedQueueState + ); + ActiveCoachSessionQueueState = UpdatedQueueState; + } + } RefreshRepositoryViews(); return RunState; } @@ -1134,6 +1218,22 @@ FHyperTwistTrainingSession UHyperTwistTrainingSubsystem::CompleteActiveRun(bool ActiveCoachActionPlan ); } + if (ActiveCoachSessionQueueState.IsStructurallyValid()) + { + ActiveCoachSessionQueueState = UHyperTwistTrainingRepositoryLibrary::FinalizeCoachSessionQueueEntry( + ActiveCoachSessionQueueState, + ActiveRunState.Session.TrainingSessionId, + ActiveRunState.Session.EndedAtUtc, + bAbortSession + ); + if (ActiveCoachSessionQueueState.IsStructurallyValid()) + { + TrainingRepositoryState = UHyperTwistTrainingRepositoryLibrary::UpsertCoachSessionQueueState( + TrainingRepositoryState, + ActiveCoachSessionQueueState + ); + } + } RefreshRepositoryViews(); return ActiveRunState.Session; } @@ -1213,6 +1313,8 @@ void UHyperTwistTrainingSubsystem::RefreshRepositoryViews() ActiveCoachCarryForwardPolicySummary = FHyperTwistTrainingCoachCarryForwardPolicySummary(); ActiveCoachArchivePolicySummary = FHyperTwistTrainingCoachArchivePolicySummary(); ActiveCoachFollowUpBrief = FHyperTwistCoachBrief(); + ActiveCoachSessionQueueSummary = FHyperTwistCoachSessionQueueSummary(); + ActiveCoachSessionQueueState = FHyperTwistTrainingCoachSessionQueueState(); ActiveLearnerMethodSegments.Reset(); ActiveLearnerPresets.Reset(); ActiveTrainingSessionTemplates.Reset(); @@ -1382,6 +1484,44 @@ void UHyperTwistTrainingSubsystem::RefreshRepositoryViews() ActiveCoachActionPlanOutcomeSummary ) : FHyperTwistCoachBrief(); + ActiveCoachSessionQueueSummary = UHyperTwistTrainingCoachLibrary::DeriveCoachSessionQueueSummary( + ActiveCoachBrief, + ActiveCoachFollowUpBrief, + ActiveCoachArchivePolicySummary, + ActiveReviewProgramSummary + ); + if (ActiveCoachSessionQueueSummary.IsStructurallyValid()) + { + FHyperTwistTrainingCoachSessionQueueState StoredQueueState; + const bool bHasStoredQueueState = UHyperTwistTrainingRepositoryLibrary::TryGetCoachSessionQueueState( + TrainingRepositoryState, + ActiveCoachSessionQueueSummary.UserId, + HyperTwistTrainingSubsystemInternal::ActiveCoachSessionQueueId, + StoredQueueState + ); + ActiveCoachSessionQueueState = bHasStoredQueueState + ? UHyperTwistTrainingRepositoryLibrary::ResolveCoachSessionQueueState( + StoredQueueState, + ActiveCoachSessionQueueSummary, + ActiveLearnerDeckStateSummary.ReferenceUtc + ) + : UHyperTwistTrainingRepositoryLibrary::BuildCoachSessionQueueState( + ActiveCoachSessionQueueSummary, + HyperTwistTrainingSubsystemInternal::ActiveCoachSessionQueueId, + ActiveLearnerDeckStateSummary.ReferenceUtc + ); + if (ActiveCoachSessionQueueState.IsStructurallyValid()) + { + TrainingRepositoryState = UHyperTwistTrainingRepositoryLibrary::UpsertCoachSessionQueueState( + TrainingRepositoryState, + ActiveCoachSessionQueueState + ); + } + } + else + { + ActiveCoachSessionQueueState = FHyperTwistTrainingCoachSessionQueueState(); + } } TArray UHyperTwistTrainingSubsystem::BuildTrainingSessionTemplatesForUser( diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistBootstrap/HyperTwistContractLibrary.h b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistBootstrap/HyperTwistContractLibrary.h index 96a5bc0..ebe7d76 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistBootstrap/HyperTwistContractLibrary.h +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistBootstrap/HyperTwistContractLibrary.h @@ -206,6 +206,12 @@ public: UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Coach") static FHyperTwistCoachBrief MakeSampleTrainingCoachFollowUpBrief(); + UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Coach") + static FHyperTwistCoachSessionQueueSummary MakeSampleTrainingCoachSessionQueueSummary(); + + UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Coach") + static FHyperTwistTrainingCoachSessionQueueState MakeSampleTrainingCoachSessionQueueState(); + UFUNCTION(BlueprintPure, Category = "HyperTwist|Validation") static bool IsPuzzleStateStructurallyValid(const FHyperTwistPuzzleState& State); diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingCoachLibrary.h b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingCoachLibrary.h index 5268212..184ecbb 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingCoachLibrary.h +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingCoachLibrary.h @@ -142,6 +142,101 @@ struct FHyperTwistCoachBrief bool bPreferRecognitionReplayReview = false; }; +USTRUCT(BlueprintType) +struct FHyperTwistCoachSessionQueueEntry +{ + GENERATED_BODY() + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString EntryId; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString SourceLabel; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 SlotIndex = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + EHyperTwistCoachPriority Priority = EHyperTwistCoachPriority::Informational; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + float PriorityScore = 0.0f; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString Headline; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + EHyperTwistTrainingDeliveryMode SuggestedMode = EHyperTwistTrainingDeliveryMode::Timer; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + EHyperTwistTrainingSelectionPolicy SuggestedSelectionPolicy = EHyperTwistTrainingSelectionPolicy::Weighted; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString FocusDeckId; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString FocusMethodSegmentId; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + TArray FocusCaseIds; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + bool bRequiresFollowUp = false; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + bool bRequiresArchiveRetentionTuning = false; +}; + +USTRUCT(BlueprintType) +struct FHyperTwistCoachSessionQueueSummary +{ + GENERATED_BODY() + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString UserId; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString ReferenceUtc; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString FocusDeckId; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString FocusMethodSegmentId; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString PrimaryQueueLabel; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString NextQueueLabel; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + TArray Entries; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 TotalEntryCount = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 FollowUpEntryCount = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 ArchiveRetentionEntryCount = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 ReviewBacklogEntryCount = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + bool bHasImmediateQueue = false; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + bool bNeedsArchiveAwareSequencing = false; + + bool IsStructurallyValid() const + { + return !UserId.IsEmpty() && Entries.Num() > 0; + } +}; + UCLASS() class UNREALHYPERTWIST_API UHyperTwistTrainingCoachLibrary : public UBlueprintFunctionLibrary { @@ -178,4 +273,12 @@ public: const FHyperTwistTrainingCoachActionPlan& ActionPlan, const FHyperTwistTrainingCoachActionPlanOutcomeSummary& OutcomeSummary ); + + UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Coach") + static FHyperTwistCoachSessionQueueSummary DeriveCoachSessionQueueSummary( + const FHyperTwistCoachBrief& CurrentBrief, + const FHyperTwistCoachBrief& FollowUpBrief, + const FHyperTwistTrainingCoachArchivePolicySummary& ArchivePolicySummary, + const FHyperTwistTrainingReviewProgramSummary& ReviewProgramSummary + ); }; diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingRepositoryLibrary.h b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingRepositoryLibrary.h index 049b3e4..11a3cc4 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingRepositoryLibrary.h +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingRepositoryLibrary.h @@ -347,6 +347,56 @@ public: const FHyperTwistTrainingCoachActionPlan& ActionPlan ); + UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Repository|Coach") + static FHyperTwistTrainingCoachSessionQueueState BuildCoachSessionQueueState( + const FHyperTwistCoachSessionQueueSummary& QueueSummary, + const FString& QueueId, + const FString& ReferenceUtc + ); + + UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Repository|Coach") + static FHyperTwistTrainingCoachSessionQueueState ResolveCoachSessionQueueState( + const FHyperTwistTrainingCoachSessionQueueState& ExistingQueueState, + const FHyperTwistCoachSessionQueueSummary& QueueSummary, + const FString& ReferenceUtc + ); + + UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Repository|Coach") + static FHyperTwistTrainingCoachSessionQueueState StartCoachSessionQueueEntryBySourceLabel( + const FHyperTwistTrainingCoachSessionQueueState& QueueState, + const FString& SourceLabel, + const FString& TrainingSessionId, + const FString& StartedAtUtc + ); + + UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Repository|Coach") + static FHyperTwistTrainingCoachSessionQueueState FinalizeCoachSessionQueueEntry( + const FHyperTwistTrainingCoachSessionQueueState& QueueState, + const FString& TrainingSessionId, + const FString& CompletedAtUtc, + bool bAbortEntry + ); + + UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Repository|Coach") + static TArray ListCoachSessionQueueStatesForUser( + const FHyperTwistTrainingRepositoryState& RepositoryState, + const FString& UserId + ); + + UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Repository|Coach") + static bool TryGetCoachSessionQueueState( + const FHyperTwistTrainingRepositoryState& RepositoryState, + const FString& UserId, + const FString& QueueId, + FHyperTwistTrainingCoachSessionQueueState& OutQueueState + ); + + UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Repository|Coach") + static FHyperTwistTrainingRepositoryState UpsertCoachSessionQueueState( + const FHyperTwistTrainingRepositoryState& RepositoryState, + const FHyperTwistTrainingCoachSessionQueueState& QueueState + ); + UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Repository|Coach") static FHyperTwistTrainingCoachActionPlanSummary DeriveCoachActionPlanSummary( const FHyperTwistTrainingRepositoryState& RepositoryState, diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingRuntimeLibrary.h b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingRuntimeLibrary.h index 7832060..914c62d 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingRuntimeLibrary.h +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingRuntimeLibrary.h @@ -117,6 +117,14 @@ public: UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Coach", meta = (WorldContext = "WorldContextObject")) static FHyperTwistCoachBrief GetActiveTrainingCoachFollowUpBrief(UObject* WorldContextObject); + UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Coach", meta = (WorldContext = "WorldContextObject")) + static FHyperTwistCoachSessionQueueSummary GetActiveTrainingCoachSessionQueueSummary(UObject* WorldContextObject); + + UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Coach", meta = (WorldContext = "WorldContextObject")) + static FHyperTwistTrainingCoachSessionQueueState GetActiveTrainingCoachSessionQueueState( + UObject* WorldContextObject + ); + UFUNCTION(BlueprintPure, Category = "HyperTwist|Training", meta = (WorldContext = "WorldContextObject")) static TArray GetActiveTrainingLearnerMethodSegments(UObject* WorldContextObject); @@ -197,6 +205,14 @@ public: FHyperTwistTrainingCoachActionPlan& OutActionPlan ); + UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Coach", meta = (WorldContext = "WorldContextObject")) + static bool TryGetStoredTrainingCoachSessionQueueState( + UObject* WorldContextObject, + const FString& UserId, + const FString& QueueId, + FHyperTwistTrainingCoachSessionQueueState& OutQueueState + ); + UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training", meta = (WorldContext = "WorldContextObject")) static bool TryGetStoredTrainingSessionTemplate( UObject* WorldContextObject, @@ -239,6 +255,12 @@ public: const FHyperTwistTrainingMethodDrillBatch& DrillBatch ); + UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Coach", meta = (WorldContext = "WorldContextObject")) + static bool SaveCoachSessionQueueState( + UObject* WorldContextObject, + const FHyperTwistTrainingCoachSessionQueueState& QueueState + ); + UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Drill", meta = (WorldContext = "WorldContextObject")) static bool RemoveMethodDrillBatch( UObject* WorldContextObject, diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingSubsystem.h b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingSubsystem.h index 036353f..91255ec 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingSubsystem.h +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingSubsystem.h @@ -104,6 +104,12 @@ public: UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Coach") FHyperTwistCoachBrief GetActiveCoachFollowUpBrief() const; + UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Coach") + FHyperTwistCoachSessionQueueSummary GetActiveCoachSessionQueueSummary() const; + + UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Coach") + FHyperTwistTrainingCoachSessionQueueState GetActiveCoachSessionQueueState() const; + UFUNCTION(BlueprintPure, Category = "HyperTwist|Training") TArray GetActiveLearnerMethodSegments() const; @@ -176,6 +182,13 @@ public: FHyperTwistTrainingCoachActionPlan& OutActionPlan ) const; + UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Coach") + bool TryGetStoredCoachSessionQueueState( + const FString& UserId, + const FString& QueueId, + FHyperTwistTrainingCoachSessionQueueState& OutQueueState + ) const; + UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training") bool TryGetStoredTrainingSessionTemplate( const FString& UserId, @@ -206,6 +219,9 @@ public: UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Drill") bool SaveMethodDrillBatch(const FHyperTwistTrainingMethodDrillBatch& DrillBatch); + UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Coach") + bool SaveCoachSessionQueueState(const FHyperTwistTrainingCoachSessionQueueState& QueueState); + UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Drill") bool RemoveMethodDrillBatch( const FString& UserId, @@ -453,6 +469,12 @@ private: UPROPERTY() FHyperTwistCoachBrief ActiveCoachFollowUpBrief; + UPROPERTY() + FHyperTwistCoachSessionQueueSummary ActiveCoachSessionQueueSummary; + + UPROPERTY() + FHyperTwistTrainingCoachSessionQueueState ActiveCoachSessionQueueState; + UPROPERTY() TArray ActiveLearnerMethodSegments; diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingTypes.h b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingTypes.h index c85eb7f..64cb8f8 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingTypes.h +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingTypes.h @@ -2718,6 +2718,17 @@ enum class EHyperTwistTrainingCoachActionPlanState : uint8 Aborted UMETA(DisplayName = "Aborted") }; +UENUM(BlueprintType) +enum class EHyperTwistTrainingCoachSessionQueueEntryState : uint8 +{ + Pending UMETA(DisplayName = "Pending"), + Ready UMETA(DisplayName = "Ready"), + InProgress UMETA(DisplayName = "In Progress"), + Completed UMETA(DisplayName = "Completed"), + Skipped UMETA(DisplayName = "Skipped"), + Expired UMETA(DisplayName = "Expired") +}; + USTRUCT(BlueprintType) struct FHyperTwistTrainingCoachActionPlanEntry { @@ -2859,6 +2870,152 @@ struct FHyperTwistTrainingCoachActionPlan } }; +USTRUCT(BlueprintType) +struct FHyperTwistTrainingCoachSessionQueueStateEntry +{ + GENERATED_BODY() + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString EntryId; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString SourceLabel; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString WorkFingerprint; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString SourceTrainingSessionId; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 SlotIndex = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + EHyperTwistTrainingCoachSessionQueueEntryState EntryState = + EHyperTwistTrainingCoachSessionQueueEntryState::Pending; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString PriorityLabel; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + float PriorityScore = 0.0f; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString Headline; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + EHyperTwistTrainingDeliveryMode SuggestedMode = EHyperTwistTrainingDeliveryMode::Timer; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + EHyperTwistTrainingSelectionPolicy SuggestedSelectionPolicy = EHyperTwistTrainingSelectionPolicy::Weighted; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString FocusDeckId; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString FocusMethodSegmentId; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + TArray FocusCaseIds; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString ScheduledAtUtc; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString StartedAtUtc; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString CompletedAtUtc; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + bool bRequiresFollowUp = false; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + bool bRequiresArchiveRetentionTuning = false; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + bool bPinned = false; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + bool bCanStartImmediately = false; + + bool IsStructurallyValid() const + { + return !EntryId.IsEmpty() && !Headline.IsEmpty() && !WorkFingerprint.IsEmpty(); + } +}; + +USTRUCT(BlueprintType) +struct FHyperTwistTrainingCoachSessionQueueState +{ + GENERATED_BODY() + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString QueueId; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString UserId; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString ReferenceUtc; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString LastUpdatedAtUtc; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString FocusDeckId; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString FocusMethodSegmentId; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString PrimaryQueueLabel; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString NextQueueLabel; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString ActiveEntryId; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString ActiveTrainingSessionId; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + TArray Entries; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 TotalEntryCount = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 PendingEntryCount = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 ReadyEntryCount = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 InProgressEntryCount = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 CompletedEntryCount = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 SkippedEntryCount = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 ExpiredEntryCount = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + bool bHasImmediateQueue = false; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + bool bNeedsArchiveAwareSequencing = false; + + bool IsStructurallyValid() const + { + return !QueueId.IsEmpty() && !UserId.IsEmpty() && Entries.Num() > 0; + } +}; + USTRUCT(BlueprintType) struct FHyperTwistTrainingCoachActionPlanSummary { @@ -3524,6 +3681,9 @@ struct FHyperTwistTrainingRepositoryState UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") TArray StoredCoachActionPlans; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + TArray StoredCoachSessionQueues; }; USTRUCT(BlueprintType) @@ -3594,6 +3754,15 @@ struct FHyperTwistTrainingRepositoryIntegrityReport UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") int32 DuplicateCoachActionPlanCount = 0; + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 TotalCoachSessionQueueCount = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 InvalidCoachSessionQueueCount = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 DuplicateCoachSessionQueueCount = 0; + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") TArray RepairActions; @@ -3612,6 +3781,8 @@ struct FHyperTwistTrainingRepositoryIntegrityReport || InvalidMethodDrillBatchCount > 0 || DuplicateMethodDrillBatchCount > 0 || InvalidCoachActionPlanCount > 0 - || DuplicateCoachActionPlanCount > 0; + || DuplicateCoachActionPlanCount > 0 + || InvalidCoachSessionQueueCount > 0 + || DuplicateCoachSessionQueueCount > 0; } };