From 0839b8e13d01ff30f0c52d74b60a7ffb3153ac36 Mon Sep 17 00:00:00 2001 From: Nick Beckley Date: Sun, 30 Aug 2026 22:29:19 +0000 Subject: [PATCH] Route completed review modules to canonical pages --- PlotLine.Tests/Program.cs | 53 ++++++- .../StoryIntelligencePersistenceModels.cs | 1 + .../OnboardingStoryIntelligenceService.cs | 1 + ...178_Phase27E_ReviewCentreMetricSummary.sql | 134 ++++++++++++++++++ PlotLine/ViewModels/OnboardingViewModels.cs | 1 + .../StoryIntelligenceReviewCentre.cshtml | 75 ++++++---- 6 files changed, 238 insertions(+), 27 deletions(-) create mode 100644 PlotLine/Sql/178_Phase27E_ReviewCentreMetricSummary.sql diff --git a/PlotLine.Tests/Program.cs b/PlotLine.Tests/Program.cs index 33d66de..077d635 100644 --- a/PlotLine.Tests/Program.cs +++ b/PlotLine.Tests/Program.cs @@ -282,6 +282,7 @@ var tests = new (string Name, Action Test)[] ("Story Intelligence Review Centre uses bounded summary query", StoryIntelligenceReviewCentreUsesBoundedSummaryQuery), ("Story Intelligence detailed review GETs use selected module paths", StoryIntelligenceDetailedReviewGetsUseSelectedModulePaths), ("Story Intelligence completed Character review remains inspectable", StoryIntelligenceCompletedCharacterReviewRemainsInspectable), + ("Story Intelligence Review Centre completed modules use canonical destinations", StoryIntelligenceReviewCentreCompletedModulesUseCanonicalDestinations), ("Story Intelligence detailed reviews read persisted candidate pages", StoryIntelligenceDetailedReviewsReadPersistedCandidatePages), ("Story Intelligence Job 6 uses bounded concurrent chapter execution", StoryIntelligenceJob6UsesBoundedConcurrentChapterExecution), ("Story Intelligence global progress uses whole run scene totals", StoryIntelligenceGlobalProgressUsesWholeRunSceneTotals), @@ -728,7 +729,8 @@ static void StoryIntelligenceReviewCentreCountsOnlyNeedsAttentionCategories() Assert(viewModel.Contains("CharacterNeedsAttentionCount => CharacterReview.SummaryNeedsAttentionCount ?? CharacterReview.Candidates.Count", StringComparison.Ordinal), "Character count should prefer lightweight summary counts."); Assert(viewModel.Contains("LocationNeedsAttentionCount => LocationReview.SummaryNeedsAttentionCount ?? LocationReview.Candidates.Count", StringComparison.Ordinal), "Location count should prefer lightweight summary counts."); Assert(viewModel.Contains("NarrativeNeedsAttentionCount => NarrativeReview.SummaryNeedsAttentionCount ?? NarrativeReview.Candidates.Count", StringComparison.Ordinal), "Narrative count should prefer lightweight summary counts."); - Assert(centre.Contains("Scene Metrics", StringComparison.Ordinal) && centre.Contains("TotalAnalysedScenes", StringComparison.Ordinal), "Metrics should report analysed scene count."); + Assert(centre.Contains("Scene Metrics", StringComparison.Ordinal) && centre.Contains("MetricValueCount", StringComparison.Ordinal), "Metrics should report actual metric availability."); + Assert(!centre.Contains("Review Metrics", StringComparison.Ordinal), "Metrics should not present a fake review workflow."); Assert(!summary.Contains("Locations identified", StringComparison.Ordinal), "Summary should not present total identified rows as review workload."); } @@ -757,7 +759,7 @@ static void StoryIntelligenceReviewCentreUsesIndependentTileGrid() Assert(centre.Contains("can be reviewed in any order", StringComparison.Ordinal), "Header should communicate independent review order."); Assert(centre.Contains("Locked until Characters are resolved", StringComparison.Ordinal), "Locked dependent tiles should be visible and clearly locked."); Assert(centre.Contains(">Locked", StringComparison.Ordinal), "Locked tile action should render as non-clickable text."); - Assert(centre.Contains("Available", StringComparison.Ordinal) && centre.Contains("View Metrics", StringComparison.Ordinal), "Scene Metrics should be available, not a mandatory review workload."); + Assert(centre.Contains("No metrics available", StringComparison.Ordinal) && centre.Contains("View on Timeline", StringComparison.Ordinal), "Scene Metrics should distinguish unavailable metrics from Timeline navigation."); Assert(!centre.Contains("Next", StringComparison.Ordinal) && !centre.Contains("Previous", StringComparison.Ordinal), "Review Centre should not contain wizard navigation."); Assert(!centre.Contains("Core Import", StringComparison.Ordinal) && !centre.Contains("Analyse Manuscript", StringComparison.Ordinal), "Review Centre should not show phase panels."); @@ -1174,6 +1176,53 @@ static void StoryIntelligenceCompletedCharacterReviewRemainsInspectable() Assert(view.Contains("AliasTargetOptions", StringComparison.Ordinal), "Completed Character page should use canonical Character data when no pending candidates exist."); } +static void StoryIntelligenceReviewCentreCompletedModulesUseCanonicalDestinations() +{ + var root = PlotLineRoot(); + var centre = File.ReadAllText(Path.Combine(root, "Views/Onboarding/StoryIntelligenceReviewCentre.cshtml")); + var model = File.ReadAllText(Path.Combine(root, "ViewModels/OnboardingViewModels.cs")); + var persistence = File.ReadAllText(Path.Combine(root, "Models/StoryIntelligencePersistenceModels.cs")); + var service = File.ReadAllText(Path.Combine(root, "Services/OnboardingStoryIntelligenceService.cs")); + var metricSql = File.ReadAllText(Path.Combine(root, "Sql/178_Phase27E_ReviewCentreMetricSummary.sql")); + + Assert(centre.Contains("\"Review Characters\"", StringComparison.Ordinal), "Incomplete Characters should use the Story Intelligence review action label."); + Assert(centre.Contains("\"StoryIntelligenceCharacters\"", StringComparison.Ordinal), "Incomplete Characters should route to Character review."); + Assert(centre.Contains("\"View Characters\"", StringComparison.Ordinal), "Complete Characters should use canonical action label."); + Assert(centre.Contains("\"Characters\"", StringComparison.Ordinal) && centre.Contains("new { projectId = Model.ProjectID }", StringComparison.Ordinal), "Complete Characters should route to Characters/Index with project context."); + + Assert(centre.Contains("\"Review Locations\"", StringComparison.Ordinal) && centre.Contains("\"StoryIntelligenceLocations\"", StringComparison.Ordinal), "Incomplete Locations should route to Location review."); + Assert(centre.Contains("\"View Locations\"", StringComparison.Ordinal) && centre.Contains("\"Locations\"", StringComparison.Ordinal), "Complete Locations should route to Locations/Index."); + + Assert(centre.Contains("\"Review Assets\"", StringComparison.Ordinal) && centre.Contains("\"StoryIntelligenceAssets\"", StringComparison.Ordinal), "Incomplete Assets should route to Asset review."); + Assert(centre.Contains("\"View Assets\"", StringComparison.Ordinal) && centre.Contains("\"StoryAssets\"", StringComparison.Ordinal), "Complete Assets should route to StoryAssets/Index."); + + Assert(centre.Contains("\"Review Relationships\"", StringComparison.Ordinal) && centre.Contains("\"StoryIntelligenceRelationships\"", StringComparison.Ordinal), "Incomplete Relationships should route to Relationship review."); + Assert(centre.Contains("\"View Relationships\"", StringComparison.Ordinal) && centre.Contains("\"RelationshipMap\"", StringComparison.Ordinal), "Complete Relationships should route to RelationshipMap/Index."); + Assert(centre.Contains("new { projectId = Model.ProjectID, bookId = Model.BookID }", StringComparison.Ordinal), "Book-scoped canonical destinations should include book context."); + + Assert(centre.Contains("\"Review Knowledge\"", StringComparison.Ordinal) && centre.Contains("\"StoryIntelligenceKnowledge\"", StringComparison.Ordinal), "Incomplete Knowledge should route to Knowledge review."); + Assert(centre.Contains("\"View Knowledge\"", StringComparison.Ordinal) && centre.Contains("\"StoryBible\"", StringComparison.Ordinal), "Complete Knowledge should route to StoryBible/Index."); + + Assert(centre.Contains("Review Plot Lines & Threads", StringComparison.Ordinal), "Incomplete Plot should route to Plot review."); + Assert(centre.Contains("Url.Action(\"StoryIntelligenceNarrative\", \"Onboarding\"", StringComparison.Ordinal), "Incomplete Plot should use the Story Intelligence Plot review route."); + Assert(centre.Contains("Url.Action(\"Index\", \"Timeline\", new { projectId = Model.ProjectID, bookId = Model.BookID })", StringComparison.Ordinal), "Complete Plot and available Metrics should route to Timeline."); + Assert(centre.Contains("View on Timeline", StringComparison.Ordinal), "Complete Plot should label the canonical Timeline action."); + + Assert(centre.Contains("Model.MetricValueCount > 0", StringComparison.Ordinal), "Metrics availability should be based on actual metric values."); + Assert(centre.Contains("No metrics available", StringComparison.Ordinal), "Zero metrics should be clearly identified."); + Assert(centre.Contains("No action", StringComparison.Ordinal), "Zero metrics should not expose a misleading Review Metrics action."); + Assert(!centre.Contains("Review Metrics", StringComparison.Ordinal), "Review Centre should not advertise a non-existent metrics review workflow."); + + Assert(model.Contains("public int MetricValueCount", StringComparison.Ordinal), "Review Centre model should expose metric availability."); + Assert(persistence.Contains("public int MetricValueCount", StringComparison.Ordinal), "Persisted summary should expose metric availability."); + Assert(service.Contains("MetricValueCount = summary.MetricValueCount", StringComparison.Ordinal), "Review Centre summary should hydrate metric availability."); + Assert(metricSql.Contains("MetricValueSummary", StringComparison.Ordinal) && metricSql.Contains("SceneMetricValues", StringComparison.Ordinal), "Metric summary SQL should count existing metric values."); + + Assert(centre.Contains("var href = complete", StringComparison.Ordinal), "Module helper should switch destination by completion state."); + Assert(centre.Contains("Url.Action(reviewAction, \"Onboarding\", new { batchId = Model.BatchID })", StringComparison.Ordinal), "Incomplete modules should still open Story Intelligence review."); + Assert(centre.Contains("Url.Action(\"Index\", completeController, completeRouteValues)", StringComparison.Ordinal), "Complete modules should open canonical Index routes."); +} + static void StoryIntelligenceDetailedReviewsReadPersistedCandidatePages() { var root = PlotLineRoot(); diff --git a/PlotLine/Models/StoryIntelligencePersistenceModels.cs b/PlotLine/Models/StoryIntelligencePersistenceModels.cs index 98cefa8..46f14e4 100644 --- a/PlotLine/Models/StoryIntelligencePersistenceModels.cs +++ b/PlotLine/Models/StoryIntelligencePersistenceModels.cs @@ -179,6 +179,7 @@ public sealed class StoryIntelligenceReviewCentreSummary public int NarrativePendingCount { get; init; } public int NarrativeThreadCount { get; init; } public int NarrativeEventCount { get; init; } + public int MetricValueCount { get; init; } } public sealed class StoryIntelligenceCommittedRunSummary diff --git a/PlotLine/Services/OnboardingStoryIntelligenceService.cs b/PlotLine/Services/OnboardingStoryIntelligenceService.cs index 92acd68..79f1817 100644 --- a/PlotLine/Services/OnboardingStoryIntelligenceService.cs +++ b/PlotLine/Services/OnboardingStoryIntelligenceService.cs @@ -846,6 +846,7 @@ public sealed class OnboardingStoryIntelligenceService( BookID = summary.BookID, ProjectName = summary.ProjectName, BookTitle = summary.BookDisplayTitle, + MetricValueCount = summary.MetricValueCount, CharacterReview = new StoryIntelligenceCharacterReviewViewModel { HasCommittedScenes = hasCommittedScenes, diff --git a/PlotLine/Sql/178_Phase27E_ReviewCentreMetricSummary.sql b/PlotLine/Sql/178_Phase27E_ReviewCentreMetricSummary.sql new file mode 100644 index 0000000..c9619bc --- /dev/null +++ b/PlotLine/Sql/178_Phase27E_ReviewCentreMetricSummary.sql @@ -0,0 +1,134 @@ +SET ANSI_NULLS ON; +GO +SET QUOTED_IDENTIFIER ON; +GO + +CREATE OR ALTER PROCEDURE dbo.StoryIntelligenceReviewCentre_GetSummary + @BookID int, + @UserID int +AS +BEGIN + SET NOCOUNT ON; + + ;WITH RunSummary AS + ( + SELECT + r.BookID, + COUNT_BIG(*) AS RunCount, + SUM(CASE WHEN r.Status IN (N'Completed', N'CompletedWithWarnings') THEN 1 ELSE 0 END) AS CompletedRunCount, + SUM(CASE WHEN r.Status IN (N'Pending', N'Running') THEN 1 ELSE 0 END) AS ActiveRunCount, + SUM(CASE WHEN r.Status IN (N'Failed', N'Cancelled') THEN 1 ELSE 0 END) AS FailedRunCount, + SUM(COALESCE(r.TotalDetectedScenes, 0)) AS TotalDetectedScenes, + SUM(COALESCE(r.CompletedScenes, 0)) AS CompletedScenes, + SUM(COALESCE(r.FailedScenes, 0)) AS FailedScenes + FROM dbo.StoryIntelligenceRuns r + WHERE r.BookID = @BookID + GROUP BY r.BookID + ), + NarrativeSummary AS + ( + SELECT + nc.BookID, + COUNT_BIG(*) AS NarrativeCandidateCount, + SUM(CASE WHEN nc.Status = N'Pending' THEN 1 ELSE 0 END) AS NarrativePendingCount, + SUM(CASE WHEN nc.CandidateType = N'Thread' THEN 1 ELSE 0 END) AS NarrativeThreadCount + FROM dbo.StoryIntelligenceNarrativeCandidates nc + WHERE nc.BookID = @BookID + AND nc.WholeBookPlotSynthesisRunID IS NOT NULL + GROUP BY nc.BookID + ), + NarrativeEventSummary AS + ( + SELECT + nc.BookID, + COUNT_BIG(*) AS NarrativeEventCount + FROM dbo.StoryIntelligenceNarrativeCandidateEvents ev + INNER JOIN dbo.StoryIntelligenceNarrativeCandidates nc + ON nc.NarrativeCandidateID = ev.NarrativeCandidateID + WHERE nc.BookID = @BookID + AND nc.WholeBookPlotSynthesisRunID IS NOT NULL + GROUP BY nc.BookID + ), + MetricValueSummary AS + ( + SELECT + chapter.BookID, + COUNT_BIG(*) AS MetricValueCount + FROM dbo.SceneMetricValues metric + INNER JOIN dbo.Scenes scene + ON scene.SceneID = metric.SceneID + INNER JOIN dbo.Chapters chapter + ON chapter.ChapterID = scene.ChapterID + WHERE chapter.BookID = @BookID + GROUP BY chapter.BookID + ), + CurrentPlotRun AS + ( + SELECT TOP (1) + run.BookID, + run.WholeBookPlotSynthesisRunID, + run.Status AS WholeBookPlotSynthesisStatus, + run.ErrorMessage AS WholeBookPlotSynthesisErrorMessage + FROM dbo.WholeBookPlotSynthesisRuns run + WHERE run.BookID = @BookID + AND run.IsCurrent = 1 + ORDER BY run.WholeBookPlotSynthesisRunID DESC + ) + SELECT + p.StoryIntelligenceBookPipelineID, + p.ProjectID, + project.ProjectName, + p.BookID, + book.BookTitle, + book.Subtitle AS BookSubtitle, + p.CurrentStage, + p.LastCompletedStage, + p.CurrentReviewStage, + p.Status, + p.CompletedUtc, + p.CharacterReviewCompletedUtc, + p.LocationReviewCompletedUtc, + p.AssetReviewCompletedUtc, + p.RelationshipReviewCompletedUtc, + p.KnowledgeReviewCompletedUtc, + p.NarrativeReviewCompletedUtc, + p.LastRunID, + CAST(COALESCE(rs.RunCount, 0) AS int) AS RunCount, + CAST(COALESCE(rs.CompletedRunCount, 0) AS int) AS CompletedRunCount, + CAST(COALESCE(rs.ActiveRunCount, 0) AS int) AS ActiveRunCount, + CAST(COALESCE(rs.FailedRunCount, 0) AS int) AS FailedRunCount, + CAST(COALESCE(rs.TotalDetectedScenes, 0) AS int) AS TotalDetectedScenes, + CAST(COALESCE(rs.CompletedScenes, 0) AS int) AS CompletedScenes, + CAST(COALESCE(rs.FailedScenes, 0) AS int) AS FailedScenes, + plot.WholeBookPlotSynthesisRunID, + plot.WholeBookPlotSynthesisStatus, + plot.WholeBookPlotSynthesisErrorMessage, + CAST(COALESCE(ns.NarrativeCandidateCount, 0) AS int) AS NarrativeCandidateCount, + CAST(COALESCE(ns.NarrativePendingCount, 0) AS int) AS NarrativePendingCount, + CAST(COALESCE(ns.NarrativeThreadCount, 0) AS int) AS NarrativeThreadCount, + CAST(COALESCE(nes.NarrativeEventCount, 0) AS int) AS NarrativeEventCount, + CAST(COALESCE(metrics.MetricValueCount, 0) AS int) AS MetricValueCount + FROM dbo.StoryIntelligenceBookPipelines p + INNER JOIN dbo.Projects project + ON project.ProjectID = p.ProjectID + INNER JOIN dbo.Books book + ON book.BookID = p.BookID + INNER JOIN dbo.ProjectUserAccess access + ON access.ProjectID = p.ProjectID + AND access.UserID = @UserID + AND access.IsActive = 1 + LEFT JOIN RunSummary rs + ON rs.BookID = p.BookID + LEFT JOIN NarrativeSummary ns + ON ns.BookID = p.BookID + LEFT JOIN NarrativeEventSummary nes + ON nes.BookID = p.BookID + LEFT JOIN MetricValueSummary metrics + ON metrics.BookID = p.BookID + LEFT JOIN CurrentPlotRun plot + ON plot.BookID = p.BookID + WHERE p.BookID = @BookID + AND book.IsArchived = 0 + AND project.IsArchived = 0; +END; +GO diff --git a/PlotLine/ViewModels/OnboardingViewModels.cs b/PlotLine/ViewModels/OnboardingViewModels.cs index 24eb02b..bbcbd87 100644 --- a/PlotLine/ViewModels/OnboardingViewModels.cs +++ b/PlotLine/ViewModels/OnboardingViewModels.cs @@ -184,6 +184,7 @@ public sealed class StoryIntelligenceProgressViewModel public int TotalDetectedScenes => Chapters.Sum(chapter => chapter.Scenes.Count > 0 ? chapter.Scenes.Count : chapter.TotalDetectedScenes ?? 0); public int TotalCompletedScenes => Chapters.Sum(chapter => chapter.CompletedScenes ?? 0); public int TotalAnalysedScenes => Math.Max(TotalCompletedScenes, PipelineDashboard.ScenesImported); + public int MetricValueCount { get; init; } public int TotalFailedScenes => Chapters.Sum(chapter => chapter.FailedScenes ?? 0); public int TotalAnalysedWords => Chapters.Sum(chapter => chapter.SourceWordCount ?? 0); public decimal? TotalEstimatedCostUSD => Chapters.Any(chapter => chapter.EstimatedCostUSD.HasValue) ? Chapters.Sum(chapter => chapter.EstimatedCostUSD ?? 0m) : null; diff --git a/PlotLine/Views/Onboarding/StoryIntelligenceReviewCentre.cshtml b/PlotLine/Views/Onboarding/StoryIntelligenceReviewCentre.cshtml index ad77980..cce32b2 100644 --- a/PlotLine/Views/Onboarding/StoryIntelligenceReviewCentre.cshtml +++ b/PlotLine/Views/Onboarding/StoryIntelligenceReviewCentre.cshtml @@ -26,11 +26,14 @@ @ReviewModule( "Characters", Model.CharacterNeedsAttentionCount, - Model.CharacterGateSatisfied, + Model.PipelineDashboard.CharacterStageComplete, false, "Resolve character identities before reviewing dependent Story Intelligence.", - Model.CharacterGateSatisfied ? "Review Characters" : "Resolve Characters", + "Review Characters", "StoryIntelligenceCharacters", + "View Characters", + "Characters", + new { projectId = Model.ProjectID }, true) @ReviewModule( @@ -39,8 +42,11 @@ Model.PipelineDashboard.AssetStageComplete, !Model.CharacterGateSatisfied, "Important objects and possessions found in the manuscript.", - ModuleAction("Assets", Model.AssetNeedsAttentionCount, Model.PipelineDashboard.AssetStageComplete), - "StoryIntelligenceAssets") + "Review Assets", + "StoryIntelligenceAssets", + "View Assets", + "StoryAssets", + new { projectId = Model.ProjectID }) @ReviewModule( "Knowledge", @@ -48,8 +54,11 @@ Model.PipelineDashboard.KnowledgeStageComplete, !Model.CharacterGateSatisfied, "Facts, discoveries, beliefs, and suspicions found in the manuscript.", - ModuleAction("Knowledge", Model.KnowledgeNeedsAttentionCount, Model.PipelineDashboard.KnowledgeStageComplete), - "StoryIntelligenceKnowledge") + "Review Knowledge", + "StoryIntelligenceKnowledge", + "View Knowledge", + "StoryBible", + new { projectId = Model.ProjectID }) @ReviewModule( "Locations", @@ -57,8 +66,11 @@ Model.PipelineDashboard.LocationStageComplete, !Model.CharacterGateSatisfied, "Places found in the manuscript.", - ModuleAction("Locations", Model.LocationNeedsAttentionCount, Model.PipelineDashboard.LocationStageComplete), - "StoryIntelligenceLocations") + "Review Locations", + "StoryIntelligenceLocations", + "View Locations", + "Locations", + new { projectId = Model.ProjectID }) @ReviewPlotSynthesis() @@ -68,8 +80,11 @@ Model.PipelineDashboard.RelationshipStageComplete, !Model.CharacterGateSatisfied, "Character relationships found in scene evidence.", - ModuleAction("Relationships", Model.RelationshipNeedsAttentionCount, Model.PipelineDashboard.RelationshipStageComplete), - "StoryIntelligenceRelationships") + "Review Relationships", + "StoryIntelligenceRelationships", + "View Relationships", + "RelationshipMap", + new { projectId = Model.ProjectID, bookId = Model.BookID }) @ReviewMetrics(!Model.CharacterGateSatisfied) @@ -82,7 +97,10 @@ false, "Some chapters or scenes reported analysis issues.", "Return to Book", - null) + "StoryIntelligenceReviewCentre", + "Return to Book", + "Books", + new { id = Model.BookID }) } @@ -94,9 +112,6 @@ @functions { - private string ModuleAction(string title, int? count, bool complete) - => count.GetValueOrDefault() > 0 || (!complete && !count.HasValue) ? $"Review {title}" : $"View {title}"; - private Microsoft.AspNetCore.Html.IHtmlContent ReviewPlotSynthesis() { var blocked = !Model.CharacterGateSatisfied; @@ -146,8 +161,8 @@ { status = "Complete"; description = "Plot Lines and Threads review has been saved."; - var href = Url.Action("StoryIntelligenceNarrative", "Onboarding", new { batchId = Model.BatchID }) ?? "#"; - actionHtml = $"View Plot Lines & Threads"; + var href = Url.Action("Index", "Timeline", new { projectId = Model.ProjectID, bookId = Model.BookID }) ?? "#"; + actionHtml = $"View on Timeline"; } else { @@ -177,7 +192,7 @@ if (string.Equals(title, "Scene Metrics", StringComparison.Ordinal)) { - return "Available"; + return Model.MetricValueCount > 0 ? "Available" : "No metrics available"; } if (complete) @@ -200,13 +215,19 @@ private Microsoft.AspNetCore.Html.IHtmlContent ReviewMetrics(bool blocked) { - var status = StatusText("Scene Metrics", Model.TotalAnalysedScenes, false, blocked); + var hasMetrics = Model.MetricValueCount > 0; + var status = StatusText("Scene Metrics", Model.MetricValueCount, false, blocked); var statusClass = blocked ? "story-review-centre-tile__status story-review-centre-tile__status--locked" - : "story-review-centre-tile__status story-review-centre-tile__status--ready"; + : hasMetrics + ? "story-review-centre-tile__status story-review-centre-tile__status--ready" + : "story-review-centre-tile__status story-review-centre-tile__status--locked"; + var href = Url.Action("Index", "Timeline", new { projectId = Model.ProjectID, bookId = Model.BookID }) ?? "#"; var actionHtml = blocked ? "Locked" - : "View Metrics"; + : hasMetrics + ? $"View on Timeline" + : "No action"; return new Microsoft.AspNetCore.Html.HtmlString($$"""
@@ -226,8 +247,11 @@ bool complete, bool blocked, string description, - string actionLabel, - string? action, + string reviewActionLabel, + string reviewAction, + string completeActionLabel, + string completeController, + object completeRouteValues, bool primary = false) { var status = StatusText(title, count, complete, blocked); @@ -239,9 +263,10 @@ var tileClass = "story-review-centre-tile" + (primary ? " story-review-centre-tile--primary" : string.Empty) + (blocked ? " story-review-centre-tile--locked" : string.Empty); - var href = action is null - ? Url.Action("Details", "Books", new { id = Model.BookID }) ?? "#" - : Url.Action(action, "Onboarding", new { batchId = Model.BatchID }) ?? "#"; + var href = complete + ? Url.Action("Index", completeController, completeRouteValues) ?? "#" + : Url.Action(reviewAction, "Onboarding", new { batchId = Model.BatchID }) ?? "#"; + var actionLabel = complete ? completeActionLabel : reviewActionLabel; var actionHtml = blocked ? "Locked" : $"{System.Net.WebUtility.HtmlEncode(actionLabel)}";