Phase 6A Plot Lines

This commit is contained in:
Nick Beckley 2026-06-11 19:41:03 +01:00
parent 9de7f099cb
commit fb5352ab8f
8 changed files with 1152 additions and 15 deletions

View File

@ -381,6 +381,7 @@ public sealed class ImportRepository(ISqlConnectionFactory connectionFactory) :
BookID = (int?)null, BookID = (int?)null,
PlotLineName = Clean(DisplayName(plotLine.DisplayName, plotLine.Name)), PlotLineName = Clean(DisplayName(plotLine.DisplayName, plotLine.Name)),
PlotLineTypeID = ResolvePlotLineTypeId(lookupIds, plotLine.Type), PlotLineTypeID = ResolvePlotLineTypeId(lookupIds, plotLine.Type),
PlotImportanceID = ResolvePlotImportanceId(lookupIds, plotLine),
Description = CombineText( Description = CombineText(
plotLine.Description, plotLine.Description,
MetadataLine("Type", plotLine.Type), MetadataLine("Type", plotLine.Type),
@ -388,9 +389,9 @@ public sealed class ImportRepository(ISqlConnectionFactory connectionFactory) :
plotLine.Notes, plotLine.Notes,
ImportMarker), ImportMarker),
ParentPlotLineID = (int?)null, ParentPlotLineID = (int?)null,
EmergesFromPlotLineID = (int?)null,
SortOrder = (int?)null, SortOrder = (int?)null,
Colour = "#2f6f63", Colour = "#2f6f63",
IsMainPlot = string.Equals(plotLine.Type?.Trim(), "Main Plot", StringComparison.OrdinalIgnoreCase),
IsVisibleOnTimeline = true IsVisibleOnTimeline = true
}, },
transaction, transaction,
@ -685,6 +686,9 @@ public sealed class ImportRepository(ISqlConnectionFactory connectionFactory) :
var plotLineTypes = (await connection.QueryAsync<ImportLookupRow>( var plotLineTypes = (await connection.QueryAsync<ImportLookupRow>(
"SELECT PlotLineTypeID AS LookupID, TypeName FROM dbo.PlotLineTypes WHERE IsActive = 1;", "SELECT PlotLineTypeID AS LookupID, TypeName FROM dbo.PlotLineTypes WHERE IsActive = 1;",
transaction: transaction)).ToDictionary(x => x.TypeName, x => x.LookupID, StringComparer.OrdinalIgnoreCase); transaction: transaction)).ToDictionary(x => x.TypeName, x => x.LookupID, StringComparer.OrdinalIgnoreCase);
var plotImportance = (await connection.QueryAsync<ImportLookupRow>(
"SELECT PlotImportanceID AS LookupID, ImportanceName AS TypeName FROM dbo.PlotImportance WHERE IsActive = 1;",
transaction: transaction)).ToDictionary(x => x.TypeName, x => x.LookupID, StringComparer.OrdinalIgnoreCase);
var threadTypes = (await connection.QueryAsync<ImportLookupRow>( var threadTypes = (await connection.QueryAsync<ImportLookupRow>(
"SELECT ThreadTypeID AS LookupID, TypeName FROM dbo.ThreadTypes WHERE IsActive = 1;", "SELECT ThreadTypeID AS LookupID, TypeName FROM dbo.ThreadTypes WHERE IsActive = 1;",
transaction: transaction)).ToDictionary(x => x.TypeName, x => x.LookupID, StringComparer.OrdinalIgnoreCase); transaction: transaction)).ToDictionary(x => x.TypeName, x => x.LookupID, StringComparer.OrdinalIgnoreCase);
@ -726,6 +730,7 @@ public sealed class ImportRepository(ISqlConnectionFactory connectionFactory) :
roleTypes, roleTypes,
presenceTypes, presenceTypes,
plotLineTypes, plotLineTypes,
plotImportance,
threadTypes, threadTypes,
threadStatuses, threadStatuses,
threadEventTypes, threadEventTypes,
@ -1111,6 +1116,19 @@ public sealed class ImportRepository(ISqlConnectionFactory connectionFactory) :
: lookupIds.PlotLineTypeIds.Values.First(); : lookupIds.PlotLineTypeIds.Values.First();
} }
private static int ResolvePlotImportanceId(ImportLookupIds lookupIds, ImportPlotLineDto plotLine)
{
if (string.Equals(plotLine.Type?.Trim(), "Main Plot", StringComparison.OrdinalIgnoreCase)
&& lookupIds.PlotImportanceIds.TryGetValue("Primary", out var primaryId))
{
return primaryId;
}
return lookupIds.PlotImportanceIds.TryGetValue("Secondary", out var secondaryId)
? secondaryId
: lookupIds.PlotImportanceIds.Values.First();
}
private static int ResolveThreadTypeId(ImportLookupIds lookupIds, string? type) private static int ResolveThreadTypeId(ImportLookupIds lookupIds, string? type)
{ {
if (!string.IsNullOrWhiteSpace(type) && lookupIds.ThreadTypeIds.TryGetValue(type.Trim(), out var id)) if (!string.IsNullOrWhiteSpace(type) && lookupIds.ThreadTypeIds.TryGetValue(type.Trim(), out var id))
@ -1387,6 +1405,7 @@ public sealed class ImportRepository(ISqlConnectionFactory connectionFactory) :
IReadOnlyDictionary<string, int> RoleTypeIds, IReadOnlyDictionary<string, int> RoleTypeIds,
IReadOnlyDictionary<string, int> PresenceTypeIds, IReadOnlyDictionary<string, int> PresenceTypeIds,
IReadOnlyDictionary<string, int> PlotLineTypeIds, IReadOnlyDictionary<string, int> PlotLineTypeIds,
IReadOnlyDictionary<string, int> PlotImportanceIds,
IReadOnlyDictionary<string, int> ThreadTypeIds, IReadOnlyDictionary<string, int> ThreadTypeIds,
IReadOnlyDictionary<string, int> ThreadStatusIds, IReadOnlyDictionary<string, int> ThreadStatusIds,
IReadOnlyDictionary<string, int> ThreadEventTypeIds, IReadOnlyDictionary<string, int> ThreadEventTypeIds,

View File

@ -1850,9 +1850,11 @@ public sealed class PlotRepository(ISqlConnectionFactory connectionFactory) : IP
return new PlotLookupData return new PlotLookupData
{ {
PlotLineTypes = (await result.ReadAsync<PlotLineType>()).ToList(), PlotLineTypes = (await result.ReadAsync<PlotLineType>()).ToList(),
PlotImportance = (await result.ReadAsync<PlotImportance>()).ToList(),
ThreadTypes = (await result.ReadAsync<ThreadType>()).ToList(), ThreadTypes = (await result.ReadAsync<ThreadType>()).ToList(),
ThreadStatuses = (await result.ReadAsync<ThreadStatus>()).ToList(), ThreadStatuses = (await result.ReadAsync<ThreadStatus>()).ToList(),
ThreadEventTypes = (await result.ReadAsync<ThreadEventType>()).ToList() ThreadEventTypes = (await result.ReadAsync<ThreadEventType>()).ToList(),
PlotEventTypes = (await result.ReadAsync<PlotEventType>()).ToList()
}; };
} }
@ -1881,11 +1883,12 @@ public sealed class PlotRepository(ISqlConnectionFactory connectionFactory) : IP
plotLine.BookID, plotLine.BookID,
plotLine.PlotLineName, plotLine.PlotLineName,
plotLine.PlotLineTypeID, plotLine.PlotLineTypeID,
plotLine.PlotImportanceID,
plotLine.Description, plotLine.Description,
plotLine.ParentPlotLineID, plotLine.ParentPlotLineID,
plotLine.EmergesFromPlotLineID,
plotLine.SortOrder, plotLine.SortOrder,
plotLine.Colour, plotLine.Colour,
plotLine.IsMainPlot,
plotLine.IsVisibleOnTimeline plotLine.IsVisibleOnTimeline
}, },
commandType: CommandType.StoredProcedure); commandType: CommandType.StoredProcedure);
@ -1969,6 +1972,8 @@ public sealed class PlotRepository(ISqlConnectionFactory connectionFactory) : IP
threadEvent.PlotThreadID, threadEvent.PlotThreadID,
threadEvent.SceneID, threadEvent.SceneID,
threadEvent.EventTypeID, threadEvent.EventTypeID,
threadEvent.PlotEventTypeID,
threadEvent.TargetPlotLineID,
threadEvent.EventTitle, threadEvent.EventTitle,
threadEvent.EventDescription threadEvent.EventDescription
}, },

View File

@ -361,6 +361,14 @@ public sealed class PlotLineType
public bool IsActive { get; set; } public bool IsActive { get; set; }
} }
public sealed class PlotImportance
{
public int PlotImportanceID { get; set; }
public string ImportanceName { get; set; } = string.Empty;
public int SortOrder { get; set; }
public bool IsActive { get; set; }
}
public sealed class PlotLineItem public sealed class PlotLineItem
{ {
public int PlotLineID { get; set; } public int PlotLineID { get; set; }
@ -370,12 +378,15 @@ public sealed class PlotLineItem
public string PlotLineName { get; set; } = string.Empty; public string PlotLineName { get; set; } = string.Empty;
public int PlotLineTypeID { get; set; } public int PlotLineTypeID { get; set; }
public string PlotLineTypeName { get; set; } = string.Empty; public string PlotLineTypeName { get; set; } = string.Empty;
public int PlotImportanceID { get; set; } = 2;
public string PlotImportanceName { get; set; } = "Secondary";
public string? Description { get; set; } public string? Description { get; set; }
public int? ParentPlotLineID { get; set; } public int? ParentPlotLineID { get; set; }
public string? ParentPlotLineName { get; set; } public string? ParentPlotLineName { get; set; }
public int? EmergesFromPlotLineID { get; set; }
public string? EmergesFromPlotLineName { get; set; }
public int SortOrder { get; set; } public int SortOrder { get; set; }
public string Colour { get; set; } = "#2f6f63"; public string Colour { get; set; } = "#2f6f63";
public bool IsMainPlot { get; set; }
public bool IsVisibleOnTimeline { get; set; } = true; public bool IsVisibleOnTimeline { get; set; } = true;
public DateTime CreatedDate { get; set; } public DateTime CreatedDate { get; set; }
public DateTime UpdatedDate { get; set; } public DateTime UpdatedDate { get; set; }
@ -434,6 +445,14 @@ public sealed class ThreadEventType
public bool IsActive { get; set; } public bool IsActive { get; set; }
} }
public sealed class PlotEventType
{
public int PlotEventTypeID { get; set; }
public string TypeName { get; set; } = string.Empty;
public int SortOrder { get; set; }
public bool IsActive { get; set; }
}
public sealed class ThreadEvent public sealed class ThreadEvent
{ {
public int ThreadEventID { get; set; } public int ThreadEventID { get; set; }
@ -446,12 +465,23 @@ public sealed class ThreadEvent
public int EventTypeID { get; set; } public int EventTypeID { get; set; }
public string EventTypeName { get; set; } = string.Empty; public string EventTypeName { get; set; } = string.Empty;
public string MarkerText { get; set; } = string.Empty; public string MarkerText { get; set; } = string.Empty;
public int PlotEventTypeID { get; set; } = 2;
public string PlotEventTypeName { get; set; } = "Progress";
public int? TargetPlotLineID { get; set; }
public string? TargetPlotLineName { get; set; }
public string EventTitle { get; set; } = string.Empty; public string EventTitle { get; set; } = string.Empty;
public string? EventDescription { get; set; } public string? EventDescription { get; set; }
public DateTime CreatedDate { get; set; } public DateTime CreatedDate { get; set; }
public DateTime UpdatedDate { get; set; } public DateTime UpdatedDate { get; set; }
} }
public sealed class PlotEventTargetPlotLine
{
public int PlotEventID { get; set; }
public int PlotLineID { get; set; }
public DateTime CreatedDate { get; set; }
}
public sealed class SceneOption public sealed class SceneOption
{ {
public int SceneID { get; set; } public int SceneID { get; set; }
@ -465,9 +495,11 @@ public sealed class SceneOption
public sealed class PlotLookupData public sealed class PlotLookupData
{ {
public IReadOnlyList<PlotLineType> PlotLineTypes { get; init; } = []; public IReadOnlyList<PlotLineType> PlotLineTypes { get; init; } = [];
public IReadOnlyList<PlotImportance> PlotImportance { get; init; } = [];
public IReadOnlyList<ThreadType> ThreadTypes { get; init; } = []; public IReadOnlyList<ThreadType> ThreadTypes { get; init; } = [];
public IReadOnlyList<ThreadStatus> ThreadStatuses { get; init; } = []; public IReadOnlyList<ThreadStatus> ThreadStatuses { get; init; } = [];
public IReadOnlyList<ThreadEventType> ThreadEventTypes { get; init; } = []; public IReadOnlyList<ThreadEventType> ThreadEventTypes { get; init; } = [];
public IReadOnlyList<PlotEventType> PlotEventTypes { get; init; } = [];
} }
public sealed class AssetKind public sealed class AssetKind

View File

@ -2058,6 +2058,8 @@ public sealed class PlotService(IProjectRepository projects, IBookRepository boo
SortOrder = (plotLines.Count + 1) * 10, SortOrder = (plotLines.Count + 1) * 10,
Colour = "#2f6f63", Colour = "#2f6f63",
IsVisibleOnTimeline = true, IsVisibleOnTimeline = true,
PlotImportanceID = lookupData.PlotImportance.FirstOrDefault(x => x.ImportanceName == "Secondary")?.PlotImportanceID
?? lookupData.PlotImportance.First().PlotImportanceID,
PlotLineTypeID = lookupData.PlotLineTypes.FirstOrDefault(x => x.TypeName == "Side Plot")?.PlotLineTypeID PlotLineTypeID = lookupData.PlotLineTypes.FirstOrDefault(x => x.TypeName == "Side Plot")?.PlotLineTypeID
?? lookupData.PlotLineTypes.First().PlotLineTypeID ?? lookupData.PlotLineTypes.First().PlotLineTypeID
}, lookupData, plotLines); }, lookupData, plotLines);
@ -2086,11 +2088,12 @@ public sealed class PlotService(IProjectRepository projects, IBookRepository boo
BookID = plotLine.BookID, BookID = plotLine.BookID,
PlotLineName = plotLine.PlotLineName, PlotLineName = plotLine.PlotLineName,
PlotLineTypeID = plotLine.PlotLineTypeID, PlotLineTypeID = plotLine.PlotLineTypeID,
PlotImportanceID = plotLine.PlotImportanceID,
Description = plotLine.Description, Description = plotLine.Description,
ParentPlotLineID = plotLine.ParentPlotLineID, ParentPlotLineID = plotLine.ParentPlotLineID,
EmergesFromPlotLineID = plotLine.EmergesFromPlotLineID,
SortOrder = plotLine.SortOrder, SortOrder = plotLine.SortOrder,
Colour = plotLine.Colour, Colour = plotLine.Colour,
IsMainPlot = plotLine.IsMainPlot,
IsVisibleOnTimeline = plotLine.IsVisibleOnTimeline, IsVisibleOnTimeline = plotLine.IsVisibleOnTimeline,
Project = project Project = project
}, lookupData, plotLines.Where(x => x.PlotLineID != plotLine.PlotLineID)); }, lookupData, plotLines.Where(x => x.PlotLineID != plotLine.PlotLineID));
@ -2106,11 +2109,12 @@ public sealed class PlotService(IProjectRepository projects, IBookRepository boo
BookID = model.BookID, BookID = model.BookID,
PlotLineName = model.PlotLineName, PlotLineName = model.PlotLineName,
PlotLineTypeID = model.PlotLineTypeID, PlotLineTypeID = model.PlotLineTypeID,
PlotImportanceID = model.PlotImportanceID,
Description = model.Description, Description = model.Description,
ParentPlotLineID = model.ParentPlotLineID, ParentPlotLineID = model.ParentPlotLineID,
EmergesFromPlotLineID = model.EmergesFromPlotLineID,
SortOrder = model.SortOrder, SortOrder = model.SortOrder,
Colour = string.IsNullOrWhiteSpace(model.Colour) ? "#2f6f63" : model.Colour, Colour = string.IsNullOrWhiteSpace(model.Colour) ? "#2f6f63" : model.Colour,
IsMainPlot = model.IsMainPlot,
IsVisibleOnTimeline = model.IsVisibleOnTimeline IsVisibleOnTimeline = model.IsVisibleOnTimeline
}); });
await activity.RecordAsync(model.ProjectID, isNew ? "Created" : "Updated", "Plot Line", plotLineId, model.PlotLineName); await activity.RecordAsync(model.ProjectID, isNew ? "Created" : "Updated", "Plot Line", plotLineId, model.PlotLineName);
@ -2264,6 +2268,7 @@ public sealed class PlotService(IProjectRepository projects, IBookRepository boo
PlotThreadID = plotThreadId, PlotThreadID = plotThreadId,
SceneID = model.SceneID, SceneID = model.SceneID,
EventTypeID = eventType.ThreadEventTypeID, EventTypeID = eventType.ThreadEventTypeID,
PlotEventTypeID = lookupData.PlotEventTypes.FirstOrDefault(x => x.TypeName == "Progress")?.PlotEventTypeID ?? 2,
EventTitle = title, EventTitle = title,
EventDescription = model.EventDescription EventDescription = model.EventDescription
}); });
@ -2297,6 +2302,7 @@ public sealed class PlotService(IProjectRepository projects, IBookRepository boo
private async Task<PlotLineEditViewModel> PopulatePlotLineListsAsync(PlotLineEditViewModel model, PlotLookupData lookupData, IEnumerable<PlotLineItem> parentCandidates) private async Task<PlotLineEditViewModel> PopulatePlotLineListsAsync(PlotLineEditViewModel model, PlotLookupData lookupData, IEnumerable<PlotLineItem> parentCandidates)
{ {
model.PlotLineTypes = ChapterService.ToSelectList(lookupData.PlotLineTypes, x => x.PlotLineTypeID, x => x.TypeName); model.PlotLineTypes = ChapterService.ToSelectList(lookupData.PlotLineTypes, x => x.PlotLineTypeID, x => x.TypeName);
model.PlotImportance = ChapterService.ToSelectList(lookupData.PlotImportance, x => x.PlotImportanceID, x => x.ImportanceName);
model.BookOptions = (await books.ListByProjectAsync(model.ProjectID)) model.BookOptions = (await books.ListByProjectAsync(model.ProjectID))
.Select(x => new SelectListItem($"Book {x.BookNumber}: {x.BookTitle}", x.BookID.ToString())) .Select(x => new SelectListItem($"Book {x.BookNumber}: {x.BookTitle}", x.BookID.ToString()))
.ToList(); .ToList();

File diff suppressed because it is too large Load Diff

View File

@ -704,26 +704,30 @@ public sealed class PlotLineEditViewModel
[Display(Name = "Type")] [Display(Name = "Type")]
public int PlotLineTypeID { get; set; } public int PlotLineTypeID { get; set; }
[Display(Name = "Importance")]
public int PlotImportanceID { get; set; } = 2;
public int? BookID { get; set; } public int? BookID { get; set; }
public string? Description { get; set; } public string? Description { get; set; }
[Display(Name = "Parent plot line")] [Display(Name = "Parent plot line")]
public int? ParentPlotLineID { get; set; } public int? ParentPlotLineID { get; set; }
[Display(Name = "Emerges from plot line")]
public int? EmergesFromPlotLineID { get; set; }
[Display(Name = "Sort order")] [Display(Name = "Sort order")]
public int SortOrder { get; set; } public int SortOrder { get; set; }
[Display(Name = "Colour")] [Display(Name = "Colour")]
public string Colour { get; set; } = "#2f6f63"; public string Colour { get; set; } = "#2f6f63";
[Display(Name = "Main plot")]
public bool IsMainPlot { get; set; }
[Display(Name = "Visible on timeline")] [Display(Name = "Visible on timeline")]
public bool IsVisibleOnTimeline { get; set; } = true; public bool IsVisibleOnTimeline { get; set; } = true;
public Project? Project { get; set; } public Project? Project { get; set; }
public IReadOnlyList<SelectListItem> PlotLineTypes { get; set; } = []; public IReadOnlyList<SelectListItem> PlotLineTypes { get; set; } = [];
public IReadOnlyList<SelectListItem> PlotImportance { get; set; } = [];
public IReadOnlyList<SelectListItem> BookOptions { get; set; } = []; public IReadOnlyList<SelectListItem> BookOptions { get; set; } = [];
public IReadOnlyList<SelectListItem> ParentPlotLineOptions { get; set; } = []; public IReadOnlyList<SelectListItem> ParentPlotLineOptions { get; set; } = [];
} }

View File

@ -48,6 +48,10 @@
<label asp-for="PlotLineTypeID" class="form-label">Type <help-icon key="plotLines.fields.type" mode="inline" /></label> <label asp-for="PlotLineTypeID" class="form-label">Type <help-icon key="plotLines.fields.type" mode="inline" /></label>
<select asp-for="PlotLineTypeID" asp-items="Model.PlotLineTypes" class="form-select"></select> <select asp-for="PlotLineTypeID" asp-items="Model.PlotLineTypes" class="form-select"></select>
</div> </div>
<div class="col-md-4">
<label asp-for="PlotImportanceID" class="form-label">Importance</label>
<select asp-for="PlotImportanceID" asp-items="Model.PlotImportance" class="form-select"></select>
</div>
<div class="col-md-4"> <div class="col-md-4">
<label asp-for="BookID" class="form-label">Book / scope <help-icon key="plotLines.fields.scope" mode="inline" /></label> <label asp-for="BookID" class="form-label">Book / scope <help-icon key="plotLines.fields.scope" mode="inline" /></label>
<select asp-for="BookID" asp-items="Model.BookOptions" class="form-select"> <select asp-for="BookID" asp-items="Model.BookOptions" class="form-select">
@ -60,6 +64,12 @@
<option value="">None</option> <option value="">None</option>
</select> </select>
</div> </div>
<div class="col-md-4">
<label asp-for="EmergesFromPlotLineID" class="form-label">Emerges from plot line</label>
<select asp-for="EmergesFromPlotLineID" asp-items="Model.ParentPlotLineOptions" class="form-select">
<option value="">None</option>
</select>
</div>
<div class="col-md-2"> <div class="col-md-2">
<label asp-for="SortOrder" class="form-label"></label> <label asp-for="SortOrder" class="form-label"></label>
<input asp-for="SortOrder" class="form-control" /> <input asp-for="SortOrder" class="form-control" />
@ -72,12 +82,6 @@
<label asp-for="Description" class="form-label"></label> <label asp-for="Description" class="form-label"></label>
<textarea asp-for="Description" class="form-control" rows="4"></textarea> <textarea asp-for="Description" class="form-control" rows="4"></textarea>
</div> </div>
<div class="col-md-4">
<label class="purpose-check">
<input asp-for="IsMainPlot" />
<span>Main plot <help-icon key="plotLines.fields.mainPlot" mode="inline" /></span>
</label>
</div>
<div class="col-md-4"> <div class="col-md-4">
<label class="purpose-check"> <label class="purpose-check">
<input asp-for="IsVisibleOnTimeline" /> <input asp-for="IsVisibleOnTimeline" />

View File

@ -36,6 +36,7 @@
<th>Colour</th> <th>Colour</th>
<th>Name</th> <th>Name</th>
<th>Type</th> <th>Type</th>
<th>Importance</th>
<th>Book</th> <th>Book</th>
<th>Timeline</th> <th>Timeline</th>
<th></th> <th></th>
@ -48,6 +49,7 @@
<td><span class="colour-swatch" style="background:@plotLine.Colour"></span></td> <td><span class="colour-swatch" style="background:@plotLine.Colour"></span></td>
<td><a asp-action="Edit" asp-route-id="@plotLine.PlotLineID">@plotLine.PlotLineName</a></td> <td><a asp-action="Edit" asp-route-id="@plotLine.PlotLineID">@plotLine.PlotLineName</a></td>
<td>@plotLine.PlotLineTypeName</td> <td>@plotLine.PlotLineTypeName</td>
<td>@plotLine.PlotImportanceName</td>
<td>@(plotLine.BookTitle ?? "Project-wide")</td> <td>@(plotLine.BookTitle ?? "Project-wide")</td>
<td>@(plotLine.IsVisibleOnTimeline ? "Visible" : "Hidden")</td> <td>@(plotLine.IsVisibleOnTimeline ? "Visible" : "Hidden")</td>
<td class="text-end"> <td class="text-end">