From 1a309c862c4833828cf4a2f241dd273c50d65aef Mon Sep 17 00:00:00 2001 From: axiomlogicnexus Date: Sun, 26 Apr 2026 04:32:38 +0200 Subject: [PATCH] Add coach scheduling policy controls --- .../HyperTwistContractLibrary.cpp | 30 +++- .../HyperTwistTrainingCoachLibrary.cpp | 125 +++++++++++++- .../HyperTwistTrainingRepositoryLibrary.cpp | 154 +++++++++++++++--- .../HyperTwistTrainingRuntimeLibrary.cpp | 46 ++++++ .../HyperTwistTrainingSubsystem.cpp | 71 ++++++++ .../HyperTwistContractLibrary.h | 3 + .../HyperTwistTrainingCoachLibrary.h | 88 ++++++++++ .../HyperTwistTrainingRepositoryLibrary.h | 17 ++ .../HyperTwistTrainingRuntimeLibrary.h | 20 +++ .../HyperTwistTrainingSubsystem.h | 17 ++ .../HyperTwistTrainingTypes.h | 19 +++ 11 files changed, 564 insertions(+), 26 deletions(-) diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistBootstrap/HyperTwistContractLibrary.cpp b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistBootstrap/HyperTwistContractLibrary.cpp index 308b2d4..8feca5f 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistBootstrap/HyperTwistContractLibrary.cpp +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistBootstrap/HyperTwistContractLibrary.cpp @@ -1323,11 +1323,29 @@ FHyperTwistCoachSessionQueueSummary UHyperTwistContractLibrary::MakeSampleTraini FHyperTwistTrainingCoachSessionQueueState UHyperTwistContractLibrary::MakeSampleTrainingCoachSessionQueueState() { - return UHyperTwistTrainingRepositoryLibrary::BuildCoachSessionQueueState( + FHyperTwistTrainingCoachSessionQueueState QueueState = UHyperTwistTrainingRepositoryLibrary::BuildCoachSessionQueueState( MakeSampleTrainingCoachSessionQueueSummary(), TEXT("coach_queue_active"), TEXT("2026-04-28T12:12:00Z") ); + + QueueState = UHyperTwistTrainingRepositoryLibrary::DeferCoachSessionQueueEntry( + QueueState, + TEXT("coach_queue_follow_up"), + TEXT("2026-04-28T18:00:00Z"), + TEXT("follow-up-held-behind-primary"), + false, + TEXT("2026-04-28T12:13:00Z") + ); + QueueState = UHyperTwistTrainingRepositoryLibrary::DeferCoachSessionQueueEntry( + QueueState, + TEXT("coach_queue_archive"), + TEXT("2026-04-29T09:00:00Z"), + TEXT("archive-retention-window"), + true, + TEXT("2026-04-28T12:14:00Z") + ); + return QueueState; } FHyperTwistCoachScheduleHorizonSummary UHyperTwistContractLibrary::MakeSampleTrainingCoachScheduleHorizonSummary() @@ -1339,6 +1357,16 @@ FHyperTwistCoachScheduleHorizonSummary UHyperTwistContractLibrary::MakeSampleTra ); } +FHyperTwistCoachSchedulePolicySummary UHyperTwistContractLibrary::MakeSampleTrainingCoachSchedulePolicySummary() +{ + return UHyperTwistTrainingCoachLibrary::DeriveCoachSchedulePolicySummary( + MakeSampleTrainingCoachSessionQueueState(), + MakeSampleTrainingCoachScheduleHorizonSummary(), + MakeSampleTrainingCoachArchivePolicySummary(), + TEXT("2026-04-28T12:30: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 48fa5b6..9ee091d 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingCoachLibrary.cpp +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingCoachLibrary.cpp @@ -746,7 +746,9 @@ FHyperTwistCoachScheduleHorizonSummary UHyperTwistTrainingCoachLibrary::DeriveCo const bool bIsImmediateEntry = Entry.EntryState == EHyperTwistTrainingCoachSessionQueueEntryState::Ready || Entry.EntryState == EHyperTwistTrainingCoachSessionQueueEntryState::InProgress; - const bool bIsDeferredEntry = Entry.EntryState == EHyperTwistTrainingCoachSessionQueueEntryState::Pending; + const bool bIsDeferredEntry = + Entry.EntryState == EHyperTwistTrainingCoachSessionQueueEntryState::Deferred + || Entry.EntryState == EHyperTwistTrainingCoachSessionQueueEntryState::Pending; Summary.ImmediateEntryCount += bIsImmediateEntry ? 1 : 0; Summary.DeferredEntryCount += bIsDeferredEntry ? 1 : 0; @@ -808,3 +810,124 @@ FHyperTwistCoachScheduleHorizonSummary UHyperTwistTrainingCoachLibrary::DeriveCo return Summary; } + +FHyperTwistCoachSchedulePolicySummary UHyperTwistTrainingCoachLibrary::DeriveCoachSchedulePolicySummary( + const FHyperTwistTrainingCoachSessionQueueState& QueueState, + const FHyperTwistCoachScheduleHorizonSummary& HorizonSummary, + const FHyperTwistTrainingCoachArchivePolicySummary& ArchivePolicySummary, + const FString& ReferenceUtc +) +{ + FHyperTwistCoachSchedulePolicySummary Summary; + if (!QueueState.IsStructurallyValid()) + { + return Summary; + } + + Summary.UserId = QueueState.UserId; + Summary.ReferenceUtc = !ReferenceUtc.IsEmpty() + ? ReferenceUtc + : (!QueueState.LastUpdatedAtUtc.IsEmpty() ? QueueState.LastUpdatedAtUtc : QueueState.ReferenceUtc); + Summary.QueueId = QueueState.QueueId; + Summary.FocusDeckId = QueueState.FocusDeckId; + Summary.FocusMethodSegmentId = QueueState.FocusMethodSegmentId; + Summary.NextImmediateEntryId = HorizonSummary.NextReadyEntryId; + + TMap DeferralReasonCounts; + for (const FHyperTwistTrainingCoachSessionQueueStateEntry& Entry : QueueState.Entries) + { + if (!Entry.IsStructurallyValid()) + { + continue; + } + + switch (Entry.EntryState) + { + case EHyperTwistTrainingCoachSessionQueueEntryState::Ready: + case EHyperTwistTrainingCoachSessionQueueEntryState::InProgress: + Summary.ImmediateEntryCount += 1; + break; + case EHyperTwistTrainingCoachSessionQueueEntryState::Pending: + Summary.PendingEntryCount += 1; + if (Summary.NextPendingEntryId.IsEmpty()) + { + Summary.NextPendingEntryId = Entry.EntryId; + } + break; + case EHyperTwistTrainingCoachSessionQueueEntryState::Deferred: + Summary.DeferredEntryCount += 1; + if (Summary.NextDeferredEntryId.IsEmpty()) + { + Summary.NextDeferredEntryId = Entry.EntryId; + } + if (Entry.bRequiresArchiveRetentionTuning) + { + Summary.ArchiveAwareDeferredEntryCount += 1; + } + if (Entry.bRequiresFollowUp) + { + Summary.BlockedFollowUpDeferredEntryCount += 1; + } + if (Summary.EarliestDeferredUntilUtc.IsEmpty() + || (!Entry.DeferredUntilUtc.IsEmpty() && Entry.DeferredUntilUtc < Summary.EarliestDeferredUntilUtc)) + { + Summary.EarliestDeferredUntilUtc = Entry.DeferredUntilUtc; + } + if (!Entry.DeferralReasonLabel.IsEmpty()) + { + DeferralReasonCounts.FindOrAdd(Entry.DeferralReasonLabel) += 1; + } + if (!Entry.DeferredUntilUtc.IsEmpty() && Entry.DeferredUntilUtc <= Summary.ReferenceUtc) + { + Summary.bHasExpiredDeferrals = true; + } + break; + default: + break; + } + + if (Entry.bWasManuallyRescheduled) + { + Summary.ManualRescheduleEntryCount += 1; + if (Summary.NextManualRescheduleEntryId.IsEmpty() + && (Entry.EntryState == EHyperTwistTrainingCoachSessionQueueEntryState::Deferred + || Entry.EntryState == EHyperTwistTrainingCoachSessionQueueEntryState::Pending)) + { + Summary.NextManualRescheduleEntryId = Entry.EntryId; + } + } + } + + int32 DominantReasonCount = 0; + for (const TPair& Pair : DeferralReasonCounts) + { + if (Pair.Value > DominantReasonCount + || (Pair.Value == DominantReasonCount + && (Summary.DominantDeferralReasonLabel.IsEmpty() + || Pair.Key < Summary.DominantDeferralReasonLabel))) + { + DominantReasonCount = Pair.Value; + Summary.DominantDeferralReasonLabel = Pair.Key; + } + } + + Summary.bAllowsManualReschedule = QueueState.IsStructurallyValid() + && !HorizonSummary.bHasInProgressEntry; + Summary.bHasArchiveAwareDeferrals = Summary.ArchiveAwareDeferredEntryCount > 0 + || (ArchivePolicySummary.bNeedsArchiveRetentionTuning && Summary.DeferredEntryCount > 0); + Summary.bHasBlockedFollowUpDeferrals = Summary.BlockedFollowUpDeferredEntryCount > 0; + Summary.bNeedsManualRescheduleReview = Summary.bHasExpiredDeferrals + || Summary.ManualRescheduleEntryCount >= 2 + || Summary.bHasBlockedFollowUpDeferrals; + Summary.ReschedulePressureScore = FMath::Clamp( + static_cast(Summary.DeferredEntryCount) * 8.0f + + static_cast(Summary.ManualRescheduleEntryCount) * 10.0f + + static_cast(Summary.ArchiveAwareDeferredEntryCount) * 12.0f + + static_cast(Summary.BlockedFollowUpDeferredEntryCount) * 12.0f + + (Summary.bHasExpiredDeferrals ? 15.0f : 0.0f), + 0.0f, + 100.0f + ); + + return Summary; +} diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingRepositoryLibrary.cpp b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingRepositoryLibrary.cpp index bf5a26b..46a7c97 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingRepositoryLibrary.cpp +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingRepositoryLibrary.cpp @@ -487,6 +487,18 @@ namespace HyperTwistTrainingRepositoryLibraryInternal || EntryState == EHyperTwistTrainingCoachSessionQueueEntryState::Expired; } + bool IsUtcDueOrPast(const FString& CandidateUtc, const FString& ReferenceUtc) + { + FDateTime CandidateTime; + FDateTime ReferenceTime; + if (!TryParseUtc(CandidateUtc, CandidateTime) || !TryParseUtc(ReferenceUtc, ReferenceTime)) + { + return false; + } + + return CandidateTime <= ReferenceTime; + } + FString MakeCoachSessionQueueEntryFingerprint( const FHyperTwistTrainingCoachSessionQueueStateEntry& Entry ) @@ -531,9 +543,20 @@ namespace HyperTwistTrainingRepositoryLibraryInternal { NormalizedEntry.WorkFingerprint = MakeCoachSessionQueueEntryFingerprint(NormalizedEntry); } + NormalizedEntry.RescheduleCount = FMath::Max(0, NormalizedEntry.RescheduleCount); NormalizedEntry.bPinned = NormalizedEntry.bPinned || NormalizedEntry.bRequiresFollowUp || NormalizedEntry.bRequiresArchiveRetentionTuning; + if (NormalizedEntry.EntryState == EHyperTwistTrainingCoachSessionQueueEntryState::Deferred + && NormalizedEntry.DeferredUntilUtc.IsEmpty()) + { + NormalizedEntry.EntryState = EHyperTwistTrainingCoachSessionQueueEntryState::Pending; + } + if (NormalizedEntry.EntryState == EHyperTwistTrainingCoachSessionQueueEntryState::Deferred + && IsUtcDueOrPast(NormalizedEntry.DeferredUntilUtc, NormalizedState.ReferenceUtc)) + { + NormalizedEntry.EntryState = EHyperTwistTrainingCoachSessionQueueEntryState::Pending; + } if (NormalizedEntry.EntryState == EHyperTwistTrainingCoachSessionQueueEntryState::Ready || NormalizedEntry.EntryState == EHyperTwistTrainingCoachSessionQueueEntryState::InProgress) { @@ -541,7 +564,8 @@ namespace HyperTwistTrainingRepositoryLibraryInternal ? NormalizedEntry.ScheduledAtUtc : NormalizedState.ReferenceUtc; } - if (IsTerminalCoachQueueEntryState(NormalizedEntry.EntryState)) + if (NormalizedEntry.EntryState == EHyperTwistTrainingCoachSessionQueueEntryState::Deferred + || IsTerminalCoachQueueEntryState(NormalizedEntry.EntryState)) { NormalizedEntry.bCanStartImmediately = false; } @@ -580,6 +604,7 @@ namespace HyperTwistTrainingRepositoryLibraryInternal NormalizedState.ActiveEntryId.Reset(); NormalizedState.ActiveTrainingSessionId.Reset(); NormalizedState.PendingEntryCount = 0; + NormalizedState.DeferredEntryCount = 0; NormalizedState.ReadyEntryCount = 0; NormalizedState.InProgressEntryCount = 0; NormalizedState.CompletedEntryCount = 0; @@ -632,6 +657,9 @@ namespace HyperTwistTrainingRepositoryLibraryInternal case EHyperTwistTrainingCoachSessionQueueEntryState::Pending: NormalizedState.PendingEntryCount += 1; break; + case EHyperTwistTrainingCoachSessionQueueEntryState::Deferred: + NormalizedState.DeferredEntryCount += 1; + break; case EHyperTwistTrainingCoachSessionQueueEntryState::Ready: NormalizedState.ReadyEntryCount += 1; break; @@ -656,30 +684,9 @@ namespace HyperTwistTrainingRepositoryLibraryInternal { for (FHyperTwistTrainingCoachSessionQueueStateEntry& Entry : NormalizedEntries) { - if (!IsTerminalCoachQueueEntryState(Entry.EntryState)) + if (Entry.EntryState == EHyperTwistTrainingCoachSessionQueueEntryState::Pending) { - 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; - } - + NormalizedState.PendingEntryCount -= 1; Entry.EntryState = EHyperTwistTrainingCoachSessionQueueEntryState::Ready; Entry.ScheduledAtUtc = !Entry.ScheduledAtUtc.IsEmpty() ? Entry.ScheduledAtUtc : NormalizedState.ReferenceUtc; Entry.bCanStartImmediately = true; @@ -5398,10 +5405,15 @@ FHyperTwistTrainingCoachSessionQueueState UHyperTwistTrainingRepositoryLibrary:: QueueEntry.EntryState = ExistingEntry->EntryState; QueueEntry.SourceTrainingSessionId = ExistingEntry->SourceTrainingSessionId; QueueEntry.ScheduledAtUtc = ExistingEntry->ScheduledAtUtc; + QueueEntry.DeferredUntilUtc = ExistingEntry->DeferredUntilUtc; + QueueEntry.DeferredAtUtc = ExistingEntry->DeferredAtUtc; + QueueEntry.DeferralReasonLabel = ExistingEntry->DeferralReasonLabel; QueueEntry.StartedAtUtc = ExistingEntry->StartedAtUtc; QueueEntry.CompletedAtUtc = ExistingEntry->CompletedAtUtc; + QueueEntry.RescheduleCount = ExistingEntry->RescheduleCount; QueueEntry.bPinned = QueueEntry.bPinned || ExistingEntry->bPinned; QueueEntry.bCanStartImmediately = ExistingEntry->bCanStartImmediately; + QueueEntry.bWasManuallyRescheduled = ExistingEntry->bWasManuallyRescheduled; } ResolvedQueueState.LastUpdatedAtUtc = HyperTwistTrainingRepositoryLibraryInternal::ResolveReferenceUtc( @@ -5431,6 +5443,7 @@ FHyperTwistTrainingCoachSessionQueueState UHyperTwistTrainingRepositoryLibrary:: { const FHyperTwistTrainingCoachSessionQueueStateEntry& Entry = UpdatedQueueState.Entries[EntryIndex]; if (Entry.SourceLabel == SourceLabel + && Entry.EntryState != EHyperTwistTrainingCoachSessionQueueEntryState::Deferred && !HyperTwistTrainingRepositoryLibraryInternal::IsTerminalCoachQueueEntryState(Entry.EntryState)) { ActivatedIndex = EntryIndex; @@ -5460,6 +5473,7 @@ FHyperTwistTrainingCoachSessionQueueState UHyperTwistTrainingRepositoryLibrary:: ActiveEntry.ScheduledAtUtc = !ActiveEntry.ScheduledAtUtc.IsEmpty() ? ActiveEntry.ScheduledAtUtc : ResolvedStartedAtUtc; + ActiveEntry.DeferredUntilUtc.Reset(); ActiveEntry.StartedAtUtc = ResolvedStartedAtUtc; ActiveEntry.bCanStartImmediately = false; UpdatedQueueState.LastUpdatedAtUtc = ResolvedStartedAtUtc; @@ -5513,6 +5527,98 @@ FHyperTwistTrainingCoachSessionQueueState UHyperTwistTrainingRepositoryLibrary:: return HyperTwistTrainingRepositoryLibraryInternal::NormalizeCoachSessionQueueState(UpdatedQueueState); } +FHyperTwistTrainingCoachSessionQueueState UHyperTwistTrainingRepositoryLibrary::DeferCoachSessionQueueEntry( + const FHyperTwistTrainingCoachSessionQueueState& QueueState, + const FString& EntryId, + const FString& DeferredUntilUtc, + const FString& DeferralReasonLabel, + bool bManualReschedule, + const FString& ReferenceUtc +) +{ + FHyperTwistTrainingCoachSessionQueueState UpdatedQueueState = + HyperTwistTrainingRepositoryLibraryInternal::NormalizeCoachSessionQueueState(QueueState); + if (!UpdatedQueueState.IsStructurallyValid() || EntryId.IsEmpty() || DeferredUntilUtc.IsEmpty()) + { + return UpdatedQueueState; + } + + const FString ResolvedReferenceUtc = + HyperTwistTrainingRepositoryLibraryInternal::ResolveReferenceUtc( + ReferenceUtc.IsEmpty() ? UpdatedQueueState.ReferenceUtc : ReferenceUtc + ); + FHyperTwistTrainingCoachSessionQueueStateEntry* EntryToDefer = + UpdatedQueueState.Entries.FindByPredicate( + [&EntryId](const FHyperTwistTrainingCoachSessionQueueStateEntry& Candidate) + { + return Candidate.EntryId == EntryId + && Candidate.EntryState != EHyperTwistTrainingCoachSessionQueueEntryState::InProgress + && !HyperTwistTrainingRepositoryLibraryInternal::IsTerminalCoachQueueEntryState(Candidate.EntryState); + } + ); + if (EntryToDefer == nullptr) + { + return UpdatedQueueState; + } + + EntryToDefer->EntryState = EHyperTwistTrainingCoachSessionQueueEntryState::Deferred; + EntryToDefer->DeferredUntilUtc = DeferredUntilUtc; + EntryToDefer->DeferredAtUtc = ResolvedReferenceUtc; + EntryToDefer->DeferralReasonLabel = !DeferralReasonLabel.IsEmpty() + ? DeferralReasonLabel + : (bManualReschedule ? TEXT("manual-reschedule") : TEXT("deferred-window")); + EntryToDefer->SourceTrainingSessionId.Reset(); + EntryToDefer->StartedAtUtc.Reset(); + EntryToDefer->CompletedAtUtc.Reset(); + EntryToDefer->bCanStartImmediately = false; + EntryToDefer->RescheduleCount += 1; + EntryToDefer->bWasManuallyRescheduled = EntryToDefer->bWasManuallyRescheduled || bManualReschedule; + UpdatedQueueState.ActiveEntryId.Reset(); + UpdatedQueueState.ActiveTrainingSessionId.Reset(); + UpdatedQueueState.LastUpdatedAtUtc = ResolvedReferenceUtc; + return HyperTwistTrainingRepositoryLibraryInternal::NormalizeCoachSessionQueueState(UpdatedQueueState); +} + +FHyperTwistTrainingCoachSessionQueueState UHyperTwistTrainingRepositoryLibrary::PromoteCoachSessionQueueEntry( + const FHyperTwistTrainingCoachSessionQueueState& QueueState, + const FString& EntryId, + const FString& ReferenceUtc +) +{ + FHyperTwistTrainingCoachSessionQueueState UpdatedQueueState = + HyperTwistTrainingRepositoryLibraryInternal::NormalizeCoachSessionQueueState(QueueState); + if (!UpdatedQueueState.IsStructurallyValid() || EntryId.IsEmpty()) + { + return UpdatedQueueState; + } + + const FString ResolvedReferenceUtc = + HyperTwistTrainingRepositoryLibraryInternal::ResolveReferenceUtc( + ReferenceUtc.IsEmpty() ? UpdatedQueueState.ReferenceUtc : ReferenceUtc + ); + FHyperTwistTrainingCoachSessionQueueStateEntry* EntryToPromote = + UpdatedQueueState.Entries.FindByPredicate( + [&EntryId](const FHyperTwistTrainingCoachSessionQueueStateEntry& Candidate) + { + return Candidate.EntryId == EntryId + && Candidate.EntryState != EHyperTwistTrainingCoachSessionQueueEntryState::InProgress + && !HyperTwistTrainingRepositoryLibraryInternal::IsTerminalCoachQueueEntryState(Candidate.EntryState); + } + ); + if (EntryToPromote == nullptr) + { + return UpdatedQueueState; + } + + EntryToPromote->EntryState = EHyperTwistTrainingCoachSessionQueueEntryState::Ready; + EntryToPromote->ScheduledAtUtc = ResolvedReferenceUtc; + EntryToPromote->DeferredUntilUtc.Reset(); + EntryToPromote->bCanStartImmediately = true; + EntryToPromote->SlotIndex = -1; + UpdatedQueueState.LastUpdatedAtUtc = ResolvedReferenceUtc; + return HyperTwistTrainingRepositoryLibraryInternal::NormalizeCoachSessionQueueState(UpdatedQueueState); +} + TArray UHyperTwistTrainingRepositoryLibrary::ListCoachSessionQueueStatesForUser( 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 b66c4a7..d4c14b6 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingRuntimeLibrary.cpp +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingRuntimeLibrary.cpp @@ -393,6 +393,18 @@ FHyperTwistCoachScheduleHorizonSummary UHyperTwistTrainingRuntimeLibrary::GetAct return FHyperTwistCoachScheduleHorizonSummary(); } +FHyperTwistCoachSchedulePolicySummary UHyperTwistTrainingRuntimeLibrary::GetActiveTrainingCoachSchedulePolicySummary( + UObject* WorldContextObject +) +{ + if (UHyperTwistTrainingSubsystem* TrainingSubsystem = GetTrainingSubsystem(WorldContextObject)) + { + return TrainingSubsystem->GetActiveCoachSchedulePolicySummary(); + } + + return FHyperTwistCoachSchedulePolicySummary(); +} + TArray UHyperTwistTrainingRuntimeLibrary::GetActiveTrainingLearnerMethodSegments(UObject* WorldContextObject) { if (UHyperTwistTrainingSubsystem* TrainingSubsystem = GetTrainingSubsystem(WorldContextObject)) @@ -697,6 +709,40 @@ bool UHyperTwistTrainingRuntimeLibrary::SaveCoachSessionQueueState( return false; } +bool UHyperTwistTrainingRuntimeLibrary::DeferCoachSessionQueueEntry( + UObject* WorldContextObject, + const FString& EntryId, + const FString& DeferredUntilUtc, + const FString& DeferralReasonLabel, + bool bManualReschedule +) +{ + if (UHyperTwistTrainingSubsystem* TrainingSubsystem = GetTrainingSubsystem(WorldContextObject)) + { + return TrainingSubsystem->DeferCoachSessionQueueEntry( + EntryId, + DeferredUntilUtc, + DeferralReasonLabel, + bManualReschedule + ); + } + + return false; +} + +bool UHyperTwistTrainingRuntimeLibrary::PromoteCoachSessionQueueEntry( + UObject* WorldContextObject, + const FString& EntryId +) +{ + if (UHyperTwistTrainingSubsystem* TrainingSubsystem = GetTrainingSubsystem(WorldContextObject)) + { + return TrainingSubsystem->PromoteCoachSessionQueueEntry(EntryId); + } + + 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 4a4c464..0c58cf3 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingSubsystem.cpp +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingSubsystem.cpp @@ -178,6 +178,11 @@ FHyperTwistCoachScheduleHorizonSummary UHyperTwistTrainingSubsystem::GetActiveCo return ActiveCoachScheduleHorizonSummary; } +FHyperTwistCoachSchedulePolicySummary UHyperTwistTrainingSubsystem::GetActiveCoachSchedulePolicySummary() const +{ + return ActiveCoachSchedulePolicySummary; +} + TArray UHyperTwistTrainingSubsystem::GetActiveLearnerMethodSegments() const { return ActiveLearnerMethodSegments; @@ -445,6 +450,64 @@ bool UHyperTwistTrainingSubsystem::SaveCoachSessionQueueState( return true; } +bool UHyperTwistTrainingSubsystem::DeferCoachSessionQueueEntry( + const FString& EntryId, + const FString& DeferredUntilUtc, + const FString& DeferralReasonLabel, + bool bManualReschedule +) +{ + if (!ActiveCoachSessionQueueState.IsStructurallyValid() || EntryId.IsEmpty() || DeferredUntilUtc.IsEmpty()) + { + return false; + } + + ActiveCoachSessionQueueState = UHyperTwistTrainingRepositoryLibrary::DeferCoachSessionQueueEntry( + ActiveCoachSessionQueueState, + EntryId, + DeferredUntilUtc, + DeferralReasonLabel, + bManualReschedule, + FDateTime::UtcNow().ToIso8601() + ); + if (!ActiveCoachSessionQueueState.IsStructurallyValid()) + { + return false; + } + + TrainingRepositoryState = UHyperTwistTrainingRepositoryLibrary::UpsertCoachSessionQueueState( + TrainingRepositoryState, + ActiveCoachSessionQueueState + ); + RefreshRepositoryViews(); + return true; +} + +bool UHyperTwistTrainingSubsystem::PromoteCoachSessionQueueEntry(const FString& EntryId) +{ + if (!ActiveCoachSessionQueueState.IsStructurallyValid() || EntryId.IsEmpty()) + { + return false; + } + + ActiveCoachSessionQueueState = UHyperTwistTrainingRepositoryLibrary::PromoteCoachSessionQueueEntry( + ActiveCoachSessionQueueState, + EntryId, + FDateTime::UtcNow().ToIso8601() + ); + if (!ActiveCoachSessionQueueState.IsStructurallyValid()) + { + return false; + } + + TrainingRepositoryState = UHyperTwistTrainingRepositoryLibrary::UpsertCoachSessionQueueState( + TrainingRepositoryState, + ActiveCoachSessionQueueState + ); + RefreshRepositoryViews(); + return true; +} + bool UHyperTwistTrainingSubsystem::RemoveMethodDrillBatch( const FString& UserId, const FString& BatchId @@ -1321,6 +1384,7 @@ void UHyperTwistTrainingSubsystem::RefreshRepositoryViews() ActiveCoachSessionQueueSummary = FHyperTwistCoachSessionQueueSummary(); ActiveCoachSessionQueueState = FHyperTwistTrainingCoachSessionQueueState(); ActiveCoachScheduleHorizonSummary = FHyperTwistCoachScheduleHorizonSummary(); + ActiveCoachSchedulePolicySummary = FHyperTwistCoachSchedulePolicySummary(); ActiveLearnerMethodSegments.Reset(); ActiveLearnerPresets.Reset(); ActiveTrainingSessionTemplates.Reset(); @@ -1528,11 +1592,18 @@ void UHyperTwistTrainingSubsystem::RefreshRepositoryViews() ActiveCoachArchivePolicySummary, ActiveLearnerDeckStateSummary.ReferenceUtc ); + ActiveCoachSchedulePolicySummary = UHyperTwistTrainingCoachLibrary::DeriveCoachSchedulePolicySummary( + ActiveCoachSessionQueueState, + ActiveCoachScheduleHorizonSummary, + ActiveCoachArchivePolicySummary, + ActiveLearnerDeckStateSummary.ReferenceUtc + ); } else { ActiveCoachSessionQueueState = FHyperTwistTrainingCoachSessionQueueState(); ActiveCoachScheduleHorizonSummary = FHyperTwistCoachScheduleHorizonSummary(); + ActiveCoachSchedulePolicySummary = FHyperTwistCoachSchedulePolicySummary(); } } diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistBootstrap/HyperTwistContractLibrary.h b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistBootstrap/HyperTwistContractLibrary.h index 3af4e9e..957d61b 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistBootstrap/HyperTwistContractLibrary.h +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistBootstrap/HyperTwistContractLibrary.h @@ -215,6 +215,9 @@ public: UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Coach") static FHyperTwistCoachScheduleHorizonSummary MakeSampleTrainingCoachScheduleHorizonSummary(); + UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Coach") + static FHyperTwistCoachSchedulePolicySummary MakeSampleTrainingCoachSchedulePolicySummary(); + 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 5a05233..6f96825 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingCoachLibrary.h +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingCoachLibrary.h @@ -311,6 +311,86 @@ struct FHyperTwistCoachScheduleHorizonSummary } }; +USTRUCT(BlueprintType) +struct FHyperTwistCoachSchedulePolicySummary +{ + GENERATED_BODY() + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString UserId; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString ReferenceUtc; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString QueueId; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString FocusDeckId; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString FocusMethodSegmentId; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString NextImmediateEntryId; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString NextPendingEntryId; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString NextDeferredEntryId; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString NextManualRescheduleEntryId; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString EarliestDeferredUntilUtc; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString DominantDeferralReasonLabel; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 ImmediateEntryCount = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 PendingEntryCount = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 DeferredEntryCount = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 ManualRescheduleEntryCount = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 ArchiveAwareDeferredEntryCount = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 BlockedFollowUpDeferredEntryCount = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + float ReschedulePressureScore = 0.0f; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + bool bAllowsManualReschedule = false; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + bool bHasExpiredDeferrals = false; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + bool bNeedsManualRescheduleReview = false; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + bool bHasArchiveAwareDeferrals = false; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + bool bHasBlockedFollowUpDeferrals = false; + + bool IsStructurallyValid() const + { + return !UserId.IsEmpty() && !QueueId.IsEmpty(); + } +}; + UCLASS() class UNREALHYPERTWIST_API UHyperTwistTrainingCoachLibrary : public UBlueprintFunctionLibrary { @@ -362,4 +442,12 @@ public: const FHyperTwistTrainingCoachArchivePolicySummary& ArchivePolicySummary, const FString& ReferenceUtc ); + + UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Coach") + static FHyperTwistCoachSchedulePolicySummary DeriveCoachSchedulePolicySummary( + const FHyperTwistTrainingCoachSessionQueueState& QueueState, + const FHyperTwistCoachScheduleHorizonSummary& HorizonSummary, + const FHyperTwistTrainingCoachArchivePolicySummary& ArchivePolicySummary, + const FString& ReferenceUtc + ); }; diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingRepositoryLibrary.h b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingRepositoryLibrary.h index 11a3cc4..9ae05b9 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingRepositoryLibrary.h +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingRepositoryLibrary.h @@ -377,6 +377,23 @@ public: bool bAbortEntry ); + UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Repository|Coach") + static FHyperTwistTrainingCoachSessionQueueState DeferCoachSessionQueueEntry( + const FHyperTwistTrainingCoachSessionQueueState& QueueState, + const FString& EntryId, + const FString& DeferredUntilUtc, + const FString& DeferralReasonLabel, + bool bManualReschedule, + const FString& ReferenceUtc + ); + + UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Repository|Coach") + static FHyperTwistTrainingCoachSessionQueueState PromoteCoachSessionQueueEntry( + const FHyperTwistTrainingCoachSessionQueueState& QueueState, + const FString& EntryId, + const FString& ReferenceUtc + ); + UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Repository|Coach") static TArray ListCoachSessionQueueStatesForUser( const FHyperTwistTrainingRepositoryState& RepositoryState, diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingRuntimeLibrary.h b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingRuntimeLibrary.h index d70111d..731114a 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingRuntimeLibrary.h +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingRuntimeLibrary.h @@ -130,6 +130,11 @@ public: UObject* WorldContextObject ); + UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Coach", meta = (WorldContext = "WorldContextObject")) + static FHyperTwistCoachSchedulePolicySummary GetActiveTrainingCoachSchedulePolicySummary( + UObject* WorldContextObject + ); + UFUNCTION(BlueprintPure, Category = "HyperTwist|Training", meta = (WorldContext = "WorldContextObject")) static TArray GetActiveTrainingLearnerMethodSegments(UObject* WorldContextObject); @@ -266,6 +271,21 @@ public: const FHyperTwistTrainingCoachSessionQueueState& QueueState ); + UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Coach", meta = (WorldContext = "WorldContextObject")) + static bool DeferCoachSessionQueueEntry( + UObject* WorldContextObject, + const FString& EntryId, + const FString& DeferredUntilUtc, + const FString& DeferralReasonLabel, + bool bManualReschedule + ); + + UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Coach", meta = (WorldContext = "WorldContextObject")) + static bool PromoteCoachSessionQueueEntry( + UObject* WorldContextObject, + const FString& EntryId + ); + 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 52d316e..a7609a1 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingSubsystem.h +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingSubsystem.h @@ -113,6 +113,9 @@ public: UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Coach") FHyperTwistCoachScheduleHorizonSummary GetActiveCoachScheduleHorizonSummary() const; + UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Coach") + FHyperTwistCoachSchedulePolicySummary GetActiveCoachSchedulePolicySummary() const; + UFUNCTION(BlueprintPure, Category = "HyperTwist|Training") TArray GetActiveLearnerMethodSegments() const; @@ -225,6 +228,17 @@ public: UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Coach") bool SaveCoachSessionQueueState(const FHyperTwistTrainingCoachSessionQueueState& QueueState); + UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Coach") + bool DeferCoachSessionQueueEntry( + const FString& EntryId, + const FString& DeferredUntilUtc, + const FString& DeferralReasonLabel, + bool bManualReschedule + ); + + UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Coach") + bool PromoteCoachSessionQueueEntry(const FString& EntryId); + UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Drill") bool RemoveMethodDrillBatch( const FString& UserId, @@ -481,6 +495,9 @@ private: UPROPERTY() FHyperTwistCoachScheduleHorizonSummary ActiveCoachScheduleHorizonSummary; + UPROPERTY() + FHyperTwistCoachSchedulePolicySummary ActiveCoachSchedulePolicySummary; + UPROPERTY() TArray ActiveLearnerMethodSegments; diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingTypes.h b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingTypes.h index 64cb8f8..4217ceb 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingTypes.h +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingTypes.h @@ -2722,6 +2722,7 @@ UENUM(BlueprintType) enum class EHyperTwistTrainingCoachSessionQueueEntryState : uint8 { Pending UMETA(DisplayName = "Pending"), + Deferred UMETA(DisplayName = "Deferred"), Ready UMETA(DisplayName = "Ready"), InProgress UMETA(DisplayName = "In Progress"), Completed UMETA(DisplayName = "Completed"), @@ -2921,12 +2922,24 @@ struct FHyperTwistTrainingCoachSessionQueueStateEntry UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") FString ScheduledAtUtc; + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString DeferredUntilUtc; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString DeferredAtUtc; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString DeferralReasonLabel; + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") FString StartedAtUtc; UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") FString CompletedAtUtc; + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 RescheduleCount = 0; + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") bool bRequiresFollowUp = false; @@ -2939,6 +2952,9 @@ struct FHyperTwistTrainingCoachSessionQueueStateEntry UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") bool bCanStartImmediately = false; + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + bool bWasManuallyRescheduled = false; + bool IsStructurallyValid() const { return !EntryId.IsEmpty() && !Headline.IsEmpty() && !WorkFingerprint.IsEmpty(); @@ -2989,6 +3005,9 @@ struct FHyperTwistTrainingCoachSessionQueueState UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") int32 PendingEntryCount = 0; + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 DeferredEntryCount = 0; + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") int32 ReadyEntryCount = 0;