Phase 10F: Sync Current Scene Progress
This commit is contained in:
parent
7c9e9b0b6f
commit
b813b187d7
@ -14,7 +14,7 @@ public interface IWordCompanionRepository
|
|||||||
Task<WordCompanionSyncSceneResponse?> SyncSceneAsync(int bookId, int userId, WordCompanionSyncSceneRequest request);
|
Task<WordCompanionSyncSceneResponse?> SyncSceneAsync(int bookId, int userId, WordCompanionSyncSceneRequest request);
|
||||||
Task<WordCompanionSyncManuscriptResponse?> SyncManuscriptAsync(int bookId, int userId, WordCompanionSyncManuscriptRequest request);
|
Task<WordCompanionSyncManuscriptResponse?> SyncManuscriptAsync(int bookId, int userId, WordCompanionSyncManuscriptRequest request);
|
||||||
Task<WordCompanionUpdateSceneLinksResponse?> UpdateSceneLinksAsync(int sceneId, int userId, WordCompanionUpdateSceneLinksRequest request);
|
Task<WordCompanionUpdateSceneLinksResponse?> UpdateSceneLinksAsync(int sceneId, int userId, WordCompanionUpdateSceneLinksRequest request);
|
||||||
Task<WordCompanionUpdateWordCountResponse?> UpdateWordCountAsync(int sceneId, int userId, int actualWords);
|
Task<WordCompanionUpdateWordCountResponse?> UpdateWordCountAsync(int sceneId, int userId, int actualWords, DateTime? lastWorkedOn);
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed class WordCompanionRepository(ISqlConnectionFactory connectionFactory) : IWordCompanionRepository
|
public sealed class WordCompanionRepository(ISqlConnectionFactory connectionFactory) : IWordCompanionRepository
|
||||||
@ -179,12 +179,12 @@ public sealed class WordCompanionRepository(ISqlConnectionFactory connectionFact
|
|||||||
commandType: CommandType.StoredProcedure);
|
commandType: CommandType.StoredProcedure);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<WordCompanionUpdateWordCountResponse?> UpdateWordCountAsync(int sceneId, int userId, int actualWords)
|
public async Task<WordCompanionUpdateWordCountResponse?> UpdateWordCountAsync(int sceneId, int userId, int actualWords, DateTime? lastWorkedOn)
|
||||||
{
|
{
|
||||||
using var connection = connectionFactory.CreateConnection();
|
using var connection = connectionFactory.CreateConnection();
|
||||||
return await connection.QuerySingleOrDefaultAsync<WordCompanionUpdateWordCountResponse>(
|
return await connection.QuerySingleOrDefaultAsync<WordCompanionUpdateWordCountResponse>(
|
||||||
"dbo.WordCompanion_Scene_UpdateWordCount",
|
"dbo.WordCompanion_Scene_UpdateWordCount",
|
||||||
new { SceneID = sceneId, UserID = userId, ActualWords = actualWords },
|
new { SceneID = sceneId, UserID = userId, ActualWords = actualWords, LastWorkedOn = lastWorkedOn },
|
||||||
commandType: CommandType.StoredProcedure);
|
commandType: CommandType.StoredProcedure);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -163,12 +163,15 @@ public sealed class WordCompanionUpdateSceneLinksResponse
|
|||||||
|
|
||||||
public sealed class WordCompanionUpdateWordCountRequest
|
public sealed class WordCompanionUpdateWordCountRequest
|
||||||
{
|
{
|
||||||
public int ActualWords { get; set; }
|
public int? ActualWords { get; set; }
|
||||||
|
public int? ActualWordCount { get; set; }
|
||||||
|
public DateTime? LastWorkedOn { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed class WordCompanionUpdateWordCountResponse
|
public sealed class WordCompanionUpdateWordCountResponse
|
||||||
{
|
{
|
||||||
public int SceneId { get; init; }
|
public int SceneId { get; init; }
|
||||||
public int ActualWords { get; init; }
|
public int ActualWords { get; init; }
|
||||||
|
public DateTime? LastWorkedOn { get; init; }
|
||||||
public string Message { get; init; } = "Word count updated.";
|
public string Message { get; init; } = "Word count updated.";
|
||||||
}
|
}
|
||||||
|
|||||||
@ -107,9 +107,10 @@ public sealed class WordCompanionService(IWordCompanionRepository repository, IC
|
|||||||
|
|
||||||
public Task<WordCompanionUpdateWordCountResponse?> UpdateWordCountAsync(int sceneId, WordCompanionUpdateWordCountRequest request)
|
public Task<WordCompanionUpdateWordCountResponse?> UpdateWordCountAsync(int sceneId, WordCompanionUpdateWordCountRequest request)
|
||||||
{
|
{
|
||||||
return sceneId <= 0 || request.ActualWords < 0
|
var actualWords = request.ActualWordCount ?? request.ActualWords;
|
||||||
|
return sceneId <= 0 || actualWords is null or < 0
|
||||||
? Task.FromResult<WordCompanionUpdateWordCountResponse?>(null)
|
? Task.FromResult<WordCompanionUpdateWordCountResponse?>(null)
|
||||||
: repository.UpdateWordCountAsync(sceneId, RequireUserId(), request.ActualWords);
|
: repository.UpdateWordCountAsync(sceneId, RequireUserId(), actualWords.Value, request.LastWorkedOn);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<WordCompanionResolveSceneResponse?> ResolveSceneCoreAsync(int bookId, WordCompanionResolveSceneRequest request)
|
private async Task<WordCompanionResolveSceneResponse?> ResolveSceneCoreAsync(int bookId, WordCompanionResolveSceneRequest request)
|
||||||
@ -124,4 +125,3 @@ public sealed class WordCompanionService(IWordCompanionRepository repository, IC
|
|||||||
|
|
||||||
private static bool HasInvalidIds(IEnumerable<int> ids) => ids.Any(id => id <= 0);
|
private static bool HasInvalidIds(IEnumerable<int> ids) => ids.Any(id => id <= 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
52
PlotLine/Sql/059_Phase10F_WordCompanionSceneProgressSync.sql
Normal file
52
PlotLine/Sql/059_Phase10F_WordCompanionSceneProgressSync.sql
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
CREATE OR ALTER PROCEDURE dbo.WordCompanion_Scene_UpdateWordCount
|
||||||
|
@SceneID int,
|
||||||
|
@UserID int,
|
||||||
|
@ActualWords int,
|
||||||
|
@LastWorkedOn datetime2 = NULL
|
||||||
|
AS
|
||||||
|
BEGIN
|
||||||
|
SET NOCOUNT ON;
|
||||||
|
|
||||||
|
IF @ActualWords < 0
|
||||||
|
RETURN;
|
||||||
|
|
||||||
|
IF NOT EXISTS
|
||||||
|
(
|
||||||
|
SELECT 1
|
||||||
|
FROM dbo.Scenes s
|
||||||
|
INNER JOIN dbo.Chapters c ON c.ChapterID = s.ChapterID
|
||||||
|
INNER JOIN dbo.Books b ON b.BookID = c.BookID
|
||||||
|
INNER JOIN dbo.Projects p ON p.ProjectID = b.ProjectID
|
||||||
|
INNER JOIN dbo.ProjectUserAccess pua ON pua.ProjectID = p.ProjectID
|
||||||
|
WHERE s.SceneID = @SceneID
|
||||||
|
AND s.IsArchived = 0
|
||||||
|
AND c.IsArchived = 0
|
||||||
|
AND b.IsArchived = 0
|
||||||
|
AND p.IsArchived = 0
|
||||||
|
AND pua.UserID = @UserID
|
||||||
|
AND pua.IsActive = 1
|
||||||
|
)
|
||||||
|
RETURN;
|
||||||
|
|
||||||
|
DECLARE @LastWorkedOnDate date = COALESCE(CONVERT(date, @LastWorkedOn), CONVERT(date, SYSUTCDATETIME()));
|
||||||
|
|
||||||
|
IF EXISTS (SELECT 1 FROM dbo.SceneWorkflow WHERE SceneID = @SceneID)
|
||||||
|
BEGIN
|
||||||
|
UPDATE dbo.SceneWorkflow
|
||||||
|
SET ActualWordCount = @ActualWords,
|
||||||
|
LastWorkedOn = @LastWorkedOnDate,
|
||||||
|
UpdatedDate = SYSUTCDATETIME()
|
||||||
|
WHERE SceneID = @SceneID;
|
||||||
|
END
|
||||||
|
ELSE
|
||||||
|
BEGIN
|
||||||
|
INSERT dbo.SceneWorkflow (SceneID, ActualWordCount, LastWorkedOn)
|
||||||
|
VALUES (@SceneID, @ActualWords, @LastWorkedOnDate);
|
||||||
|
END;
|
||||||
|
|
||||||
|
SELECT @SceneID AS SceneId,
|
||||||
|
@ActualWords AS ActualWords,
|
||||||
|
@LastWorkedOnDate AS LastWorkedOn,
|
||||||
|
N'Word count updated.' AS Message;
|
||||||
|
END;
|
||||||
|
GO
|
||||||
@ -154,6 +154,30 @@
|
|||||||
<strong data-plotdirector-blocked-reason>-</strong>
|
<strong data-plotdirector-blocked-reason>-</strong>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="word-companion-progress-sync">
|
||||||
|
<div class="word-companion-section-header">
|
||||||
|
<span>Progress Sync</span>
|
||||||
|
<button type="button" data-sync-scene-progress disabled>Sync Scene Progress</button>
|
||||||
|
</div>
|
||||||
|
<div class="word-companion-progress-grid">
|
||||||
|
<div>
|
||||||
|
<span>Word Document Count</span>
|
||||||
|
<strong data-sync-word-count>-</strong>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span>PlotDirector Count</span>
|
||||||
|
<strong data-sync-plotdirector-count>-</strong>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span>Last Synced</span>
|
||||||
|
<strong data-sync-last-synced>-</strong>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span>Sync Status</span>
|
||||||
|
<strong data-sync-status>No resolved PlotDirector scene.</strong>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="word-companion-brief" data-writing-brief-block hidden>
|
<div class="word-companion-brief" data-writing-brief-block hidden>
|
||||||
<span>Writing Brief</span>
|
<span>Writing Brief</span>
|
||||||
<p data-writing-brief></p>
|
<p data-writing-brief></p>
|
||||||
|
|||||||
@ -197,6 +197,27 @@ body {
|
|||||||
font-size: 0.86rem;
|
font-size: 0.86rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.word-companion-progress-sync {
|
||||||
|
display: grid;
|
||||||
|
gap: 8px;
|
||||||
|
border-top: 1px solid var(--companion-line);
|
||||||
|
padding-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.word-companion-progress-grid {
|
||||||
|
display: grid;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.word-companion-progress-grid div {
|
||||||
|
display: grid;
|
||||||
|
gap: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.word-companion-progress-grid strong {
|
||||||
|
font-size: 0.88rem;
|
||||||
|
}
|
||||||
|
|
||||||
.word-companion-brief {
|
.word-companion-brief {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 5px;
|
gap: 5px;
|
||||||
|
|||||||
@ -31,6 +31,11 @@
|
|||||||
const plotDirectorBlockedStatus = document.querySelector("[data-plotdirector-blocked-status]");
|
const plotDirectorBlockedStatus = document.querySelector("[data-plotdirector-blocked-status]");
|
||||||
const plotDirectorBlockedReasonRow = document.querySelector("[data-plotdirector-blocked-reason-row]");
|
const plotDirectorBlockedReasonRow = document.querySelector("[data-plotdirector-blocked-reason-row]");
|
||||||
const plotDirectorBlockedReason = document.querySelector("[data-plotdirector-blocked-reason]");
|
const plotDirectorBlockedReason = document.querySelector("[data-plotdirector-blocked-reason]");
|
||||||
|
const syncSceneProgressButton = document.querySelector("[data-sync-scene-progress]");
|
||||||
|
const syncWordCount = document.querySelector("[data-sync-word-count]");
|
||||||
|
const syncPlotDirectorCount = document.querySelector("[data-sync-plotdirector-count]");
|
||||||
|
const syncLastSynced = document.querySelector("[data-sync-last-synced]");
|
||||||
|
const syncStatus = document.querySelector("[data-sync-status]");
|
||||||
const writingBriefBlock = document.querySelector("[data-writing-brief-block]");
|
const writingBriefBlock = document.querySelector("[data-writing-brief-block]");
|
||||||
const writingBrief = document.querySelector("[data-writing-brief]");
|
const writingBrief = document.querySelector("[data-writing-brief]");
|
||||||
const plotDirectorLinkGroups = document.querySelector("[data-plotdirector-link-groups]");
|
const plotDirectorLinkGroups = document.querySelector("[data-plotdirector-link-groups]");
|
||||||
@ -66,6 +71,8 @@
|
|||||||
let officePlatform = "Unknown";
|
let officePlatform = "Unknown";
|
||||||
let detectedChapterTitle = "";
|
let detectedChapterTitle = "";
|
||||||
let detectedSceneTitle = "";
|
let detectedSceneTitle = "";
|
||||||
|
let detectedSceneWordCount = null;
|
||||||
|
let resolvedSceneId = null;
|
||||||
|
|
||||||
const setOfficeStatus = (message) => {
|
const setOfficeStatus = (message) => {
|
||||||
if (officeStatus) {
|
if (officeStatus) {
|
||||||
@ -109,6 +116,7 @@
|
|||||||
const setCurrentStructure = (chapterTitle, sceneTitle, wordCount) => {
|
const setCurrentStructure = (chapterTitle, sceneTitle, wordCount) => {
|
||||||
detectedChapterTitle = chapterTitle || "";
|
detectedChapterTitle = chapterTitle || "";
|
||||||
detectedSceneTitle = sceneTitle || "";
|
detectedSceneTitle = sceneTitle || "";
|
||||||
|
detectedSceneWordCount = Number.isInteger(wordCount) ? wordCount : null;
|
||||||
|
|
||||||
if (currentChapter) {
|
if (currentChapter) {
|
||||||
currentChapter.textContent = chapterTitle || "Not detected";
|
currentChapter.textContent = chapterTitle || "Not detected";
|
||||||
@ -119,6 +127,7 @@
|
|||||||
if (currentWordCount) {
|
if (currentWordCount) {
|
||||||
currentWordCount.textContent = Number.isInteger(wordCount) ? String(wordCount) : "-";
|
currentWordCount.textContent = Number.isInteger(wordCount) ? String(wordCount) : "-";
|
||||||
}
|
}
|
||||||
|
setText(syncWordCount, Number.isInteger(wordCount) ? String(wordCount) : "-");
|
||||||
};
|
};
|
||||||
|
|
||||||
const setHidden = (element, hidden) => {
|
const setHidden = (element, hidden) => {
|
||||||
@ -127,6 +136,18 @@
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const setSyncStatus = (message) => {
|
||||||
|
setText(syncStatus, message);
|
||||||
|
};
|
||||||
|
|
||||||
|
const setResolvedScene = (sceneId) => {
|
||||||
|
resolvedSceneId = Number.isInteger(sceneId) && sceneId > 0 ? sceneId : null;
|
||||||
|
if (syncSceneProgressButton) {
|
||||||
|
syncSceneProgressButton.disabled = !resolvedSceneId;
|
||||||
|
}
|
||||||
|
setSyncStatus(resolvedSceneId ? "Ready to sync." : "No resolved PlotDirector scene.");
|
||||||
|
};
|
||||||
|
|
||||||
const displayValue = (value) => {
|
const displayValue = (value) => {
|
||||||
if (value === null || value === undefined || value === "") {
|
if (value === null || value === undefined || value === "") {
|
||||||
return "-";
|
return "-";
|
||||||
@ -140,6 +161,8 @@
|
|||||||
setHidden(plotDirectorSceneDetails, true);
|
setHidden(plotDirectorSceneDetails, true);
|
||||||
setHidden(writingBriefBlock, true);
|
setHidden(writingBriefBlock, true);
|
||||||
setHidden(plotDirectorLinkGroups, true);
|
setHidden(plotDirectorLinkGroups, true);
|
||||||
|
setResolvedScene(null);
|
||||||
|
setText(syncPlotDirectorCount, "-");
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderLinkedItems = (container, items) => {
|
const renderLinkedItems = (container, items) => {
|
||||||
@ -170,6 +193,7 @@
|
|||||||
setText(plotDirectorRevisionStatus, displayValue(data?.revisionStatus || data?.revisionStatusName || "Not provided"));
|
setText(plotDirectorRevisionStatus, displayValue(data?.revisionStatus || data?.revisionStatusName || "Not provided"));
|
||||||
setText(plotDirectorEstimatedWords, displayValue(data?.estimatedWords));
|
setText(plotDirectorEstimatedWords, displayValue(data?.estimatedWords));
|
||||||
setText(plotDirectorActualWords, displayValue(data?.actualWords));
|
setText(plotDirectorActualWords, displayValue(data?.actualWords));
|
||||||
|
setText(syncPlotDirectorCount, displayValue(data?.actualWords));
|
||||||
setText(plotDirectorBlockedStatus, data?.blocked ? "Blocked" : "Not blocked");
|
setText(plotDirectorBlockedStatus, data?.blocked ? "Blocked" : "Not blocked");
|
||||||
|
|
||||||
if (data?.blockedReason) {
|
if (data?.blockedReason) {
|
||||||
@ -567,6 +591,7 @@
|
|||||||
structure.currentScene?.title,
|
structure.currentScene?.title,
|
||||||
structure.currentScene?.wordCount
|
structure.currentScene?.wordCount
|
||||||
);
|
);
|
||||||
|
resetPlotDirectorScene(selectedBook() ? "No resolved PlotDirector scene." : "Select a PlotDirector book.");
|
||||||
renderOutline(structure);
|
renderOutline(structure);
|
||||||
setDocumentMessage("");
|
setDocumentMessage("");
|
||||||
setDiagnostics({
|
setDiagnostics({
|
||||||
@ -654,6 +679,48 @@
|
|||||||
body: JSON.stringify(body)
|
body: JSON.stringify(body)
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const patchJson = (url, body) => fetchJson(url, {
|
||||||
|
method: "PATCH",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify(body)
|
||||||
|
});
|
||||||
|
|
||||||
|
const syncSceneProgress = async () => {
|
||||||
|
if (!resolvedSceneId) {
|
||||||
|
setSyncStatus("No resolved PlotDirector scene.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Number.isInteger(detectedSceneWordCount)) {
|
||||||
|
setSyncStatus("Unable to sync progress.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (syncSceneProgressButton) {
|
||||||
|
syncSceneProgressButton.disabled = true;
|
||||||
|
syncSceneProgressButton.textContent = "Syncing...";
|
||||||
|
}
|
||||||
|
|
||||||
|
const lastWorkedOn = new Date().toISOString();
|
||||||
|
try {
|
||||||
|
const response = await patchJson(`/api/word-companion/scenes/${resolvedSceneId}/word-count`, {
|
||||||
|
actualWordCount: detectedSceneWordCount,
|
||||||
|
lastWorkedOn
|
||||||
|
});
|
||||||
|
setText(syncPlotDirectorCount, displayValue(response?.actualWords ?? detectedSceneWordCount));
|
||||||
|
setText(plotDirectorActualWords, displayValue(response?.actualWords ?? detectedSceneWordCount));
|
||||||
|
setText(syncLastSynced, new Date().toLocaleString());
|
||||||
|
setSyncStatus("Progress synced successfully.");
|
||||||
|
} catch {
|
||||||
|
setSyncStatus("Unable to sync progress.");
|
||||||
|
} finally {
|
||||||
|
if (syncSceneProgressButton) {
|
||||||
|
syncSceneProgressButton.disabled = !resolvedSceneId;
|
||||||
|
syncSceneProgressButton.textContent = "Sync Scene Progress";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const refreshPlotDirectorScene = async () => {
|
const refreshPlotDirectorScene = async () => {
|
||||||
const project = selectedProject();
|
const project = selectedProject();
|
||||||
const book = selectedBook();
|
const book = selectedBook();
|
||||||
@ -738,6 +805,7 @@
|
|||||||
}
|
}
|
||||||
setPlotDirectorSceneStatus("Linked to PlotDirector");
|
setPlotDirectorSceneStatus("Linked to PlotDirector");
|
||||||
renderCompanionData(companionData);
|
renderCompanionData(companionData);
|
||||||
|
setResolvedScene(resolvedSceneId);
|
||||||
} catch {
|
} catch {
|
||||||
resetPlotDirectorScene("Unable to load PlotDirector scene data.");
|
resetPlotDirectorScene("Unable to load PlotDirector scene data.");
|
||||||
setResolveDiagnostics({ result: "Error" });
|
setResolveDiagnostics({ result: "Error" });
|
||||||
@ -854,6 +922,7 @@
|
|||||||
|
|
||||||
refreshDocumentButton?.addEventListener("click", refreshDocumentStructure);
|
refreshDocumentButton?.addEventListener("click", refreshDocumentStructure);
|
||||||
refreshPlotDirectorSceneButton?.addEventListener("click", refreshPlotDirectorScene);
|
refreshPlotDirectorSceneButton?.addEventListener("click", refreshPlotDirectorScene);
|
||||||
|
syncSceneProgressButton?.addEventListener("click", syncSceneProgress);
|
||||||
runDiagnosticsButton?.addEventListener("click", runDiagnostics);
|
runDiagnosticsButton?.addEventListener("click", runDiagnostics);
|
||||||
|
|
||||||
if (isAuthenticated) {
|
if (isAuthenticated) {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user