diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistCoachDashboardWidget.cpp b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistCoachDashboardWidget.cpp index ae78e78..c30e507 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistCoachDashboardWidget.cpp +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistCoachDashboardWidget.cpp @@ -11,6 +11,17 @@ namespace HyperTwistCoachDashboardWidgetInternal { + template + FString EnumDisplayName(const TEnum Value) + { + if (const UEnum* Enum = StaticEnum()) + { + return Enum->GetDisplayNameTextByValue(static_cast(Value)).ToString(); + } + + return TEXT("Unknown"); + } + UTextBlock* AddTextRow( UWidgetTree* WidgetTree, UVerticalBox* Parent, @@ -75,6 +86,7 @@ void UHyperTwistCoachDashboardWidget::NativeConstruct() void UHyperTwistCoachDashboardWidget::RefreshCoachDashboardView() { RefreshTrainingState(); + SyncSelectedQueueEntry(); UpdateDashboardPresentation(); } @@ -94,6 +106,7 @@ FHyperTwistTrainingRunState UHyperTwistCoachDashboardWidget::ExecutePrimaryCoach RunState = StartCoachRecommendedRun(DefaultCoachMaxCases); } + SyncSelectedQueueEntry(); UpdateDashboardPresentation(); return RunState; } @@ -105,23 +118,90 @@ bool UHyperTwistCoachDashboardWidget::DeferDisplayedQueueEntryByDefaultWindow() TEXT("dashboard-defer"), true ); + SyncSelectedQueueEntry(); UpdateDashboardPresentation(); return bSucceeded; } bool UHyperTwistCoachDashboardWidget::PromoteFirstDeferredQueueEntry() { - const FString DeferredEntryId = FindFirstDeferredQueueEntryId(); + FString DeferredEntryId; + if (const FHyperTwistTrainingCoachSessionQueueStateEntry* SelectedEntry = FindSelectedQueueEntry(); + SelectedEntry != nullptr + && SelectedEntry->EntryState == EHyperTwistTrainingCoachSessionQueueEntryState::Deferred) + { + DeferredEntryId = SelectedEntry->EntryId; + } + else + { + DeferredEntryId = FindFirstDeferredQueueEntryId(); + } if (DeferredEntryId.IsEmpty()) { return false; } const bool bSucceeded = PromoteCoachQueueEntry(DeferredEntryId); + SyncSelectedQueueEntry(); UpdateDashboardPresentation(); return bSucceeded; } +bool UHyperTwistCoachDashboardWidget::SelectPreviousQueueEntry() +{ + if (CachedCoachSessionQueueState.Entries.Num() <= 0) + { + SelectedQueueEntryIndex = INDEX_NONE; + return false; + } + + if (SelectedQueueEntryIndex == INDEX_NONE) + { + SyncSelectedQueueEntry(); + return SelectedQueueEntryIndex != INDEX_NONE; + } + + SelectedQueueEntryIndex = + (SelectedQueueEntryIndex - 1 + CachedCoachSessionQueueState.Entries.Num()) + % CachedCoachSessionQueueState.Entries.Num(); + UpdateDashboardPresentation(); + return true; +} + +bool UHyperTwistCoachDashboardWidget::SelectNextQueueEntry() +{ + if (CachedCoachSessionQueueState.Entries.Num() <= 0) + { + SelectedQueueEntryIndex = INDEX_NONE; + return false; + } + + if (SelectedQueueEntryIndex == INDEX_NONE) + { + SyncSelectedQueueEntry(); + return SelectedQueueEntryIndex != INDEX_NONE; + } + + SelectedQueueEntryIndex = (SelectedQueueEntryIndex + 1) % CachedCoachSessionQueueState.Entries.Num(); + UpdateDashboardPresentation(); + return true; +} + +FHyperTwistTrainingRunState UHyperTwistCoachDashboardWidget::StartSelectedQueueEntry() +{ + if (const FHyperTwistTrainingCoachSessionQueueStateEntry* SelectedEntry = FindSelectedQueueEntry(); + SelectedEntry != nullptr) + { + FHyperTwistTrainingRunState RunState = + StartCoachQueueEntry(SelectedEntry->EntryId, DefaultCoachMaxCases); + SyncSelectedQueueEntry(); + UpdateDashboardPresentation(); + return RunState; + } + + return FHyperTwistTrainingRunState(); +} + void UHyperTwistCoachDashboardWidget::HandlePrimaryActionClicked() { ExecutePrimaryCoachAction(); @@ -135,7 +215,21 @@ void UHyperTwistCoachDashboardWidget::HandleVerifyClicked() void UHyperTwistCoachDashboardWidget::HandleDeferClicked() { - DeferDisplayedQueueEntryByDefaultWindow(); + if (const FHyperTwistTrainingCoachSessionQueueStateEntry* SelectedEntry = FindSelectedQueueEntry()) + { + DeferCoachQueueEntry( + SelectedEntry->EntryId, + BuildDefaultDeferredUntilUtc(), + TEXT("dashboard-defer"), + true + ); + SyncSelectedQueueEntry(); + UpdateDashboardPresentation(); + } + else + { + DeferDisplayedQueueEntryByDefaultWindow(); + } } void UHyperTwistCoachDashboardWidget::HandlePromoteClicked() @@ -143,6 +237,21 @@ void UHyperTwistCoachDashboardWidget::HandlePromoteClicked() PromoteFirstDeferredQueueEntry(); } +void UHyperTwistCoachDashboardWidget::HandlePreviousQueueClicked() +{ + SelectPreviousQueueEntry(); +} + +void UHyperTwistCoachDashboardWidget::HandleStartSelectedClicked() +{ + StartSelectedQueueEntry(); +} + +void UHyperTwistCoachDashboardWidget::HandleNextQueueClicked() +{ + SelectNextQueueEntry(); +} + void UHyperTwistCoachDashboardWidget::EnsureDefaultDashboardBuilt() { if (WidgetTree == nullptr || WidgetTree->RootWidget != nullptr) @@ -176,18 +285,42 @@ void UHyperTwistCoachDashboardWidget::EnsureDefaultDashboardBuilt() TEXT("CoachFocus"), 8 ); + FollowUpTextBlock = HyperTwistCoachDashboardWidgetInternal::AddTextRow( + WidgetTree, + RootLayout, + TEXT("CoachFollowUp"), + 4 + ); QueueStatusTextBlock = HyperTwistCoachDashboardWidgetInternal::AddTextRow( WidgetTree, RootLayout, TEXT("CoachQueueStatus"), 2 ); + QueueSelectionTextBlock = HyperTwistCoachDashboardWidgetInternal::AddTextRow( + WidgetTree, + RootLayout, + TEXT("CoachQueueSelection"), + 2 + ); + QueueEntryDetailTextBlock = HyperTwistCoachDashboardWidgetInternal::AddTextRow( + WidgetTree, + RootLayout, + TEXT("CoachQueueEntryDetail"), + 2 + ); QueueCountsTextBlock = HyperTwistCoachDashboardWidgetInternal::AddTextRow( WidgetTree, RootLayout, TEXT("CoachQueueCounts"), 2 ); + QueueExecutionTextBlock = HyperTwistCoachDashboardWidgetInternal::AddTextRow( + WidgetTree, + RootLayout, + TEXT("CoachQueueExecution"), + 2 + ); VerificationStatusTextBlock = HyperTwistCoachDashboardWidgetInternal::AddTextRow( WidgetTree, RootLayout, @@ -251,6 +384,51 @@ void UHyperTwistCoachDashboardWidget::EnsureDefaultDashboardBuilt() PromoteButtonLabel = PromoteLabelRaw; } + UHorizontalBox* QueueNavRow = WidgetTree->ConstructWidget( + UHorizontalBox::StaticClass(), + TEXT("CoachDashboardQueueNavButtons") + ); + if (QueueNavRow != nullptr) + { + if (UVerticalBoxSlot* QueueNavRowSlot = RootLayout->AddChildToVerticalBox(QueueNavRow)) + { + QueueNavRowSlot->SetPadding(FMargin(0.0f, 4.0f, 0.0f, 0.0f)); + } + + UTextBlock* PreviousQueueLabelRaw = nullptr; + PreviousQueueButton = HyperTwistCoachDashboardWidgetInternal::AddButton( + WidgetTree, + QueueNavRow, + TEXT("PreviousQueueButton"), + TEXT("PreviousQueueButtonLabel"), + TEXT("Prev Queue"), + PreviousQueueLabelRaw + ); + PreviousQueueButtonLabel = PreviousQueueLabelRaw; + + UTextBlock* StartSelectedLabelRaw = nullptr; + StartSelectedButton = HyperTwistCoachDashboardWidgetInternal::AddButton( + WidgetTree, + QueueNavRow, + TEXT("StartSelectedQueueButton"), + TEXT("StartSelectedQueueButtonLabel"), + TEXT("Start Selected"), + StartSelectedLabelRaw + ); + StartSelectedButtonLabel = StartSelectedLabelRaw; + + UTextBlock* NextQueueLabelRaw = nullptr; + NextQueueButton = HyperTwistCoachDashboardWidgetInternal::AddButton( + WidgetTree, + QueueNavRow, + TEXT("NextQueueButton"), + TEXT("NextQueueButtonLabel"), + TEXT("Next Queue"), + NextQueueLabelRaw + ); + NextQueueButtonLabel = NextQueueLabelRaw; + } + if (PrimaryActionButton != nullptr) { PrimaryActionButton->OnClicked.AddDynamic(this, &UHyperTwistCoachDashboardWidget::HandlePrimaryActionClicked); @@ -267,6 +445,72 @@ void UHyperTwistCoachDashboardWidget::EnsureDefaultDashboardBuilt() { PromoteButton->OnClicked.AddDynamic(this, &UHyperTwistCoachDashboardWidget::HandlePromoteClicked); } + if (PreviousQueueButton != nullptr) + { + PreviousQueueButton->OnClicked.AddDynamic(this, &UHyperTwistCoachDashboardWidget::HandlePreviousQueueClicked); + } + if (StartSelectedButton != nullptr) + { + StartSelectedButton->OnClicked.AddDynamic(this, &UHyperTwistCoachDashboardWidget::HandleStartSelectedClicked); + } + if (NextQueueButton != nullptr) + { + NextQueueButton->OnClicked.AddDynamic(this, &UHyperTwistCoachDashboardWidget::HandleNextQueueClicked); + } +} + +void UHyperTwistCoachDashboardWidget::SyncSelectedQueueEntry() +{ + if (CachedCoachSessionQueueState.Entries.Num() <= 0) + { + SelectedQueueEntryIndex = INDEX_NONE; + return; + } + + if (SelectedQueueEntryIndex >= 0 + && SelectedQueueEntryIndex < CachedCoachSessionQueueState.Entries.Num()) + { + return; + } + + SelectedQueueEntryIndex = INDEX_NONE; + + if (bPreferActiveQueueSelectionOnRefresh && !CachedCoachPanelState.ActiveQueueEntryId.IsEmpty()) + { + SelectedQueueEntryIndex = CachedCoachSessionQueueState.Entries.IndexOfByPredicate( + [this](const FHyperTwistTrainingCoachSessionQueueStateEntry& Candidate) + { + return Candidate.EntryId == CachedCoachPanelState.ActiveQueueEntryId; + } + ); + } + + if (SelectedQueueEntryIndex == INDEX_NONE) + { + SelectedQueueEntryIndex = CachedCoachSessionQueueState.Entries.IndexOfByPredicate( + [](const FHyperTwistTrainingCoachSessionQueueStateEntry& Candidate) + { + return Candidate.EntryState == EHyperTwistTrainingCoachSessionQueueEntryState::Ready + || (Candidate.EntryState == EHyperTwistTrainingCoachSessionQueueEntryState::Pending + && Candidate.bCanStartImmediately); + } + ); + } + + if (SelectedQueueEntryIndex == INDEX_NONE) + { + SelectedQueueEntryIndex = CachedCoachSessionQueueState.Entries.IndexOfByPredicate( + [](const FHyperTwistTrainingCoachSessionQueueStateEntry& Candidate) + { + return Candidate.EntryState == EHyperTwistTrainingCoachSessionQueueEntryState::Deferred; + } + ); + } + + if (SelectedQueueEntryIndex == INDEX_NONE) + { + SelectedQueueEntryIndex = 0; + } } void UHyperTwistCoachDashboardWidget::UpdateDashboardPresentation() @@ -288,17 +532,96 @@ void UHyperTwistCoachDashboardWidget::UpdateDashboardPresentation() CachedCoachPanelState.FocusCaseCount ))); } + if (FollowUpTextBlock != nullptr) + { + const FString FollowUpLine = + (!CachedCoachFollowUpBrief.UserId.IsEmpty() && !CachedCoachFollowUpBrief.Headline.IsEmpty()) + ? FString::Printf( + TEXT("Follow-up: %s | Action: %s | Cases: %d"), + *CachedCoachFollowUpBrief.Headline, + CachedCoachFollowUpBrief.PrimaryActionLabel.IsEmpty() + ? TEXT("n/a") + : *CachedCoachFollowUpBrief.PrimaryActionLabel, + CachedCoachFollowUpBrief.FocusCaseIds.Num() + ) + : TEXT("Follow-up: none queued beyond the current coach chain"); + FollowUpTextBlock->SetText(FText::FromString(FollowUpLine)); + } if (QueueStatusTextBlock != nullptr) { QueueStatusTextBlock->SetText(FText::FromString(CachedCoachPanelState.QueueStatusLine)); } + if (QueueSelectionTextBlock != nullptr) + { + if (const FHyperTwistTrainingCoachSessionQueueStateEntry* SelectedEntry = FindSelectedQueueEntry()) + { + QueueSelectionTextBlock->SetText(FText::FromString(FString::Printf( + TEXT("Selected queue item %d/%d | Source: %s | State: %s | Priority: %s (%.1f)"), + SelectedQueueEntryIndex + 1, + CachedCoachSessionQueueState.Entries.Num(), + SelectedEntry->SourceLabel.IsEmpty() ? TEXT("n/a") : *SelectedEntry->SourceLabel, + *HyperTwistCoachDashboardWidgetInternal::EnumDisplayName(SelectedEntry->EntryState), + SelectedEntry->PriorityLabel.IsEmpty() ? TEXT("n/a") : *SelectedEntry->PriorityLabel, + SelectedEntry->PriorityScore + ))); + } + else + { + QueueSelectionTextBlock->SetText(FText::FromString(TEXT("Selected queue item: none"))); + } + } + if (QueueEntryDetailTextBlock != nullptr) + { + if (const FHyperTwistTrainingCoachSessionQueueStateEntry* SelectedEntry = FindSelectedQueueEntry()) + { + const FString DeferredLabel = SelectedEntry->DeferredUntilUtc.IsEmpty() + ? TEXT("n/a") + : SelectedEntry->DeferredUntilUtc; + const FString DeferralReason = SelectedEntry->DeferralReasonLabel.IsEmpty() + ? TEXT("n/a") + : SelectedEntry->DeferralReasonLabel; + QueueEntryDetailTextBlock->SetText(FText::FromString(FString::Printf( + TEXT("Headline: %s | Deck: %s | Method: %s | Cases: %d | Mode: %s | Policy: %s | Deferred until: %s | Reason: %s | Follow-up: %s | Archive tune: %s | Pinned: %s"), + *SelectedEntry->Headline, + SelectedEntry->FocusDeckId.IsEmpty() ? TEXT("n/a") : *SelectedEntry->FocusDeckId, + SelectedEntry->FocusMethodSegmentId.IsEmpty() ? TEXT("n/a") : *SelectedEntry->FocusMethodSegmentId, + SelectedEntry->FocusCaseIds.Num(), + *HyperTwistCoachDashboardWidgetInternal::EnumDisplayName(SelectedEntry->SuggestedMode), + *HyperTwistCoachDashboardWidgetInternal::EnumDisplayName(SelectedEntry->SuggestedSelectionPolicy), + *DeferredLabel, + *DeferralReason, + SelectedEntry->bRequiresFollowUp ? TEXT("yes") : TEXT("no"), + SelectedEntry->bRequiresArchiveRetentionTuning ? TEXT("yes") : TEXT("no"), + SelectedEntry->bPinned ? TEXT("yes") : TEXT("no") + ))); + } + else + { + QueueEntryDetailTextBlock->SetText(FText::FromString(TEXT("Queue entry detail: no queue entry selected"))); + } + } if (QueueCountsTextBlock != nullptr) { QueueCountsTextBlock->SetText(FText::FromString(FString::Printf( - TEXT("Signals: %d | Follow-up queue entries: %d | Runnable: %s"), + TEXT("Signals: %d | Ready: %d | Deferred: %d | In-progress: %d | Completed: %d"), CachedCoachPanelState.SignalCount, - CachedCoachPanelState.FollowUpQueueEntryCount, - CachedCoachPanelState.bHasRunnableQueueEntry ? TEXT("yes") : TEXT("no") + CachedCoachSessionQueueState.ReadyEntryCount, + CachedCoachSessionQueueState.DeferredEntryCount, + CachedCoachSessionQueueState.InProgressEntryCount, + CachedCoachSessionQueueState.CompletedEntryCount + ))); + } + if (QueueExecutionTextBlock != nullptr) + { + QueueExecutionTextBlock->SetText(FText::FromString(FString::Printf( + TEXT("Execution: queued %d | started %d | completed %d | deferred %d | manual reschedules %d | outstanding deferred: %s | completion gap: %s"), + CachedCoachQueueExecutionSummary.QueuedEventCount, + CachedCoachQueueExecutionSummary.StartedEventCount, + CachedCoachQueueExecutionSummary.CompletedEventCount, + CachedCoachQueueExecutionSummary.DeferredEventCount, + CachedCoachQueueExecutionSummary.ManualRescheduleEventCount, + CachedCoachQueueExecutionSummary.bHasOutstandingDeferredWork ? TEXT("yes") : TEXT("no"), + CachedCoachQueueExecutionSummary.bHasCompletionGap ? TEXT("yes") : TEXT("no") ))); } if (VerificationStatusTextBlock != nullptr) @@ -334,7 +657,7 @@ void UHyperTwistCoachDashboardWidget::UpdateDashboardPresentation() } if (DeferButton != nullptr) { - DeferButton->SetIsEnabled(CachedCoachPanelState.bHasRunnableQueueEntry); + DeferButton->SetIsEnabled(FindSelectedQueueEntry() != nullptr); } if (PromoteButtonLabel != nullptr) { @@ -344,6 +667,35 @@ void UHyperTwistCoachDashboardWidget::UpdateDashboardPresentation() { PromoteButton->SetIsEnabled(!FindFirstDeferredQueueEntryId().IsEmpty()); } + if (PreviousQueueButtonLabel != nullptr) + { + PreviousQueueButtonLabel->SetText(FText::FromString(TEXT("Prev Queue"))); + } + if (PreviousQueueButton != nullptr) + { + PreviousQueueButton->SetIsEnabled(CachedCoachSessionQueueState.Entries.Num() > 1); + } + if (StartSelectedButtonLabel != nullptr) + { + StartSelectedButtonLabel->SetText(FText::FromString(TEXT("Start Selected"))); + } + if (StartSelectedButton != nullptr) + { + const FHyperTwistTrainingCoachSessionQueueStateEntry* SelectedEntry = FindSelectedQueueEntry(); + const bool bSelectedRunnable = SelectedEntry != nullptr + && (SelectedEntry->EntryState == EHyperTwistTrainingCoachSessionQueueEntryState::Ready + || (SelectedEntry->EntryState == EHyperTwistTrainingCoachSessionQueueEntryState::Pending + && SelectedEntry->bCanStartImmediately)); + StartSelectedButton->SetIsEnabled(bSelectedRunnable); + } + if (NextQueueButtonLabel != nullptr) + { + NextQueueButtonLabel->SetText(FText::FromString(TEXT("Next Queue"))); + } + if (NextQueueButton != nullptr) + { + NextQueueButton->SetIsEnabled(CachedCoachSessionQueueState.Entries.Num() > 1); + } } FString UHyperTwistCoachDashboardWidget::BuildDefaultDeferredUntilUtc() const @@ -365,3 +717,13 @@ FString UHyperTwistCoachDashboardWidget::FindFirstDeferredQueueEntryId() const return FString(); } + +const FHyperTwistTrainingCoachSessionQueueStateEntry* UHyperTwistCoachDashboardWidget::FindSelectedQueueEntry() const +{ + if (SelectedQueueEntryIndex < 0 || SelectedQueueEntryIndex >= CachedCoachSessionQueueState.Entries.Num()) + { + return nullptr; + } + + return &CachedCoachSessionQueueState.Entries[SelectedQueueEntryIndex]; +} diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingPanelWidget.cpp b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingPanelWidget.cpp index b2a175e..1c45936 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingPanelWidget.cpp +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingPanelWidget.cpp @@ -149,6 +149,21 @@ FHyperTwistTrainingRunState UHyperTwistTrainingPanelWidget::StartNextQueuedCoach return CachedRunState; } +FHyperTwistTrainingRunState UHyperTwistTrainingPanelWidget::StartCoachQueueEntry( + const FString& EntryId, + const int32 MaxCases +) +{ + CachedRunState = UHyperTwistTrainingRuntimeLibrary::StartCoachQueueEntry( + this, + EntryId, + DefaultSessionId + TEXT("_queue_") + EntryId, + MaxCases > 0 ? MaxCases : DefaultCoachMaxCases + ); + RefreshTrainingState(); + return CachedRunState; +} + bool UHyperTwistTrainingPanelWidget::DeferCoachQueueEntry( const FString& EntryId, const FString& DeferredUntilUtc, diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingRepositoryLibrary.cpp b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingRepositoryLibrary.cpp index f69f595..93cd9c9 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingRepositoryLibrary.cpp +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingRepositoryLibrary.cpp @@ -6236,6 +6236,60 @@ FHyperTwistTrainingCoachSessionQueueState UHyperTwistTrainingRepositoryLibrary:: return HyperTwistTrainingRepositoryLibraryInternal::NormalizeCoachSessionQueueState(UpdatedQueueState); } +FHyperTwistTrainingCoachSessionQueueState UHyperTwistTrainingRepositoryLibrary::StartCoachSessionQueueEntryById( + const FHyperTwistTrainingCoachSessionQueueState& QueueState, + const FString& EntryId, + const FString& TrainingSessionId, + const FString& StartedAtUtc +) +{ + FHyperTwistTrainingCoachSessionQueueState UpdatedQueueState = + HyperTwistTrainingRepositoryLibraryInternal::NormalizeCoachSessionQueueState(QueueState); + if (!UpdatedQueueState.IsStructurallyValid() || EntryId.IsEmpty()) + { + return UpdatedQueueState; + } + + const FString ResolvedStartedAtUtc = + HyperTwistTrainingRepositoryLibraryInternal::ResolveReferenceUtc(StartedAtUtc); + const int32 ActivatedIndex = UpdatedQueueState.Entries.IndexOfByPredicate( + [&EntryId](const FHyperTwistTrainingCoachSessionQueueStateEntry& Candidate) + { + return Candidate.EntryId == EntryId + && Candidate.EntryState != EHyperTwistTrainingCoachSessionQueueEntryState::Deferred + && !HyperTwistTrainingRepositoryLibraryInternal::IsTerminalCoachQueueEntryState(Candidate.EntryState); + } + ); + 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.DeferredUntilUtc.Reset(); + ActiveEntry.StartedAtUtc = ResolvedStartedAtUtc; + ActiveEntry.bCanStartImmediately = false; + UpdatedQueueState.LastUpdatedAtUtc = ResolvedStartedAtUtc; + return HyperTwistTrainingRepositoryLibraryInternal::NormalizeCoachSessionQueueState(UpdatedQueueState); +} + FHyperTwistTrainingCoachSessionQueueState UHyperTwistTrainingRepositoryLibrary::FinalizeCoachSessionQueueEntry( const FHyperTwistTrainingCoachSessionQueueState& QueueState, const FString& TrainingSessionId, diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingRuntimeLibrary.cpp b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingRuntimeLibrary.cpp index 928a4b9..ef34513 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingRuntimeLibrary.cpp +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingRuntimeLibrary.cpp @@ -1043,6 +1043,21 @@ FHyperTwistTrainingRunState UHyperTwistTrainingRuntimeLibrary::StartNextQueuedCo return FHyperTwistTrainingRunState(); } +FHyperTwistTrainingRunState UHyperTwistTrainingRuntimeLibrary::StartCoachQueueEntry( + UObject* WorldContextObject, + const FString& EntryId, + const FString& SessionId, + int32 MaxCases +) +{ + if (UHyperTwistTrainingSubsystem* TrainingSubsystem = GetTrainingSubsystem(WorldContextObject)) + { + return TrainingSubsystem->StartCoachQueueEntry(EntryId, SessionId, MaxCases); + } + + return FHyperTwistTrainingRunState(); +} + FHyperTwistTrainingRunState UHyperTwistTrainingRuntimeLibrary::StartSampleClassicTrainingRun( UObject* WorldContextObject, const FString& UserId, diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingSessionActor.cpp b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingSessionActor.cpp index 072d56f..e345356 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingSessionActor.cpp +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingSessionActor.cpp @@ -161,6 +161,21 @@ FHyperTwistTrainingRunState AHyperTwistTrainingSessionActor::StartNextQueuedCoac return CachedRunState; } +FHyperTwistTrainingRunState AHyperTwistTrainingSessionActor::StartCoachQueueEntryTrainingRun( + const FString& EntryId, + const int32 MaxCases +) +{ + CachedRunState = UHyperTwistTrainingRuntimeLibrary::StartCoachQueueEntry( + this, + EntryId, + AutoStartSessionId + TEXT("_queue_") + EntryId, + MaxCases > 0 ? MaxCases : DefaultCoachMaxCases + ); + RefreshCachedTrainingState(); + return CachedRunState; +} + bool AHyperTwistTrainingSessionActor::DeferCoachQueueEntry( const FString& EntryId, const FString& DeferredUntilUtc, diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingSubsystem.cpp b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingSubsystem.cpp index aded721..ea9d649 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingSubsystem.cpp +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingSubsystem.cpp @@ -75,6 +75,24 @@ namespace HyperTwistTrainingSubsystemInternal return BestEntry; } + const FHyperTwistTrainingCoachSessionQueueStateEntry* FindCoachQueueEntryById( + const FHyperTwistTrainingCoachSessionQueueState& QueueState, + const FString& EntryId + ) + { + if (!QueueState.IsStructurallyValid() || EntryId.IsEmpty()) + { + return nullptr; + } + + return QueueState.Entries.FindByPredicate( + [&EntryId](const FHyperTwistTrainingCoachSessionQueueStateEntry& Candidate) + { + return Candidate.EntryId == EntryId; + } + ); + } + FHyperTwistTrainingDeck BuildCoachQueueDeckFromEntry( const FHyperTwistTrainingCoachSessionQueueStateEntry& QueueEntry, const FHyperTwistTrainingRunState& ActiveRunState, @@ -1469,6 +1487,123 @@ FHyperTwistTrainingRunState UHyperTwistTrainingSubsystem::StartNextQueuedCoachRu return RunState; } +FHyperTwistTrainingRunState UHyperTwistTrainingSubsystem::StartCoachQueueEntry( + const FString& EntryId, + const FString& SessionId, + int32 MaxCases +) +{ + const FHyperTwistTrainingCoachSessionQueueStateEntry* SelectedEntry = + HyperTwistTrainingSubsystemInternal::FindCoachQueueEntryById(ActiveCoachSessionQueueState, EntryId); + if (SelectedEntry == nullptr) + { + return FHyperTwistTrainingRunState(); + } + + const bool bIsRunnable = SelectedEntry->EntryState == EHyperTwistTrainingCoachSessionQueueEntryState::Ready + || (SelectedEntry->EntryState == EHyperTwistTrainingCoachSessionQueueEntryState::Pending + && SelectedEntry->bCanStartImmediately); + if (!bIsRunnable) + { + return FHyperTwistTrainingRunState(); + } + + FHyperTwistTrainingDeck QueueDeck; + FHyperTwistCoachBrief QueueBrief; + const bool bHasPrimaryCoachBrief = !ActiveCoachBrief.UserId.IsEmpty() && !ActiveCoachBrief.Headline.IsEmpty(); + const bool bHasFollowUpCoachBrief = + !ActiveCoachFollowUpBrief.UserId.IsEmpty() && !ActiveCoachFollowUpBrief.Headline.IsEmpty(); + FString UserId = !ActiveCoachSessionQueueState.UserId.IsEmpty() + ? ActiveCoachSessionQueueState.UserId + : (ActiveRunState.Session.UserId.IsEmpty() ? TEXT("local-user") : ActiveRunState.Session.UserId); + + if (SelectedEntry->SourceLabel == TEXT("primary-brief") && bHasPrimaryCoachBrief) + { + QueueDeck = BuildActiveCoachRecommendedDeck(MaxCases); + QueueBrief = ActiveCoachBrief; + UserId = ActiveCoachBrief.UserId.IsEmpty() ? UserId : ActiveCoachBrief.UserId; + } + else if (SelectedEntry->SourceLabel == TEXT("follow-up-brief") && bHasFollowUpCoachBrief) + { + QueueDeck = BuildActiveCoachFollowUpDeck(MaxCases); + QueueBrief = ActiveCoachFollowUpBrief; + UserId = ActiveCoachFollowUpBrief.UserId.IsEmpty() ? UserId : ActiveCoachFollowUpBrief.UserId; + } + else + { + QueueDeck = HyperTwistTrainingSubsystemInternal::BuildCoachQueueDeckFromEntry( + *SelectedEntry, + ActiveRunState, + MaxCases + ); + QueueBrief = HyperTwistTrainingSubsystemInternal::BuildCoachBriefFromQueueEntry( + ActiveCoachSessionQueueState, + *SelectedEntry, + ActiveCoachSessionQueueState.ReferenceUtc + ); + } + + if (!QueueDeck.IsStructurallyValid()) + { + return FHyperTwistTrainingRunState(); + } + + const FHyperTwistTrainingCoachSessionQueueState QueueStateBeforeStart = ActiveCoachSessionQueueState; + const FHyperTwistTrainingCoachActionPlan DraftActionPlan = + (!QueueBrief.UserId.IsEmpty() && !QueueBrief.Headline.IsEmpty()) + ? UHyperTwistTrainingRepositoryLibrary::BuildCoachActionPlan( + QueueBrief, + FString::Printf(TEXT("coach_queue_%s_%s"), *SelectedEntry->EntryId, *SessionId), + FString() + ) + : FHyperTwistTrainingCoachActionPlan(); + FHyperTwistTrainingRunState RunState = StartTrainingRunFromDeck( + QueueDeck, + UserId, + SessionId, + SelectedEntry->SuggestedMode + ); + if (!RunState.IsStructurallyValid()) + { + return RunState; + } + + if (DraftActionPlan.IsStructurallyValid()) + { + ActiveCoachActionPlan = UHyperTwistTrainingRepositoryLibrary::ActivateCoachActionPlan( + DraftActionPlan, + RunState.Session.TrainingSessionId, + RunState.Session.StartedAtUtc + ); + TrainingRepositoryState = UHyperTwistTrainingRepositoryLibrary::UpsertCoachActionPlan( + TrainingRepositoryState, + ActiveCoachActionPlan + ); + } + + if (QueueStateBeforeStart.IsStructurallyValid()) + { + const FHyperTwistTrainingCoachSessionQueueState UpdatedQueueState = + UHyperTwistTrainingRepositoryLibrary::StartCoachSessionQueueEntryById( + QueueStateBeforeStart, + SelectedEntry->EntryId, + RunState.Session.TrainingSessionId, + RunState.Session.StartedAtUtc + ); + if (UpdatedQueueState.IsStructurallyValid()) + { + TrainingRepositoryState = UHyperTwistTrainingRepositoryLibrary::UpsertCoachSessionQueueState( + TrainingRepositoryState, + UpdatedQueueState + ); + ActiveCoachSessionQueueState = UpdatedQueueState; + } + } + + RefreshRepositoryViews(); + return RunState; +} + FHyperTwistTrainingRunState UHyperTwistTrainingSubsystem::StartTrainingRunFromDeck( const FHyperTwistTrainingDeck& Deck, const FString& UserId, diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistCoachDashboardWidget.h b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistCoachDashboardWidget.h index 49d6462..98a672b 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistCoachDashboardWidget.h +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistCoachDashboardWidget.h @@ -19,6 +19,12 @@ public: UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Training|Coach|Dashboard") int32 DefaultDeferredHours = 24; + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Training|Coach|Dashboard") + bool bPreferActiveQueueSelectionOnRefresh = true; + + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "HyperTwist|Training|Coach|Dashboard") + int32 SelectedQueueEntryIndex = INDEX_NONE; + UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Coach|Dashboard") void RefreshCoachDashboardView(); @@ -31,6 +37,15 @@ public: UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Coach|Dashboard") bool PromoteFirstDeferredQueueEntry(); + UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Coach|Dashboard") + bool SelectPreviousQueueEntry(); + + UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Coach|Dashboard") + bool SelectNextQueueEntry(); + + UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Coach|Dashboard") + FHyperTwistTrainingRunState StartSelectedQueueEntry(); + protected: UPROPERTY(Transient) TObjectPtr RootLayout = nullptr; @@ -53,6 +68,18 @@ protected: UPROPERTY(Transient) TObjectPtr QueueCountsTextBlock = nullptr; + UPROPERTY(Transient) + TObjectPtr FollowUpTextBlock = nullptr; + + UPROPERTY(Transient) + TObjectPtr QueueSelectionTextBlock = nullptr; + + UPROPERTY(Transient) + TObjectPtr QueueEntryDetailTextBlock = nullptr; + + UPROPERTY(Transient) + TObjectPtr QueueExecutionTextBlock = nullptr; + UPROPERTY(Transient) TObjectPtr PrimaryActionButton = nullptr; @@ -77,6 +104,24 @@ protected: UPROPERTY(Transient) TObjectPtr PromoteButtonLabel = nullptr; + UPROPERTY(Transient) + TObjectPtr PreviousQueueButton = nullptr; + + UPROPERTY(Transient) + TObjectPtr PreviousQueueButtonLabel = nullptr; + + UPROPERTY(Transient) + TObjectPtr StartSelectedButton = nullptr; + + UPROPERTY(Transient) + TObjectPtr StartSelectedButtonLabel = nullptr; + + UPROPERTY(Transient) + TObjectPtr NextQueueButton = nullptr; + + UPROPERTY(Transient) + TObjectPtr NextQueueButtonLabel = nullptr; + UFUNCTION() void HandlePrimaryActionClicked(); @@ -89,9 +134,20 @@ protected: UFUNCTION() void HandlePromoteClicked(); + UFUNCTION() + void HandlePreviousQueueClicked(); + + UFUNCTION() + void HandleStartSelectedClicked(); + + UFUNCTION() + void HandleNextQueueClicked(); + private: void EnsureDefaultDashboardBuilt(); + void SyncSelectedQueueEntry(); void UpdateDashboardPresentation(); FString BuildDefaultDeferredUntilUtc() const; FString FindFirstDeferredQueueEntryId() const; + const FHyperTwistTrainingCoachSessionQueueStateEntry* FindSelectedQueueEntry() const; }; diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingPanelWidget.h b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingPanelWidget.h index 9db96dd..d146496 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingPanelWidget.h +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingPanelWidget.h @@ -118,6 +118,12 @@ public: UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Coach") FHyperTwistTrainingRunState StartNextQueuedCoachRun(int32 MaxCases); + UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Coach") + FHyperTwistTrainingRunState StartCoachQueueEntry( + const FString& EntryId, + int32 MaxCases + ); + UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Coach") bool DeferCoachQueueEntry( const FString& EntryId, diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingRepositoryLibrary.h b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingRepositoryLibrary.h index dc497f1..e47ac6c 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingRepositoryLibrary.h +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingRepositoryLibrary.h @@ -369,6 +369,14 @@ public: const FString& StartedAtUtc ); + UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Repository|Coach") + static FHyperTwistTrainingCoachSessionQueueState StartCoachSessionQueueEntryById( + const FHyperTwistTrainingCoachSessionQueueState& QueueState, + const FString& EntryId, + const FString& TrainingSessionId, + const FString& StartedAtUtc + ); + UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Repository|Coach") static FHyperTwistTrainingCoachSessionQueueState FinalizeCoachSessionQueueEntry( const FHyperTwistTrainingCoachSessionQueueState& QueueState, diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingRuntimeLibrary.h b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingRuntimeLibrary.h index 1486e62..0db8837 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingRuntimeLibrary.h +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingRuntimeLibrary.h @@ -424,6 +424,14 @@ public: int32 MaxCases ); + UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Coach", meta = (WorldContext = "WorldContextObject")) + static FHyperTwistTrainingRunState StartCoachQueueEntry( + UObject* WorldContextObject, + const FString& EntryId, + const FString& SessionId, + int32 MaxCases + ); + UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training", meta = (WorldContext = "WorldContextObject")) static FHyperTwistTrainingRunState StartSampleClassicTrainingRun( UObject* WorldContextObject, diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingSessionActor.h b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingSessionActor.h index 63ca269..73469a9 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingSessionActor.h +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingSessionActor.h @@ -120,6 +120,12 @@ public: UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Coach") FHyperTwistTrainingRunState StartNextQueuedCoachTrainingRun(int32 MaxCases); + UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Coach") + FHyperTwistTrainingRunState StartCoachQueueEntryTrainingRun( + const FString& EntryId, + int32 MaxCases + ); + UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Coach") bool DeferCoachQueueEntry( const FString& EntryId, diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingSubsystem.h b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingSubsystem.h index 6d19b46..7492ee2 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingSubsystem.h +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingSubsystem.h @@ -342,6 +342,13 @@ public: int32 MaxCases ); + UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Coach") + FHyperTwistTrainingRunState StartCoachQueueEntry( + const FString& EntryId, + const FString& SessionId, + int32 MaxCases + ); + UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training") FHyperTwistTrainingRunState StartTrainingRunFromDeck( const FHyperTwistTrainingDeck& Deck,