Writer Workspace changes.

This commit is contained in:
Nick Beckley 2026-06-09 21:06:45 +01:00
parent c130879b27
commit 77d5623e51
9 changed files with 212 additions and 21 deletions

View File

@ -1485,6 +1485,8 @@ public sealed class WriterDashboardScene
public decimal SceneNumber { get; set; }
public string SceneTitle { get; set; } = string.Empty;
public string? Summary { get; set; }
public int RevisionStatusID { get; set; }
public string RevisionStatusName { get; set; } = string.Empty;
public string DraftStatus { get; set; } = "Planned";
public int? Priority { get; set; }
public bool IsBlocked { get; set; }
@ -1501,6 +1503,8 @@ public sealed class ChapterWorkflowScene
public int SceneID { get; set; }
public decimal SceneNumber { get; set; }
public string SceneTitle { get; set; } = string.Empty;
public int RevisionStatusID { get; set; }
public string RevisionStatusName { get; set; } = string.Empty;
public string DraftStatus { get; set; } = "Planned";
public int? Priority { get; set; }
public bool ReadyForDraft { get; set; }

View File

@ -2896,6 +2896,7 @@ public sealed class WriterWorkspaceService(
{
ProjectID = projectId,
ProjectOptions = projectRows.Select(x => new SelectListItem(x.ProjectName, x.ProjectID.ToString(), x.ProjectID == projectId)).ToList(),
WriteNext = await GetDashboardScenesAsync(projectIds, "WriteNext"),
ContinueWriting = await GetDashboardScenesAsync(projectIds, "ContinueWriting"),
NeedsAttention = await GetDashboardScenesAsync(projectIds, "NeedsAttention"),
RevisionQueue = await GetDashboardScenesAsync(projectIds, "RevisionQueue"),

View File

@ -0,0 +1,144 @@
SET ANSI_NULLS ON;
GO
SET QUOTED_IDENTIFIER ON;
GO
DELETE rs
FROM dbo.RevisionStatuses rs
WHERE rs.StatusName IN (N'Ready To Draft', N'Drafting', N'Revising', N'Polishing', N'Complete', N'On Hold')
AND NOT EXISTS (SELECT 1 FROM dbo.Scenes s WHERE s.RevisionStatusID = rs.RevisionStatusID)
AND NOT EXISTS (SELECT 1 FROM dbo.Chapters c WHERE c.RevisionStatusID = rs.RevisionStatusID);
GO
CREATE OR ALTER PROCEDURE dbo.ChapterWorkflow_Summary
@ChapterID int
AS
BEGIN
SET NOCOUNT ON;
SELECT s.SceneID, s.SceneNumber, s.SceneTitle,
s.RevisionStatusID,
rs.StatusName AS RevisionStatusName,
COALESCE(sw.DraftStatus, rs.StatusName, N'Planned') AS DraftStatus,
sw.Priority,
COALESCE(sw.ReadyForDraft, 0) AS ReadyForDraft,
COALESCE(sw.ReadyForRevision, 0) AS ReadyForRevision,
COALESCE(sw.ReadyForPolish, 0) AS ReadyForPolish,
COALESCE(sw.IsBlocked, 0) AS IsBlocked,
sw.BlockedReason,
ISNULL(warnings.WarningCount, 0) AS WarningCount,
ISNULL(checklist.RemainingChecklistCount, 0) AS RemainingChecklistCount,
ISNULL(notes.UnresolvedNoteCount, 0) AS UnresolvedNoteCount
FROM dbo.Scenes s
INNER JOIN dbo.RevisionStatuses rs ON rs.RevisionStatusID = s.RevisionStatusID
LEFT JOIN dbo.SceneWorkflow sw ON sw.SceneID = s.SceneID
OUTER APPLY
(
SELECT COUNT(*) AS WarningCount
FROM dbo.ContinuityWarnings cw
WHERE cw.SceneID = s.SceneID AND cw.IsDismissed = 0
) warnings
OUTER APPLY
(
SELECT COUNT(*) AS RemainingChecklistCount
FROM dbo.SceneChecklistItems sci
WHERE sci.SceneID = s.SceneID AND sci.IsCompleted = 0
) checklist
OUTER APPLY
(
SELECT COUNT(*) AS UnresolvedNoteCount
FROM dbo.SceneNotes sn
WHERE sn.SceneID = s.SceneID AND sn.IsResolved = 0
) notes
WHERE s.ChapterID = @ChapterID AND s.IsArchived = 0
ORDER BY s.SortOrder, s.SceneID;
END;
GO
CREATE OR ALTER PROCEDURE dbo.WriterDashboard_Scenes
@ProjectID int = NULL,
@Queue nvarchar(50)
AS
BEGIN
SET NOCOUNT ON;
WITH SceneBase AS
(
SELECT p.ProjectID, p.ProjectName, b.BookID, b.BookTitle, c.ChapterID, c.ChapterNumber, c.ChapterTitle,
s.SceneID, s.SceneNumber, s.SceneTitle, s.Summary,
s.RevisionStatusID,
rs.StatusName AS RevisionStatusName,
COALESCE(sw.DraftStatus, rs.StatusName, N'Planned') AS DraftStatus,
sw.Priority,
COALESCE(sw.IsBlocked, 0) AS IsBlocked,
sw.BlockedReason,
COALESCE(sw.ReadyForDraft, 0) AS ReadyForDraft,
COALESCE(sw.ReadyForRevision, 0) AS ReadyForRevision,
COALESCE(sw.ReadyForPolish, 0) AS ReadyForPolish,
b.SortOrder AS BookSortOrder,
c.SortOrder AS ChapterSortOrder,
s.SortOrder AS SceneSortOrder
FROM dbo.Projects p
INNER JOIN dbo.Books b ON b.ProjectID = p.ProjectID AND b.IsArchived = 0
INNER JOIN dbo.Chapters c ON c.BookID = b.BookID AND c.IsArchived = 0
INNER JOIN dbo.Scenes s ON s.ChapterID = c.ChapterID AND s.IsArchived = 0
INNER JOIN dbo.RevisionStatuses rs ON rs.RevisionStatusID = s.RevisionStatusID
LEFT JOIN dbo.SceneWorkflow sw ON sw.SceneID = s.SceneID
WHERE p.IsArchived = 0 AND (@ProjectID IS NULL OR p.ProjectID = @ProjectID)
)
SELECT TOP (40) sb.ProjectID, sb.ProjectName, sb.BookID, sb.BookTitle, sb.ChapterID, sb.ChapterNumber, sb.ChapterTitle,
sb.SceneID, sb.SceneNumber, sb.SceneTitle, sb.Summary, sb.RevisionStatusID, sb.RevisionStatusName,
sb.DraftStatus, sb.Priority, sb.IsBlocked, sb.BlockedReason,
ISNULL(warnings.WarningCount, 0) AS WarningCount,
ISNULL(checklist.RemainingChecklistCount, 0) AS RemainingChecklistCount,
ISNULL(notes.UnresolvedNoteCount, 0) AS UnresolvedNoteCount,
threads.ImportantThreads,
pinned.PinnedNotes
FROM SceneBase sb
OUTER APPLY
(
SELECT COUNT(*) AS WarningCount
FROM dbo.ContinuityWarnings cw
WHERE cw.SceneID = sb.SceneID AND cw.IsDismissed = 0
) warnings
OUTER APPLY
(
SELECT COUNT(*) AS RemainingChecklistCount
FROM dbo.SceneChecklistItems sci
WHERE sci.SceneID = sb.SceneID AND sci.IsCompleted = 0
) checklist
OUTER APPLY
(
SELECT COUNT(*) AS UnresolvedNoteCount
FROM dbo.SceneNotes sn
INNER JOIN dbo.SceneNoteTypes snt ON snt.SceneNoteTypeID = sn.SceneNoteTypeID
WHERE sn.SceneID = sb.SceneID AND sn.IsResolved = 0
AND (@Queue <> N'NeedsAttention' OR snt.TypeName IN (N'TODO', N'Continuity Note'))
) notes
OUTER APPLY
(
SELECT STRING_AGG(CONVERT(nvarchar(max), pt.ThreadTitle), N', ') AS ImportantThreads
FROM
(
SELECT DISTINCT TOP (5) pt.ThreadTitle, pt.Importance
FROM dbo.ThreadEvents te
INNER JOIN dbo.PlotThreads pt ON pt.PlotThreadID = te.PlotThreadID
WHERE te.SceneID = sb.SceneID
ORDER BY pt.Importance DESC, pt.ThreadTitle
) pt
) threads
OUTER APPLY
(
SELECT STRING_AGG(CONVERT(nvarchar(max), COALESCE(sn.NoteTitle, sn.NoteText)), N' | ') AS PinnedNotes
FROM dbo.SceneNotes sn
WHERE sn.SceneID = sb.SceneID AND sn.IsPinned = 1 AND sn.IsResolved = 0
) pinned
WHERE
(@Queue = N'WriteNext' AND sb.RevisionStatusName = N'Outlined')
OR (@Queue = N'ContinueWriting' AND sb.RevisionStatusName = N'Drafted')
OR (@Queue = N'NeedsAttention' AND (sb.IsBlocked = 1 OR ISNULL(warnings.WarningCount, 0) > 0 OR ISNULL(notes.UnresolvedNoteCount, 0) > 0))
OR (@Queue = N'RevisionQueue' AND sb.RevisionStatusName = N'Needs Work')
OR (@Queue = N'PolishingQueue' AND sb.RevisionStatusName = N'Revised')
ORDER BY ISNULL(sb.Priority, 0) DESC, sb.BookSortOrder, sb.ChapterSortOrder, sb.SceneSortOrder, sb.SceneID;
END;
GO

View File

@ -312,6 +312,7 @@ public sealed class WriterDashboardViewModel
{
public int? ProjectID { get; set; }
public IReadOnlyList<SelectListItem> ProjectOptions { get; set; } = [];
public IReadOnlyList<WriterDashboardScene> WriteNext { get; set; } = [];
public IReadOnlyList<WriterDashboardScene> ContinueWriting { get; set; } = [];
public IReadOnlyList<WriterDashboardScene> NeedsAttention { get; set; } = [];
public IReadOnlyList<WriterDashboardScene> RevisionQueue { get; set; } = [];

View File

@ -972,13 +972,16 @@
<input type="hidden" name="ReturnProjectID" value="@Model.ReturnProjectID" />
<input type="hidden" name="ReturnBookID" value="@Model.ReturnBookID" />
<div class="row g-2">
<div class="col-sm-6">
<label class="form-label" for="DraftStatus">Draft status</label>
<select class="form-select form-select-sm" id="DraftStatus" name="DraftStatus" asp-items="Model.DraftStatusOptions"></select>
</div>
<div class="col-sm-6">
<input type="hidden" name="DraftStatus" value="@Model.Workflow.DraftStatus" />
<input type="hidden" name="ReadyForDraft" value="false" />
<input type="hidden" name="ReadyForRevision" value="false" />
<input type="hidden" name="ReadyForPolish" value="false" />
<div class="col-12">
<label class="form-label" for="Priority">Priority</label>
<input class="form-control form-control-sm" id="Priority" name="Priority" type="number" min="1" max="10" value="@Model.Workflow.Priority" />
<div class="writer-priority-control">
<input class="form-range" id="Priority" name="Priority" type="range" min="1" max="10" value="@(Model.Workflow.Priority ?? 5)" data-priority-slider />
<span class="status-pill" data-priority-label></span>
</div>
</div>
<div class="col-sm-6">
<label class="form-label" for="EstimatedWordCount">Estimated words</label>
@ -997,9 +1000,6 @@
<input class="form-control form-control-sm" id="LastWorkedOn" name="LastWorkedOn" type="date" value="@Model.Workflow.LastWorkedOn?.ToString("yyyy-MM-dd")" />
</div>
<div class="col-12 writer-checkbox-grid">
<label><input type="checkbox" name="ReadyForDraft" value="true" checked="@Model.Workflow.ReadyForDraft" /> Ready for draft</label>
<label><input type="checkbox" name="ReadyForRevision" value="true" checked="@Model.Workflow.ReadyForRevision" /> Ready for revision</label>
<label><input type="checkbox" name="ReadyForPolish" value="true" checked="@Model.Workflow.ReadyForPolish" /> Ready for polish</label>
<label><input type="checkbox" name="IsBlocked" value="true" checked="@Model.Workflow.IsBlocked" /> Blocked</label>
</div>
<div class="col-12">
@ -1011,6 +1011,26 @@
<button class="btn btn-primary btn-sm" type="submit">Save workflow</button>
</div>
</form>
<script>
(() => {
const slider = document.querySelector('[data-priority-slider]');
const label = document.querySelector('[data-priority-label]');
if (!slider || !label) return;
const labelFor = value => {
const priority = Number(value);
if (priority <= 2) return `${priority} - Very Low`;
if (priority <= 4) return `${priority} - Low`;
if (priority <= 6) return `${priority} - Medium`;
if (priority <= 8) return `${priority} - High`;
return `${priority} - Critical`;
};
const update = () => label.textContent = labelFor(slider.value);
slider.addEventListener('input', update);
update();
})();
</script>
</section>
<section class="inspector-section writer-checklist-section" data-section-title="Scene Checklist" data-section-id="inspector-checklist">

View File

@ -2,9 +2,10 @@
@{
ViewData["Title"] = $"Chapter Workflow - {Model.Chapter.ChapterTitle}";
var blockedCount = Model.Scenes.Count(x => x.IsBlocked);
var readyDraftCount = Model.Scenes.Count(x => x.ReadyForDraft);
var readyRevisionCount = Model.Scenes.Count(x => x.ReadyForRevision);
var readyPolishCount = Model.Scenes.Count(x => x.ReadyForPolish);
var writeNextCount = Model.Scenes.Count(x => x.RevisionStatusName == "Outlined");
var continueWritingCount = Model.Scenes.Count(x => x.RevisionStatusName == "Drafted");
var revisionCount = Model.Scenes.Count(x => x.RevisionStatusName == "Needs Work");
var polishCount = Model.Scenes.Count(x => x.RevisionStatusName == "Revised");
}
<nav class="breadcrumb-trail" aria-label="Breadcrumb">
@ -31,9 +32,10 @@
<section class="story-bible-section">
<div class="story-bible-grid story-bible-grid-compact">
<article class="story-bible-card"><h3>Ready to draft</h3><strong class="writer-kpi">@readyDraftCount</strong></article>
<article class="story-bible-card"><h3>Ready to revise</h3><strong class="writer-kpi">@readyRevisionCount</strong></article>
<article class="story-bible-card"><h3>Ready to polish</h3><strong class="writer-kpi">@readyPolishCount</strong></article>
<article class="story-bible-card"><h3>Write next</h3><strong class="writer-kpi">@writeNextCount</strong></article>
<article class="story-bible-card"><h3>Continue writing</h3><strong class="writer-kpi">@continueWritingCount</strong></article>
<article class="story-bible-card"><h3>Revision</h3><strong class="writer-kpi">@revisionCount</strong></article>
<article class="story-bible-card"><h3>Polish</h3><strong class="writer-kpi">@polishCount</strong></article>
<article class="story-bible-card"><h3>Blocked</h3><strong class="writer-kpi">@blockedCount</strong></article>
</div>
</section>
@ -75,16 +77,13 @@
<article class="writer-queue-card">
<div class="writer-queue-title">
<h3>Scene @scene.SceneNumber: @scene.SceneTitle</h3>
<span class="status-pill">@scene.DraftStatus</span>
<span class="status-pill">@(string.IsNullOrWhiteSpace(scene.RevisionStatusName) ? scene.DraftStatus : scene.RevisionStatusName)</span>
</div>
<div class="story-bible-meta">
@if (scene.Priority.HasValue) { <span>Priority @scene.Priority</span> }
<span>@scene.WarningCount warnings</span>
<span>@scene.RemainingChecklistCount checklist remaining</span>
<span>@scene.UnresolvedNoteCount open notes</span>
@if (scene.ReadyForDraft) { <span>Ready for draft</span> }
@if (scene.ReadyForRevision) { <span>Ready for revision</span> }
@if (scene.ReadyForPolish) { <span>Ready for polish</span> }
@if (scene.IsBlocked) { <span>Blocked</span> }
</div>
@if (!string.IsNullOrWhiteSpace(scene.BlockedReason))

View File

@ -22,6 +22,17 @@
<div class="story-bible-section-heading">
<div>
<p class="eyebrow">Write Next</p>
<h2>Write Next <help-icon key="workspace.writeNext" /></h2>
</div>
<span class="soft-count">@Model.WriteNext.Count scenes</span>
</div>
<partial name="_WriterSceneQueue" model="Model.WriteNext" />
</section>
<section class="writer-dashboard-panel">
<div class="story-bible-section-heading">
<div>
<p class="eyebrow">In progress</p>
<h2>Continue Writing <help-icon key="workspace.writeNext" /></h2>
</div>
<span class="soft-count">@Model.ContinueWriting.Count scenes</span>

View File

@ -2,7 +2,7 @@
@if (!Model.Any())
{
<p class="muted">Nothing needs your attention here. When scenes are ready for this stage, they will appear in this queue.</p>
<p class="muted">Scenes appear here based on Revision Status. Use Outlined for Write Next, Drafted for Continue Writing, Needs Work for Revision, and Revised for Polishing.</p>
}
else
{
@ -15,7 +15,7 @@ else
<span class="eyebrow">@scene.BookTitle / Chapter @scene.ChapterNumber</span>
<h3>Scene @scene.SceneNumber: @scene.SceneTitle</h3>
</div>
<span class="status-pill">@scene.DraftStatus</span>
<span class="status-pill">@(string.IsNullOrWhiteSpace(scene.RevisionStatusName) ? scene.DraftStatus : scene.RevisionStatusName)</span>
</div>
@if (!string.IsNullOrWhiteSpace(scene.Summary))
{

View File

@ -3146,6 +3146,17 @@ body.dragging-location [data-drag-type="location"] {
font-weight: 650;
}
.writer-priority-control {
display: grid;
grid-template-columns: minmax(0, 1fr) max-content;
align-items: center;
gap: 12px;
}
.writer-priority-control .form-range {
min-width: 0;
}
.writer-note-list,
.writer-queue-list {
display: grid;