232 lines
12 KiB
Plaintext
232 lines
12 KiB
Plaintext
@model BookDetailViewModel
|
|
@{
|
|
ViewData["Title"] = Model.Book.BookTitle;
|
|
ViewData["ProjectSection"] = "Overview";
|
|
}
|
|
|
|
<nav class="breadcrumb-trail" aria-label="Breadcrumb">
|
|
<a asp-controller="Projects" asp-action="Index">Projects</a>
|
|
<a asp-controller="Projects" asp-action="Details" asp-route-id="@Model.Project.ProjectID">@Model.Project.ProjectName</a>
|
|
<span>@Model.Book.BookTitle</span>
|
|
</nav>
|
|
|
|
<partial name="_ProjectSectionNav" model="Model.Project" />
|
|
|
|
@if (TempData["ArchiveMessage"] is string archiveMessage)
|
|
{
|
|
<div class="alert alert-info">@archiveMessage</div>
|
|
}
|
|
@if (TempData["ManuscriptMessage"] is string manuscriptMessage)
|
|
{
|
|
<div class="alert alert-info">@manuscriptMessage</div>
|
|
}
|
|
|
|
<div class="page-heading">
|
|
<div>
|
|
<p class="eyebrow"><a asp-controller="Projects" asp-action="Details" asp-route-id="@Model.Project.ProjectID">@Model.Project.ProjectName</a></p>
|
|
<h1>Book @Model.Book.BookNumber: @Model.Book.BookTitle <help-icon key="books.overview" /></h1>
|
|
@if (!string.IsNullOrWhiteSpace(Model.Book.Subtitle))
|
|
{
|
|
<p class="lead-text">@Model.Book.Subtitle</p>
|
|
}
|
|
@if (!string.IsNullOrWhiteSpace(Model.Book.Tagline))
|
|
{
|
|
<p class="muted">@Model.Book.Tagline</p>
|
|
}
|
|
@if (!string.IsNullOrWhiteSpace(Model.Book.Description))
|
|
{
|
|
<p class="lead-text">@Model.Book.Description</p>
|
|
}
|
|
</div>
|
|
<div class="button-row">
|
|
<a class="btn btn-outline-secondary" asp-action="Edit" asp-route-id="@Model.Book.BookID">Edit book</a>
|
|
<a class="btn btn-primary" asp-controller="Chapters" asp-action="Create" asp-route-bookId="@Model.Book.BookID">Add chapter</a>
|
|
<div class="dropdown pw-actions">
|
|
<button class="btn pw-actions__toggle dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">
|
|
Actions
|
|
</button>
|
|
<div class="dropdown-menu dropdown-menu-end">
|
|
<a class="dropdown-item" asp-controller="Analytics" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID" asp-route-bookId="@Model.Book.BookID">Story health</a>
|
|
<div class="dropdown-divider"></div>
|
|
<form asp-action="Archive" method="post" class="archive-button-form" data-confirm-message="Archive this book? This will hide it from active lists and timelines, but the data will be kept and can be restored later.">
|
|
<input type="hidden" name="id" value="@Model.Book.BookID" />
|
|
<input type="hidden" name="projectId" value="@Model.Project.ProjectID" />
|
|
<button class="dropdown-item text-danger" type="submit">Archive Book</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
@if (Model.StoryIntelligencePipeline is not null)
|
|
{
|
|
var pipeline = Model.StoryIntelligencePipeline;
|
|
<section class="list-section">
|
|
<div class="d-flex flex-wrap align-items-center justify-content-between gap-3">
|
|
<div>
|
|
<p class="eyebrow">Story Intelligence</p>
|
|
<h2>@(pipeline.IsComplete ? "Story Intelligence Complete" : "Continue Story Intelligence")</h2>
|
|
<p class="muted mb-0">@StoryIntelligenceDescription(pipeline)</p>
|
|
</div>
|
|
<a class="btn btn-primary"
|
|
asp-controller="Onboarding"
|
|
asp-action="ContinueStoryIntelligence"
|
|
asp-route-bookId="@Model.Book.BookID">@(pipeline.IsComplete ? "View Story Intelligence Summary" : "Continue Story Intelligence")</a>
|
|
</div>
|
|
</section>
|
|
}
|
|
|
|
<section class="list-section">
|
|
<h2>Book metadata</h2>
|
|
<div class="row g-3">
|
|
<div class="col-md-3">
|
|
<span class="status-pill">@Model.Book.Status</span>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<strong>@Model.Book.CurrentWordCount.ToString("N0")</strong>
|
|
<span class="muted">current words</span>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<strong>@(Model.Book.TargetWordCount?.ToString("N0") ?? "Not set")</strong>
|
|
<span class="muted">target words</span>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<strong>@(Model.Book.ProgressPercentage.HasValue ? $"{Model.Book.ProgressPercentage:0.#}%" : "Not set")</strong>
|
|
<span class="muted">progress</span>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<strong>@Model.Book.ChapterCount.ToString("N0")</strong>
|
|
<span class="muted">chapters</span>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<strong>@Model.Book.SceneCount.ToString("N0")</strong>
|
|
<span class="muted">scenes</span>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<strong>@(Model.Book.WritingStartDate?.ToString("dd MMM yyyy") ?? "Not set")</strong>
|
|
<span class="muted">writing start</span>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<strong>@(Model.Book.TargetCompletionDate?.ToString("dd MMM yyyy") ?? "Not set")</strong>
|
|
<span class="muted">target completion</span>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<strong>@(Model.Book.PublicationDate?.ToString("dd MMM yyyy") ?? "Not set")</strong>
|
|
<span class="muted">publication</span>
|
|
</div>
|
|
<div class="col-md-3">
|
|
@if (Model.ManuscriptDocument is null)
|
|
{
|
|
<strong>No manuscript linked</strong>
|
|
}
|
|
else
|
|
{
|
|
<strong>Manuscript linked</strong>
|
|
<span class="muted">Last synced: @(Model.ManuscriptDocument.LastSyncUtc?.ToString("dd MMM yyyy HH:mm") ?? "Never")</span>
|
|
<form asp-action="UnlinkManuscript" method="post" class="mt-2" data-confirm-message="This will unlink the Word manuscript from this Book in PlotDirector. It will not delete chapters, scenes, characters, assets, locations, or manuscript text. If the Word document is still open, you should also unlink it from the Word Companion so the hidden document metadata is removed. Continue?">
|
|
<input type="hidden" name="id" value="@Model.Book.BookID" />
|
|
<button type="submit" class="btn btn-outline-danger btn-sm">Unlink Manuscript</button>
|
|
</form>
|
|
}
|
|
<span class="muted">Manuscript Status</span>
|
|
</div>
|
|
@if (!string.IsNullOrWhiteSpace(Model.Book.ShortDescription))
|
|
{
|
|
<div class="col-12">
|
|
<p>@Model.Book.ShortDescription</p>
|
|
</div>
|
|
}
|
|
</div>
|
|
</section>
|
|
|
|
<section class="list-section">
|
|
<h2>Chapters <help-icon key="books.chapters" /></h2>
|
|
@if (!Model.Chapters.Any())
|
|
{
|
|
<p class="muted">No chapters yet.</p>
|
|
}
|
|
else
|
|
{
|
|
<div class="table-responsive">
|
|
<table class="table align-middle">
|
|
<thead>
|
|
<tr>
|
|
<th>Chapter</th>
|
|
<th>Title</th>
|
|
<th>Status</th>
|
|
<th>Manuscript</th>
|
|
<th>Summary</th>
|
|
<th></th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
@foreach (var chapter in Model.Chapters)
|
|
{
|
|
<tr>
|
|
<td class="narrow">@chapter.ChapterNumber</td>
|
|
<td><a asp-controller="Chapters" asp-action="Details" asp-route-id="@chapter.ChapterID">@chapter.ChapterTitle</a></td>
|
|
<td>
|
|
<span class="status-pill">@chapter.RevisionStatusName</span>
|
|
@if (!string.IsNullOrWhiteSpace(chapter.ChapterPurposeName))
|
|
{
|
|
<span class="status-pill">@chapter.ChapterPurposeName</span>
|
|
}
|
|
</td>
|
|
<td><span class="status-pill">@(chapter.IsLinkedToManuscript ? "🔗 Linked" : "📝 Planned")</span></td>
|
|
<td>@chapter.Summary</td>
|
|
<td class="text-end">
|
|
<a class="btn btn-outline-secondary btn-sm" asp-controller="Chapters" asp-action="Edit" asp-route-id="@chapter.ChapterID">Edit</a>
|
|
<form asp-controller="Chapters" asp-action="Archive" method="post" class="archive-button-form" data-confirm-message="Archive this chapter? This will hide it from active lists and timelines, but the data will be kept and can be restored later.">
|
|
<input type="hidden" name="id" value="@chapter.ChapterID" />
|
|
<input type="hidden" name="bookId" value="@Model.Book.BookID" />
|
|
<button class="btn btn-outline-danger btn-sm" type="submit">Archive</button>
|
|
</form>
|
|
</td>
|
|
</tr>
|
|
}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
}
|
|
</section>
|
|
|
|
@functions {
|
|
private static string StoryIntelligenceDescription(StoryIntelligenceBookPipelineState pipeline)
|
|
{
|
|
if (pipeline.IsComplete)
|
|
{
|
|
return "All currently available Story Intelligence stages are complete.";
|
|
}
|
|
|
|
return pipeline.CurrentReviewStage switch
|
|
{
|
|
StoryIntelligencePipelineStages.RelationshipReview => "Review detected relationships and decide what to create or link.",
|
|
StoryIntelligencePipelineStages.AssetReview => "Review detected assets and decide what to create or link.",
|
|
StoryIntelligencePipelineStages.LocationReview => "Review detected locations and decide what to create or link.",
|
|
StoryIntelligencePipelineStages.CharacterReview => "Scene creation is complete. Review detected characters to continue building the story database.",
|
|
StoryIntelligencePipelineStages.SceneReview => "Review prepared scenes before creating them in PlotDirector.",
|
|
_ when StoryIntelligenceReadyForRelationships(pipeline) => "Review detected relationships and decide what to create or link.",
|
|
_ when StoryIntelligenceReadyForAssets(pipeline) => "Review detected assets and decide what to create or link.",
|
|
_ when StoryIntelligenceReadyForLocations(pipeline) => "Review detected locations and decide what to create or link.",
|
|
_ => "PlotDirector will resume from the next Story Intelligence stage."
|
|
};
|
|
}
|
|
|
|
private static bool StoryIntelligenceReadyForRelationships(StoryIntelligenceBookPipelineState pipeline)
|
|
=> string.Equals(pipeline.LastCompletedStage, StoryIntelligencePipelineStages.AssetImport, StringComparison.OrdinalIgnoreCase)
|
|
|| pipeline.CurrentStage is StoryIntelligencePipelineStages.RelationshipReview or StoryIntelligencePipelineStages.RelationshipImport;
|
|
|
|
private static bool StoryIntelligenceReadyForAssets(StoryIntelligenceBookPipelineState pipeline)
|
|
=> string.Equals(pipeline.LastCompletedStage, StoryIntelligencePipelineStages.LocationImport, StringComparison.OrdinalIgnoreCase)
|
|
|| pipeline.CurrentStage is StoryIntelligencePipelineStages.AssetReview or StoryIntelligencePipelineStages.AssetImport
|
|
or StoryIntelligencePipelineStages.RelationshipReview or StoryIntelligencePipelineStages.RelationshipImport
|
|
|| (string.Equals(pipeline.CurrentStage, StoryIntelligencePipelineStages.Complete, StringComparison.OrdinalIgnoreCase)
|
|
&& !string.Equals(pipeline.LastCompletedStage, StoryIntelligencePipelineStages.RelationshipImport, StringComparison.OrdinalIgnoreCase));
|
|
|
|
private static bool StoryIntelligenceReadyForLocations(StoryIntelligenceBookPipelineState pipeline)
|
|
=> string.Equals(pipeline.LastCompletedStage, StoryIntelligencePipelineStages.CharacterImport, StringComparison.OrdinalIgnoreCase)
|
|
|| pipeline.CurrentStage is StoryIntelligencePipelineStages.LocationReview or StoryIntelligencePipelineStages.LocationImport
|
|
or StoryIntelligencePipelineStages.AssetReview or StoryIntelligencePipelineStages.AssetImport
|
|
or StoryIntelligencePipelineStages.RelationshipReview or StoryIntelligencePipelineStages.RelationshipImport;
|
|
}
|