Phase 10H.2: Fix Word Companion Scene Links to Use Real PlotDirector Scene Data
This commit is contained in:
parent
825a66f66a
commit
6dd46c2632
@ -108,6 +108,7 @@ public sealed class WordCompanionRepository(ISqlConnectionFactory connectionFact
|
||||
WritingBrief = scene.WritingBrief,
|
||||
Blocked = scene.Blocked,
|
||||
BlockedReason = scene.BlockedReason,
|
||||
PrimaryLocationId = scene.PrimaryLocationId,
|
||||
Characters = (await result.ReadAsync<WordCompanionLinkItemDto>()).ToList(),
|
||||
Assets = (await result.ReadAsync<WordCompanionLinkItemDto>()).ToList(),
|
||||
Locations = (await result.ReadAsync<WordCompanionLinkItemDto>()).ToList()
|
||||
@ -174,7 +175,8 @@ public sealed class WordCompanionRepository(ISqlConnectionFactory connectionFact
|
||||
UserID = userId,
|
||||
CharacterIds = ToCsv(request.CharacterIds),
|
||||
AssetIds = ToCsv(request.AssetIds),
|
||||
LocationIds = ToCsv(request.LocationIds)
|
||||
LocationIds = ToCsv(request.LocationIds),
|
||||
request.PrimaryLocationId
|
||||
},
|
||||
commandType: CommandType.StoredProcedure);
|
||||
}
|
||||
|
||||
@ -87,6 +87,7 @@ public sealed class WordCompanionSceneCompanionResponse
|
||||
public string? WritingBrief { get; init; }
|
||||
public bool Blocked { get; init; }
|
||||
public string? BlockedReason { get; init; }
|
||||
public int? PrimaryLocationId { get; init; }
|
||||
public IReadOnlyList<WordCompanionLinkItemDto> Characters { get; init; } = [];
|
||||
public IReadOnlyList<WordCompanionLinkItemDto> Assets { get; init; } = [];
|
||||
public IReadOnlyList<WordCompanionLinkItemDto> Locations { get; init; } = [];
|
||||
@ -150,6 +151,7 @@ public sealed class WordCompanionUpdateSceneLinksRequest
|
||||
public IReadOnlyList<int> CharacterIds { get; set; } = [];
|
||||
public IReadOnlyList<int> AssetIds { get; set; } = [];
|
||||
public IReadOnlyList<int> LocationIds { get; set; } = [];
|
||||
public int? PrimaryLocationId { get; set; }
|
||||
}
|
||||
|
||||
public sealed class WordCompanionUpdateSceneLinksResponse
|
||||
|
||||
@ -97,7 +97,11 @@ public sealed class WordCompanionService(IWordCompanionRepository repository, IC
|
||||
|
||||
public Task<WordCompanionUpdateSceneLinksResponse?> UpdateSceneLinksAsync(int sceneId, WordCompanionUpdateSceneLinksRequest request)
|
||||
{
|
||||
if (sceneId <= 0 || HasInvalidIds(request.CharacterIds) || HasInvalidIds(request.AssetIds) || HasInvalidIds(request.LocationIds))
|
||||
if (sceneId <= 0
|
||||
|| request.PrimaryLocationId is <= 0
|
||||
|| HasInvalidIds(request.CharacterIds)
|
||||
|| HasInvalidIds(request.AssetIds)
|
||||
|| HasInvalidIds(request.LocationIds))
|
||||
{
|
||||
return Task.FromResult<WordCompanionUpdateSceneLinksResponse?>(null);
|
||||
}
|
||||
|
||||
@ -0,0 +1,206 @@
|
||||
SET ANSI_NULLS ON;
|
||||
GO
|
||||
SET QUOTED_IDENTIFIER ON;
|
||||
GO
|
||||
|
||||
CREATE OR ALTER PROCEDURE dbo.WordCompanion_Scene_Companion
|
||||
@SceneID int,
|
||||
@UserID int
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON;
|
||||
|
||||
DECLARE @ProjectID int;
|
||||
DECLARE @BookID int;
|
||||
|
||||
SELECT @ProjectID = b.ProjectID, @BookID = b.BookID
|
||||
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;
|
||||
|
||||
IF @ProjectID IS NULL
|
||||
RETURN;
|
||||
|
||||
SELECT s.SceneID, c.ChapterID, c.ChapterTitle, s.SceneTitle,
|
||||
sw.ActualWordCount AS ActualWords,
|
||||
sw.EstimatedWordCount AS EstimatedWords,
|
||||
s.ScenePurposeNotes AS WritingBrief,
|
||||
COALESCE(sw.IsBlocked, 0) AS Blocked,
|
||||
sw.BlockedReason,
|
||||
s.PrimaryLocationID AS PrimaryLocationId
|
||||
FROM dbo.Scenes s
|
||||
INNER JOIN dbo.Chapters c ON c.ChapterID = s.ChapterID
|
||||
LEFT JOIN dbo.SceneWorkflow sw ON sw.SceneID = s.SceneID
|
||||
WHERE s.SceneID = @SceneID;
|
||||
|
||||
SELECT ch.CharacterID, ch.CharacterName AS Name,
|
||||
CAST(CASE WHEN EXISTS (SELECT 1 FROM dbo.SceneCharacters sc WHERE sc.SceneID = @SceneID AND sc.CharacterID = ch.CharacterID) THEN 1 ELSE 0 END AS bit) AS Linked
|
||||
FROM dbo.Characters ch
|
||||
WHERE ch.ProjectID = @ProjectID
|
||||
AND ch.IsArchived = 0
|
||||
ORDER BY ch.CharacterName, ch.CharacterID;
|
||||
|
||||
SELECT sa.StoryAssetID AS AssetId, sa.AssetName AS Name,
|
||||
CAST(CASE WHEN EXISTS (SELECT 1 FROM dbo.AssetEvents ae WHERE ae.SceneID = @SceneID AND ae.StoryAssetID = sa.StoryAssetID)
|
||||
OR EXISTS (SELECT 1 FROM dbo.SceneAssetLocations sal WHERE sal.SceneID = @SceneID AND sal.StoryAssetID = sa.StoryAssetID)
|
||||
THEN 1 ELSE 0 END AS bit) AS Linked
|
||||
FROM dbo.StoryAssets sa
|
||||
WHERE sa.ProjectID = @ProjectID
|
||||
AND sa.IsArchived = 0
|
||||
ORDER BY sa.AssetName, sa.StoryAssetID;
|
||||
|
||||
SELECT l.LocationID, l.LocationName AS Name,
|
||||
CAST(CASE WHEN EXISTS (SELECT 1 FROM dbo.Scenes s WHERE s.SceneID = @SceneID AND s.PrimaryLocationID = l.LocationID) THEN 1 ELSE 0 END AS bit) AS Linked
|
||||
FROM dbo.Locations l
|
||||
WHERE l.ProjectID = @ProjectID
|
||||
AND l.IsArchived = 0
|
||||
ORDER BY l.LocationName, l.LocationID;
|
||||
END;
|
||||
GO
|
||||
|
||||
CREATE OR ALTER PROCEDURE dbo.WordCompanion_SceneLinks_Update
|
||||
@SceneID int,
|
||||
@UserID int,
|
||||
@CharacterIds nvarchar(max) = NULL,
|
||||
@AssetIds nvarchar(max) = NULL,
|
||||
@LocationIds nvarchar(max) = NULL,
|
||||
@PrimaryLocationId int = NULL
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON;
|
||||
|
||||
DECLARE @ProjectID int;
|
||||
DECLARE @UsedAssetEventTypeID int;
|
||||
SELECT @ProjectID = b.ProjectID
|
||||
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;
|
||||
|
||||
IF @ProjectID IS NULL
|
||||
RETURN;
|
||||
|
||||
SELECT TOP (1) @UsedAssetEventTypeID = AssetEventTypeID
|
||||
FROM dbo.AssetEventTypes
|
||||
WHERE TypeName = N'Used' AND IsActive = 1
|
||||
ORDER BY SortOrder, AssetEventTypeID;
|
||||
|
||||
IF @UsedAssetEventTypeID IS NULL
|
||||
SELECT TOP (1) @UsedAssetEventTypeID = AssetEventTypeID
|
||||
FROM dbo.AssetEventTypes
|
||||
WHERE IsActive = 1
|
||||
ORDER BY SortOrder, AssetEventTypeID;
|
||||
|
||||
IF @UsedAssetEventTypeID IS NULL
|
||||
RETURN;
|
||||
|
||||
DECLARE @Characters TABLE (ID int PRIMARY KEY);
|
||||
DECLARE @Assets TABLE (ID int PRIMARY KEY);
|
||||
|
||||
INSERT @Characters (ID)
|
||||
SELECT DISTINCT TRY_CONVERT(int, LTRIM(RTRIM(value)))
|
||||
FROM STRING_SPLIT(ISNULL(@CharacterIds, N''), N',')
|
||||
WHERE NULLIF(LTRIM(RTRIM(value)), N'') IS NOT NULL
|
||||
AND TRY_CONVERT(int, LTRIM(RTRIM(value))) IS NOT NULL;
|
||||
|
||||
INSERT @Assets (ID)
|
||||
SELECT DISTINCT TRY_CONVERT(int, LTRIM(RTRIM(value)))
|
||||
FROM STRING_SPLIT(ISNULL(@AssetIds, N''), N',')
|
||||
WHERE NULLIF(LTRIM(RTRIM(value)), N'') IS NOT NULL
|
||||
AND TRY_CONVERT(int, LTRIM(RTRIM(value))) IS NOT NULL;
|
||||
|
||||
IF EXISTS (SELECT 1 FROM @Characters requested WHERE requested.ID <= 0 OR NOT EXISTS (SELECT 1 FROM dbo.Characters c WHERE c.CharacterID = requested.ID AND c.ProjectID = @ProjectID AND c.IsArchived = 0))
|
||||
RETURN;
|
||||
IF EXISTS (SELECT 1 FROM @Assets requested WHERE requested.ID <= 0 OR NOT EXISTS (SELECT 1 FROM dbo.StoryAssets sa WHERE sa.StoryAssetID = requested.ID AND sa.ProjectID = @ProjectID AND sa.IsArchived = 0))
|
||||
RETURN;
|
||||
IF @PrimaryLocationId IS NOT NULL
|
||||
AND NOT EXISTS (SELECT 1 FROM dbo.Locations l WHERE l.LocationID = @PrimaryLocationId AND l.ProjectID = @ProjectID AND l.IsArchived = 0)
|
||||
RETURN;
|
||||
|
||||
UPDATE dbo.Scenes
|
||||
SET PrimaryLocationID = @PrimaryLocationId,
|
||||
UpdatedDate = SYSUTCDATETIME()
|
||||
WHERE SceneID = @SceneID;
|
||||
|
||||
DELETE sc
|
||||
FROM dbo.SceneCharacters sc
|
||||
WHERE sc.SceneID = @SceneID
|
||||
AND NOT EXISTS (SELECT 1 FROM @Characters requested WHERE requested.ID = sc.CharacterID)
|
||||
AND sc.RoleInSceneTypeID IS NULL
|
||||
AND sc.PresenceTypeID IS NULL
|
||||
AND sc.LocationID IS NULL
|
||||
AND sc.EntryLocationID IS NULL
|
||||
AND sc.ExitLocationID IS NULL
|
||||
AND sc.AppearanceNotes IS NULL
|
||||
AND sc.OutfitDescription IS NULL
|
||||
AND sc.PhysicalCondition IS NULL
|
||||
AND sc.EmotionalState IS NULL
|
||||
AND sc.KnowledgeNotes IS NULL;
|
||||
|
||||
INSERT dbo.SceneCharacters (SceneID, CharacterID)
|
||||
SELECT @SceneID, requested.ID
|
||||
FROM @Characters requested
|
||||
WHERE NOT EXISTS (SELECT 1 FROM dbo.SceneCharacters sc WHERE sc.SceneID = @SceneID AND sc.CharacterID = requested.ID);
|
||||
|
||||
DELETE ae
|
||||
FROM dbo.AssetEvents ae
|
||||
WHERE ae.SceneID = @SceneID
|
||||
AND ae.AssetEventTypeID = @UsedAssetEventTypeID
|
||||
AND ae.EventTitle = N'Used'
|
||||
AND ae.EventDescription IS NULL
|
||||
AND ae.FromStateID IS NULL
|
||||
AND ae.ToStateID IS NULL
|
||||
AND NOT EXISTS (SELECT 1 FROM @Assets requested WHERE requested.ID = ae.StoryAssetID);
|
||||
|
||||
DELETE sal
|
||||
FROM dbo.SceneAssetLocations sal
|
||||
WHERE sal.SceneID = @SceneID
|
||||
AND sal.Description IS NULL
|
||||
AND NOT EXISTS (SELECT 1 FROM @Assets requested WHERE requested.ID = sal.StoryAssetID);
|
||||
|
||||
INSERT dbo.AssetEvents (StoryAssetID, SceneID, AssetEventTypeID, FromStateID, ToStateID, EventTitle, EventDescription)
|
||||
SELECT requested.ID, @SceneID, @UsedAssetEventTypeID, NULL, NULL, N'Used', NULL
|
||||
FROM @Assets requested
|
||||
WHERE NOT EXISTS (SELECT 1 FROM dbo.AssetEvents ae WHERE ae.SceneID = @SceneID AND ae.StoryAssetID = requested.ID)
|
||||
AND NOT EXISTS (SELECT 1 FROM dbo.SceneAssetLocations sal WHERE sal.SceneID = @SceneID AND sal.StoryAssetID = requested.ID);
|
||||
|
||||
IF @PrimaryLocationId IS NOT NULL
|
||||
BEGIN
|
||||
INSERT dbo.SceneAssetLocations (SceneID, StoryAssetID, LocationID, Description)
|
||||
SELECT @SceneID, requested.ID, @PrimaryLocationId, NULL
|
||||
FROM @Assets requested
|
||||
WHERE NOT EXISTS (SELECT 1 FROM dbo.SceneAssetLocations sal WHERE sal.SceneID = @SceneID AND sal.StoryAssetID = requested.ID);
|
||||
|
||||
UPDATE sal
|
||||
SET LocationID = @PrimaryLocationId,
|
||||
UpdatedDate = SYSUTCDATETIME()
|
||||
FROM dbo.SceneAssetLocations sal
|
||||
INNER JOIN @Assets requested ON requested.ID = sal.StoryAssetID
|
||||
WHERE sal.SceneID = @SceneID
|
||||
AND sal.Description IS NULL;
|
||||
END;
|
||||
|
||||
SELECT @SceneID AS SceneId,
|
||||
(SELECT COUNT(*) FROM @Characters) AS CharactersUpdated,
|
||||
(SELECT COUNT(*) FROM @Assets) AS AssetsUpdated,
|
||||
CASE WHEN @PrimaryLocationId IS NULL THEN 0 ELSE 1 END AS LocationsUpdated,
|
||||
N'Scene links updated.' AS Message;
|
||||
END;
|
||||
GO
|
||||
@ -124,8 +124,10 @@
|
||||
<div class="word-companion-check-list" data-asset-chips></div>
|
||||
</div>
|
||||
<div>
|
||||
<span>Locations</span>
|
||||
<div class="word-companion-check-list" data-location-chips></div>
|
||||
<label for="word-companion-primary-location">Primary Location</label>
|
||||
<select id="word-companion-primary-location" data-primary-location-select disabled>
|
||||
<option value="">None</option>
|
||||
</select>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
@ -43,6 +43,7 @@
|
||||
const characterChips = document.querySelector("[data-character-chips]");
|
||||
const assetChips = document.querySelector("[data-asset-chips]");
|
||||
const locationChips = document.querySelector("[data-location-chips]");
|
||||
const primaryLocationSelect = document.querySelector("[data-primary-location-select]");
|
||||
const saveSceneLinksButton = document.querySelector("[data-save-scene-links]");
|
||||
const sceneLinksStatus = document.querySelector("[data-scene-links-status]");
|
||||
const resolveProjectId = document.querySelector("[data-resolve-project-id]");
|
||||
@ -80,7 +81,8 @@
|
||||
let currentSceneLinks = {
|
||||
characters: [],
|
||||
assets: [],
|
||||
locations: []
|
||||
locations: [],
|
||||
primaryLocationId: null
|
||||
};
|
||||
|
||||
const setOfficeStatus = (message) => {
|
||||
@ -187,10 +189,10 @@
|
||||
setHidden(plotDirectorSceneDetails, true);
|
||||
setHidden(writingBriefBlock, true);
|
||||
setHidden(plotDirectorLinkGroups, true);
|
||||
currentSceneLinks = { characters: [], assets: [], locations: [] };
|
||||
currentSceneLinks = { characters: [], assets: [], locations: [], primaryLocationId: null };
|
||||
renderSceneLinkList(characterChips, [], "character");
|
||||
renderSceneLinkList(assetChips, [], "asset");
|
||||
renderSceneLinkList(locationChips, [], "location");
|
||||
renderPrimaryLocationOptions([], null, false);
|
||||
setResolvedScene(null);
|
||||
setText(syncPlotDirectorCount, "-");
|
||||
setText(plotDirectorSceneTitle, "-");
|
||||
@ -252,6 +254,34 @@
|
||||
}
|
||||
};
|
||||
|
||||
const renderPrimaryLocationOptions = (items, selectedLocationId, enabled = false) => {
|
||||
if (!primaryLocationSelect) {
|
||||
return;
|
||||
}
|
||||
|
||||
primaryLocationSelect.innerHTML = "";
|
||||
const none = document.createElement("option");
|
||||
none.value = "";
|
||||
none.textContent = "None";
|
||||
primaryLocationSelect.append(none);
|
||||
|
||||
const selectedId = Number.parseInt(selectedLocationId || "", 10);
|
||||
for (const item of Array.isArray(items) ? items : []) {
|
||||
const id = Number.parseInt(linkItemId(item, "location"), 10);
|
||||
if (!Number.isInteger(id) || id <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const option = document.createElement("option");
|
||||
option.value = String(id);
|
||||
option.textContent = item.name || "Unnamed";
|
||||
option.selected = id === selectedId;
|
||||
primaryLocationSelect.append(option);
|
||||
}
|
||||
|
||||
primaryLocationSelect.disabled = !enabled;
|
||||
};
|
||||
|
||||
const renderCompanionData = (data) => {
|
||||
setText(plotDirectorSceneTitle, displayValue(data?.sceneTitle));
|
||||
setText(plotDirectorRevisionStatus, displayValue(data?.revisionStatus || data?.revisionStatusName || "Not provided"));
|
||||
@ -275,11 +305,12 @@
|
||||
currentSceneLinks = {
|
||||
characters: Array.isArray(data?.characters) ? data.characters : [],
|
||||
assets: Array.isArray(data?.assets) ? data.assets : [],
|
||||
locations: Array.isArray(data?.locations) ? data.locations : []
|
||||
locations: Array.isArray(data?.locations) ? data.locations : [],
|
||||
primaryLocationId: data?.primaryLocationId ?? null
|
||||
};
|
||||
renderSceneLinkList(characterChips, currentSceneLinks.characters, "character", !!resolvedSceneId);
|
||||
renderSceneLinkList(assetChips, currentSceneLinks.assets, "asset", !!resolvedSceneId);
|
||||
renderSceneLinkList(locationChips, currentSceneLinks.locations, "location", !!resolvedSceneId);
|
||||
renderPrimaryLocationOptions(currentSceneLinks.locations, currentSceneLinks.primaryLocationId, !!resolvedSceneId);
|
||||
setSceneLinksStatus(resolvedSceneId ? "Ready to save." : "No resolved PlotDirector scene.");
|
||||
setHidden(plotDirectorSceneDetails, false);
|
||||
setHidden(writingBriefBlock, false);
|
||||
@ -779,15 +810,20 @@
|
||||
.filter((id) => Number.isInteger(id) && id > 0);
|
||||
};
|
||||
|
||||
const selectedPrimaryLocationId = () => {
|
||||
const id = Number.parseInt(primaryLocationSelect?.value || "", 10);
|
||||
return Number.isInteger(id) && id > 0 ? id : null;
|
||||
};
|
||||
|
||||
const updateCurrentSceneLinkState = () => {
|
||||
const selectedCharacters = new Set(selectedLinkIds(characterChips));
|
||||
const selectedAssets = new Set(selectedLinkIds(assetChips));
|
||||
const selectedLocations = new Set(selectedLinkIds(locationChips));
|
||||
|
||||
currentSceneLinks = {
|
||||
characters: currentSceneLinks.characters.map((item) => ({ ...item, linked: selectedCharacters.has(Number.parseInt(linkItemId(item, "character"), 10)) })),
|
||||
assets: currentSceneLinks.assets.map((item) => ({ ...item, linked: selectedAssets.has(Number.parseInt(linkItemId(item, "asset"), 10)) })),
|
||||
locations: currentSceneLinks.locations.map((item) => ({ ...item, linked: selectedLocations.has(Number.parseInt(linkItemId(item, "location"), 10)) }))
|
||||
locations: currentSceneLinks.locations,
|
||||
primaryLocationId: selectedPrimaryLocationId()
|
||||
};
|
||||
};
|
||||
|
||||
@ -806,7 +842,8 @@
|
||||
await putJson(`/api/word-companion/scenes/${resolvedSceneId}/links`, {
|
||||
characterIds: selectedLinkIds(characterChips),
|
||||
assetIds: selectedLinkIds(assetChips),
|
||||
locationIds: selectedLinkIds(locationChips)
|
||||
locationIds: [],
|
||||
primaryLocationId: selectedPrimaryLocationId()
|
||||
});
|
||||
updateCurrentSceneLinkState();
|
||||
setSceneLinksStatus("Scene links saved successfully.");
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user