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,
PlotLineName = Clean(DisplayName(plotLine.DisplayName, plotLine.Name)),
PlotLineTypeID = ResolvePlotLineTypeId(lookupIds, plotLine.Type),
PlotImportanceID = ResolvePlotImportanceId(lookupIds, plotLine),
Description = CombineText(
plotLine.Description,
MetadataLine("Type", plotLine.Type),
@ -388,9 +389,9 @@ public sealed class ImportRepository(ISqlConnectionFactory connectionFactory) :
plotLine.Notes,
ImportMarker),
ParentPlotLineID = (int?)null,
EmergesFromPlotLineID = (int?)null,
SortOrder = (int?)null,
Colour = "#2f6f63",
IsMainPlot = string.Equals(plotLine.Type?.Trim(), "Main Plot", StringComparison.OrdinalIgnoreCase),
IsVisibleOnTimeline = true
},
transaction,
@ -685,6 +686,9 @@ public sealed class ImportRepository(ISqlConnectionFactory connectionFactory) :
var plotLineTypes = (await connection.QueryAsync<ImportLookupRow>(
"SELECT PlotLineTypeID AS LookupID, TypeName FROM dbo.PlotLineTypes WHERE IsActive = 1;",
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>(
"SELECT ThreadTypeID AS LookupID, TypeName FROM dbo.ThreadTypes WHERE IsActive = 1;",
transaction: transaction)).ToDictionary(x => x.TypeName, x => x.LookupID, StringComparer.OrdinalIgnoreCase);
@ -726,6 +730,7 @@ public sealed class ImportRepository(ISqlConnectionFactory connectionFactory) :
roleTypes,
presenceTypes,
plotLineTypes,
plotImportance,
threadTypes,
threadStatuses,
threadEventTypes,
@ -1111,6 +1116,19 @@ public sealed class ImportRepository(ISqlConnectionFactory connectionFactory) :
: 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)
{
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> PresenceTypeIds,
IReadOnlyDictionary<string, int> PlotLineTypeIds,
IReadOnlyDictionary<string, int> PlotImportanceIds,
IReadOnlyDictionary<string, int> ThreadTypeIds,
IReadOnlyDictionary<string, int> ThreadStatusIds,
IReadOnlyDictionary<string, int> ThreadEventTypeIds,

View File

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

View File

@ -361,6 +361,14 @@ public sealed class PlotLineType
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 int PlotLineID { get; set; }
@ -370,12 +378,15 @@ public sealed class PlotLineItem
public string PlotLineName { get; set; } = string.Empty;
public int PlotLineTypeID { get; set; }
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 int? ParentPlotLineID { get; set; }
public string? ParentPlotLineName { get; set; }
public int? EmergesFromPlotLineID { get; set; }
public string? EmergesFromPlotLineName { get; set; }
public int SortOrder { get; set; }
public string Colour { get; set; } = "#2f6f63";
public bool IsMainPlot { get; set; }
public bool IsVisibleOnTimeline { get; set; } = true;
public DateTime CreatedDate { get; set; }
public DateTime UpdatedDate { get; set; }
@ -434,6 +445,14 @@ public sealed class ThreadEventType
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 int ThreadEventID { get; set; }
@ -446,12 +465,23 @@ public sealed class ThreadEvent
public int EventTypeID { get; set; }
public string EventTypeName { 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? EventDescription { get; set; }
public DateTime CreatedDate { 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 int SceneID { get; set; }
@ -465,9 +495,11 @@ public sealed class SceneOption
public sealed class PlotLookupData
{
public IReadOnlyList<PlotLineType> PlotLineTypes { get; init; } = [];
public IReadOnlyList<PlotImportance> PlotImportance { get; init; } = [];
public IReadOnlyList<ThreadType> ThreadTypes { get; init; } = [];
public IReadOnlyList<ThreadStatus> ThreadStatuses { get; init; } = [];
public IReadOnlyList<ThreadEventType> ThreadEventTypes { get; init; } = [];
public IReadOnlyList<PlotEventType> PlotEventTypes { get; init; } = [];
}
public sealed class AssetKind

View File

@ -2058,6 +2058,8 @@ public sealed class PlotService(IProjectRepository projects, IBookRepository boo
SortOrder = (plotLines.Count + 1) * 10,
Colour = "#2f6f63",
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
?? lookupData.PlotLineTypes.First().PlotLineTypeID
}, lookupData, plotLines);
@ -2086,11 +2088,12 @@ public sealed class PlotService(IProjectRepository projects, IBookRepository boo
BookID = plotLine.BookID,
PlotLineName = plotLine.PlotLineName,
PlotLineTypeID = plotLine.PlotLineTypeID,
PlotImportanceID = plotLine.PlotImportanceID,
Description = plotLine.Description,
ParentPlotLineID = plotLine.ParentPlotLineID,
EmergesFromPlotLineID = plotLine.EmergesFromPlotLineID,
SortOrder = plotLine.SortOrder,
Colour = plotLine.Colour,
IsMainPlot = plotLine.IsMainPlot,
IsVisibleOnTimeline = plotLine.IsVisibleOnTimeline,
Project = project
}, lookupData, plotLines.Where(x => x.PlotLineID != plotLine.PlotLineID));
@ -2106,11 +2109,12 @@ public sealed class PlotService(IProjectRepository projects, IBookRepository boo
BookID = model.BookID,
PlotLineName = model.PlotLineName,
PlotLineTypeID = model.PlotLineTypeID,
PlotImportanceID = model.PlotImportanceID,
Description = model.Description,
ParentPlotLineID = model.ParentPlotLineID,
EmergesFromPlotLineID = model.EmergesFromPlotLineID,
SortOrder = model.SortOrder,
Colour = string.IsNullOrWhiteSpace(model.Colour) ? "#2f6f63" : model.Colour,
IsMainPlot = model.IsMainPlot,
IsVisibleOnTimeline = model.IsVisibleOnTimeline
});
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,
SceneID = model.SceneID,
EventTypeID = eventType.ThreadEventTypeID,
PlotEventTypeID = lookupData.PlotEventTypes.FirstOrDefault(x => x.TypeName == "Progress")?.PlotEventTypeID ?? 2,
EventTitle = title,
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)
{
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))
.Select(x => new SelectListItem($"Book {x.BookNumber}: {x.BookTitle}", x.BookID.ToString()))
.ToList();

File diff suppressed because it is too large Load Diff

View File

@ -704,26 +704,30 @@ public sealed class PlotLineEditViewModel
[Display(Name = "Type")]
public int PlotLineTypeID { get; set; }
[Display(Name = "Importance")]
public int PlotImportanceID { get; set; } = 2;
public int? BookID { get; set; }
public string? Description { get; set; }
[Display(Name = "Parent plot line")]
public int? ParentPlotLineID { get; set; }
[Display(Name = "Emerges from plot line")]
public int? EmergesFromPlotLineID { get; set; }
[Display(Name = "Sort order")]
public int SortOrder { get; set; }
[Display(Name = "Colour")]
public string Colour { get; set; } = "#2f6f63";
[Display(Name = "Main plot")]
public bool IsMainPlot { get; set; }
[Display(Name = "Visible on timeline")]
public bool IsVisibleOnTimeline { get; set; } = true;
public Project? Project { get; set; }
public IReadOnlyList<SelectListItem> PlotLineTypes { get; set; } = [];
public IReadOnlyList<SelectListItem> PlotImportance { get; set; } = [];
public IReadOnlyList<SelectListItem> BookOptions { 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>
<select asp-for="PlotLineTypeID" asp-items="Model.PlotLineTypes" class="form-select"></select>
</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">
<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">
@ -60,6 +64,12 @@
<option value="">None</option>
</select>
</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">
<label asp-for="SortOrder" class="form-label"></label>
<input asp-for="SortOrder" class="form-control" />
@ -72,12 +82,6 @@
<label asp-for="Description" class="form-label"></label>
<textarea asp-for="Description" class="form-control" rows="4"></textarea>
</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">
<label class="purpose-check">
<input asp-for="IsVisibleOnTimeline" />

View File

@ -36,6 +36,7 @@
<th>Colour</th>
<th>Name</th>
<th>Type</th>
<th>Importance</th>
<th>Book</th>
<th>Timeline</th>
<th></th>
@ -48,6 +49,7 @@
<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>@plotLine.PlotLineTypeName</td>
<td>@plotLine.PlotImportanceName</td>
<td>@(plotLine.BookTitle ?? "Project-wide")</td>
<td>@(plotLine.IsVisibleOnTimeline ? "Visible" : "Hidden")</td>
<td class="text-end">