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<WordCompanionSyncManuscriptResponse?> SyncManuscriptAsync(int bookId, int userId, WordCompanionSyncManuscriptRequest 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
|
||||
@ -179,12 +179,12 @@ public sealed class WordCompanionRepository(ISqlConnectionFactory connectionFact
|
||||
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();
|
||||
return await connection.QuerySingleOrDefaultAsync<WordCompanionUpdateWordCountResponse>(
|
||||
"dbo.WordCompanion_Scene_UpdateWordCount",
|
||||
new { SceneID = sceneId, UserID = userId, ActualWords = actualWords },
|
||||
new { SceneID = sceneId, UserID = userId, ActualWords = actualWords, LastWorkedOn = lastWorkedOn },
|
||||
commandType: CommandType.StoredProcedure);
|
||||
}
|
||||
|
||||
|
||||
@ -163,12 +163,15 @@ public sealed class WordCompanionUpdateSceneLinksResponse
|
||||
|
||||
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 int SceneId { get; init; }
|
||||
public int ActualWords { get; init; }
|
||||
public DateTime? LastWorkedOn { get; init; }
|
||||
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)
|
||||
{
|
||||
return sceneId <= 0 || request.ActualWords < 0
|
||||
var actualWords = request.ActualWordCount ?? request.ActualWords;
|
||||
return sceneId <= 0 || actualWords is null or < 0
|
||||
? 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)
|
||||
@ -124,4 +125,3 @@ public sealed class WordCompanionService(IWordCompanionRepository repository, IC
|
||||
|
||||
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>
|
||||
</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>
|
||||
<span>Writing Brief</span>
|
||||
<p data-writing-brief></p>
|
||||
|
||||
@ -197,6 +197,27 @@ body {
|
||||
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 {
|
||||
display: grid;
|
||||
gap: 5px;
|
||||
|
||||
@ -31,6 +31,11 @@
|
||||
const plotDirectorBlockedStatus = document.querySelector("[data-plotdirector-blocked-status]");
|
||||
const plotDirectorBlockedReasonRow = document.querySelector("[data-plotdirector-blocked-reason-row]");
|
||||
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 writingBrief = document.querySelector("[data-writing-brief]");
|
||||
const plotDirectorLinkGroups = document.querySelector("[data-plotdirector-link-groups]");
|
||||
@ -66,6 +71,8 @@
|
||||
let officePlatform = "Unknown";
|
||||
let detectedChapterTitle = "";
|
||||
let detectedSceneTitle = "";
|
||||
let detectedSceneWordCount = null;
|
||||
let resolvedSceneId = null;
|
||||
|
||||
const setOfficeStatus = (message) => {
|
||||
if (officeStatus) {
|
||||
@ -109,6 +116,7 @@
|
||||
const setCurrentStructure = (chapterTitle, sceneTitle, wordCount) => {
|
||||
detectedChapterTitle = chapterTitle || "";
|
||||
detectedSceneTitle = sceneTitle || "";
|
||||
detectedSceneWordCount = Number.isInteger(wordCount) ? wordCount : null;
|
||||
|
||||
if (currentChapter) {
|
||||
currentChapter.textContent = chapterTitle || "Not detected";
|
||||
@ -119,6 +127,7 @@
|
||||
if (currentWordCount) {
|
||||
currentWordCount.textContent = Number.isInteger(wordCount) ? String(wordCount) : "-";
|
||||
}
|
||||
setText(syncWordCount, Number.isInteger(wordCount) ? String(wordCount) : "-");
|
||||
};
|
||||
|
||||
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) => {
|
||||
if (value === null || value === undefined || value === "") {
|
||||
return "-";
|
||||
@ -140,6 +161,8 @@
|
||||
setHidden(plotDirectorSceneDetails, true);
|
||||
setHidden(writingBriefBlock, true);
|
||||
setHidden(plotDirectorLinkGroups, true);
|
||||
setResolvedScene(null);
|
||||
setText(syncPlotDirectorCount, "-");
|
||||
};
|
||||
|
||||
const renderLinkedItems = (container, items) => {
|
||||
@ -170,6 +193,7 @@
|
||||
setText(plotDirectorRevisionStatus, displayValue(data?.revisionStatus || data?.revisionStatusName || "Not provided"));
|
||||
setText(plotDirectorEstimatedWords, displayValue(data?.estimatedWords));
|
||||
setText(plotDirectorActualWords, displayValue(data?.actualWords));
|
||||
setText(syncPlotDirectorCount, displayValue(data?.actualWords));
|
||||
setText(plotDirectorBlockedStatus, data?.blocked ? "Blocked" : "Not blocked");
|
||||
|
||||
if (data?.blockedReason) {
|
||||
@ -567,6 +591,7 @@
|
||||
structure.currentScene?.title,
|
||||
structure.currentScene?.wordCount
|
||||
);
|
||||
resetPlotDirectorScene(selectedBook() ? "No resolved PlotDirector scene." : "Select a PlotDirector book.");
|
||||
renderOutline(structure);
|
||||
setDocumentMessage("");
|
||||
setDiagnostics({
|
||||
@ -654,6 +679,48 @@
|
||||
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 project = selectedProject();
|
||||
const book = selectedBook();
|
||||
@ -738,6 +805,7 @@
|
||||
}
|
||||
setPlotDirectorSceneStatus("Linked to PlotDirector");
|
||||
renderCompanionData(companionData);
|
||||
setResolvedScene(resolvedSceneId);
|
||||
} catch {
|
||||
resetPlotDirectorScene("Unable to load PlotDirector scene data.");
|
||||
setResolveDiagnostics({ result: "Error" });
|
||||
@ -854,6 +922,7 @@
|
||||
|
||||
refreshDocumentButton?.addEventListener("click", refreshDocumentStructure);
|
||||
refreshPlotDirectorSceneButton?.addEventListener("click", refreshPlotDirectorScene);
|
||||
syncSceneProgressButton?.addEventListener("click", syncSceneProgress);
|
||||
runDiagnosticsButton?.addEventListener("click", runDiagnostics);
|
||||
|
||||
if (isAuthenticated) {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user