Add project files.

This commit is contained in:
Nick Beckley 2026-06-01 09:03:59 +01:00
parent 80b268b940
commit 6e237b7287
146 changed files with 99499 additions and 0 deletions

View File

@ -0,0 +1 @@
False

View File

@ -0,0 +1 @@
ac804ed3-9efd-491a-9433-d35a726f772f

View File

@ -0,0 +1 @@
01e2d38e-f216-48da-a539-6636fe1c9721

View File

@ -0,0 +1,5 @@
https://dc.services.visualstudio.com/v2/track
Content-Type:application/x-json-stream
Content-Encoding:gzip
H4sIAAAAAAAACpSTT2/cNhDFv4rAsyjzn0hRN2Odogs38SJZpEVvFGcUE5apBUU5CYz97gG1GzjprRdBGOjNvPeb0SuJ7hlJT25Pp3cvGPNCapLDVhJMaMo0ZfzITN+2vZIN10YKLv4lNQn3+J30xCjvubdIJWqkClhLBzl6CoCWMw5MtKr0dF8W0r8SFxrAl+CxmZfPmJYwR9KTv0OE+WuZ7ULjp3mFJs0T7uOSXfTFzIf97p7+dXs4PhwuXy24FHETgPSEGyaEUIZ2ykuqOhhpB5ZT3Y6ghnZw3rqLLMSMKbqpWeDpbT7MOWL2vWiEbBgVlpxrAi67YnlwCx6/n4qLDdFdqddbeXvtX8kLJtKL+ifMS7sbP4Wb52VYwwQ3l2d2y1NJeUrzCVMOuDHZYk6Ty2GO1XXSbgoYM6nJw6fqMLk8zun5N1CfLvn3/yP+sUx/9w39mhF28xpz0UpDavLe+ccQsdrfkZ4wjgJkh3QUXJd2jrpWWqq11CNyb43gpCa7OeYQ13ldqn3M+CVtAUhP/nDTghfrt8k/how+r6mk+keXWzikGVafqzf+nDWskYwV0ZpPa64+IoSEPmPJd0zrtd9/JEKLTbRbU8KYq4PLj9WfbnkkPbGu89xZZ53T3o/YMhwZMKeZAdSgEZQAA8oq0Moyr5UbuALX8mEsFVITLOuutguz0jA5qIGyDhlVQijaGddSawZjOxgkUwXxG8fqYSo65zumECS1OAJVljtqlZQUZOuM0KMxYvwFyXX7nBXAgC+Xf2VzoAE6BM2pULylylpOO9saOphusIPusPXF8z2miNMvoN4Hn+ZlHnN1PZ7qN3Qf11j+92o7pK8h0m/bju5m/4SpKjt2IZbzvq71fD6ffwAAAP//AwCvA78iNgQAAA==

View File

@ -0,0 +1,5 @@
https://dc.services.visualstudio.com/v2/track
Content-Type:application/x-json-stream
Content-Encoding:gzip
H4sIAAAAAAAACpRTwW7bOBD9FYFnUSEpihJ1C5xd1Ei7MdqgLXobcUY1UYUyKCptEfjfF5RdpN3b3ohHvpk37w1fWIAnYj27PZ3+eqaQFlay5DdICWW4MFzIR9H2TdPrupKtakXTfGEl8/f0k/Ws1c5JZ4nXZIhrFA0f6tFxRLJSSBSq0bkmfF1Y/8LAV0jP3lE1Lx8pLn4OrGeffMD5e+4NvnLTvGIV54n2YUkQXBbzz353z9/eHh4fDpdXCy2ZXHlkPZOtUErplnfa1Vx3OPIOreSmGVEPzQDOwoXmQ6IYYKoW/PbaH+cUKLleVaquBFeWnUuGkCBLHmChx5+nrGKz6C7j5QZvx/6FPVNkvSp/mXkpd+Mmf+PmpycIeDP64JcjK9kpzieKydNmxzbhNEHycyiuTXaTp5BYyR4+FIcJ0jjHpz88+nAZff8/Jn8H7ugDFfs71jMhSWHdER+VNJkDHJracmNqM5J0tlWSlWw3h+TDOq9LsQ+JvsZNJevZ3zAtdNF3G93RJ3JpjVn6Z5Ozph8+7WbMgGAlO8QZV5eKV7ulqERVi3z5sKbTmor3hD6SS5Rneozrtfx/KMqojbRbY6SQigOkY/EGliPrmYXOSbBgAYxzIzWCRoECjGiRDBpCrbBFbTUabYUzGgapERo5jBnJunO6xbZQ2liDQkqOYDquW6E52LrhQ9MNDvQgqM4WvdpaPEyZB64TmrDmlkbk2krgVtc1x7qBVpmxbdX4myXXxKXIxZCeL19jU2AQO0IjudKy4dpayTvbtHxou8EOpqPGZc33FANNvxn1zrs4L/OYiuvCFH9Y934N+XsX2/J894H/2CK7m903ikWOHHzI23xN+Xw+n/8FAAD//wMAJ8SPOyUEAAA=

View File

@ -0,0 +1,5 @@
https://dc.services.visualstudio.com/v2/track
Content-Type:application/x-json-stream
Content-Encoding:gzip
H4sIAAAAAAAACoxUy27kNhD8FYHXiDJfokjdHNuLDBw/sDNIgtxaZMtmLFMDirJ3YfjfA2ns9WMRIBdBaJJd1dXV/UQi3CNpyfF+f/aAMU+kJDmsIcGEpkxTxnesaeu6VbLiDau15n+TkoRz/E5a0ijnuLNIJWqkyrOadrJ31Hu0nHHPRK2WnHAzkfaJQKg8PgSH1Tj9gWkKYyQt+TNEPz4u2BAqN4yzr9I44CZOGaJbyFxuTs7p78fXu6vrw60Jp+VxFTxpCW+YEEI11CgnqTK+p8ZbTnXde9XVHTgLh2chZkwRhmryd2/4fswRs2tFJWTFqLDkuSQeMiyUO5hw932/sFglOl3i5Rpef9sn8oCJtKJ8FfOQ7sgN4eh+6uYw+KP1S0qyT+MeUw64qnG1Ld5IcFaxSmjBGCnJBbjbELHYnBZXw1IiOMMUekkt9p4qy4FaJSX1soZG6L5pRE9Kco4p4vAu60VwaZzGPhcvIhcfcLbHJ2cRugEXjC8wTEhKspm2GaKHYYx49g3dnA+5dmlejk/GmEOcx3kqNjHjTYKX89f3J3NKGHNxDfm2+A2mW9ISC8ZxsGABtHM91gx75hlo1njUXqNXwjdeWeW1ssxpBR1XHmre9UuElOTXRcPt7BxO0xub1STDsHIoXvp0MgSM+YOMpCWMo/DSIO0F14tLgEItLdVa6h65s43grzA7SDeYSUt603Udc64TynIHwoL0PZNe91aIpgHVy7pRQuuaNeB80wiuvGzQgewlM4KU5DqNfnb5c6vl2oDrNP6DLi9SkZZoVtfGQdcJrbgBa1BxrYy22FjVA1PSdcBrA13HGdTMWSMccG1Bdb7uf6h0Fm9CxNMw7Qf4/g7XVLqSvzgAw3tQn65/SXCPj2O6uzzYuLo825GSfJ3jshGKzWKRxxDpN71M9MU85LC7TQge/cXo8Wcf4TIuxWFCbW28YpLqpkaqhOPUcGVo4xB75TrXWU1Kcjq6O0zF4jAIcZmqH8mutsVxcrcho8tzWvj9tfK42hbXA+R+TPcfNsm7wj4LUAnJ2dIZjw+HbbRy1N4b9JpToXhNlbWcGls3tGtMZzttsHaLYldz3s+5+Io+JHR5rfjFiyvmyS26u5/FeHXBi0U5W7y2PWyxVdr/tcSeS3KPMM0J79dt3T6RTYyYVuTT+TCKm3gRhiFM6MboJ9Jq0ZjKGmZfGP7nPVnbSinJn5+fn/8FAAD//wMA8VM0JxsGAAA=

View File

@ -0,0 +1,5 @@
https://dc.services.visualstudio.com/v2/track
Content-Type:application/x-json-stream
Content-Encoding:gzip
H4sIAAAAAAAACpSU0W7cKhCGX8Xi2jiAMTa+izatukrbrJpVe3TuxjBuUBy8Apw2ivbdK+yt2lwdnRuExvzDP98MfiUenpD05Pp0eveMPkVSkuTWkGBCUaYo40fW9k3Ty7riqlGiVf+SkrhbfCE9aaUx3GikNSqk0rKGDvVoqLWoOeOWiUbmnPA9kv6VgKssPjuD1Ry/Yohu9qQn35y38498N7jKTPNiqzBPuPcxgTfZzOf97pZ+vD4c7w7bqYgxiytnSU94y4QQsqWdNDWVnR1pZzWnqhmtHJoBjIZN5nzC4GGqon38c7+dk8dkelGJumJUaHIuiYUE2fIAEY8vp+xiRXST4+UaXrf9K3nGQHpR/oa5pbsyk7t6isPiJnu1rQniY1zXEUyawwspySnMJwzJ4crnOkZ8GqaXI8TH99uZvI3vfqJZEtrdvPiUK64z1RXQNEFysy8uHneTQ5/Wjyk4H535r1w1KcndfXGYII1zeHrTj/sN8/5/UP4E5sF5LPY3pCeMo7B1h3QUXGUNUGhqTZWq1Yjc6FZwUpLd7JPzy7zEYu8Tfg9rSaQn72GKuPm7DubBJTRpCbnOf1QmcAizXUwq/vSSs4pVNWNZtKTTkoovaF1AkzAXcQzLJd/XtxKhxCraLSGgT8UB0kPxAeID6YmGznDQoAGUMSM2DEdmGSjWWlRWoZXCtlZqaZXUzCgJA5cWGj6MOUJKgnl0inValWlbVMJSQItUKom0q7uRGgVty0dRKz284VjcTVkHpmMSbU01jpZKzYFqWdfU1g20Qo1tK8a/kFzmgbMM2OLz9u42B9Z2aBWnQvKGSq057XTT0qHtBj2oDhuTPd9i8Dj9BeqTM2GO85iKy4QUb9B9WXz+dxTrtPxwnv5ce3Qzm0cMRe4xOJ+fyqWt5/P5/AsAAP//AwC9ok9gggQAAA==

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<packageSources>
<clear />
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" />
</packageSources>
</configuration>

7
NuGet.Config Normal file
View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<packageSources>
<clear />
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" />
</packageSources>
</configuration>

3
PlotLine.slnx Normal file
View File

@ -0,0 +1,3 @@
<Solution>
<Project Path="PlotLine/PlotLine.csproj" />
</Solution>

View File

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<key id="b800ca31-7991-402d-8017-716ba11edc31" version="1">
<creationDate>2026-05-31T13:09:43.9306091Z</creationDate>
<activationDate>2026-05-31T13:09:43.9306091Z</activationDate>
<expirationDate>2026-08-29T13:09:43.9306091Z</expirationDate>
<descriptor deserializerType="Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.ConfigurationModel.AuthenticatedEncryptorDescriptorDeserializer, Microsoft.AspNetCore.DataProtection, Version=10.0.0.0, Culture=neutral, PublicKeyToken=adb9793829ddae60">
<descriptor>
<encryption algorithm="AES_256_CBC" />
<validation algorithm="HMACSHA256" />
<masterKey p4:requiresEncryption="true" xmlns:p4="http://schemas.asp.net/2015/03/dataProtection">
<!-- Warning: the key below is in an unencrypted form. -->
<value>rYC5RTgXQzx3wsUyn9wS1B53mDgBKXjX+/G96+7qkSTGiNqcMbK60Zgr/ujMcmusWpeyIXHuajhvceGxlWONQg==</value>
</masterKey>
</descriptor>
</descriptor>
</key>

View File

@ -0,0 +1,19 @@
using Microsoft.AspNetCore.Mvc;
using PlotLine.Services;
namespace PlotLine.Controllers;
public sealed class AnalyticsController(IAnalyticsService analytics) : Controller
{
public async Task<IActionResult> Index(
int projectId,
int? bookId,
decimal? startChapterNumber,
decimal? endChapterNumber,
decimal? startSceneNumber,
decimal? endSceneNumber)
{
var model = await analytics.GetStoryHealthAsync(projectId, bookId, startChapterNumber, endChapterNumber, startSceneNumber, endSceneNumber);
return model is null ? NotFound() : View(model);
}
}

View File

@ -0,0 +1,47 @@
using Microsoft.AspNetCore.Mvc;
using PlotLine.Services;
using PlotLine.ViewModels;
namespace PlotLine.Controllers;
public sealed class BooksController(IBookService books) : Controller
{
public async Task<IActionResult> Details(int id)
{
var model = await books.GetDetailAsync(id);
return model is null ? NotFound() : View(model);
}
public async Task<IActionResult> Create(int projectId)
{
var model = await books.GetCreateAsync(projectId);
return model is null ? NotFound() : View("Edit", model);
}
public async Task<IActionResult> Edit(int id)
{
var model = await books.GetEditAsync(id);
return model is null ? NotFound() : View(model);
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Save(BookEditViewModel model)
{
if (!ModelState.IsValid)
{
return View("Edit", model);
}
var bookId = await books.SaveAsync(model);
return RedirectToAction(nameof(Details), new { id = bookId });
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Archive(int id, int projectId)
{
await books.ArchiveAsync(id);
return RedirectToAction("Details", "Projects", new { id = projectId });
}
}

View File

@ -0,0 +1,47 @@
using Microsoft.AspNetCore.Mvc;
using PlotLine.Services;
using PlotLine.ViewModels;
namespace PlotLine.Controllers;
public sealed class ChaptersController(IChapterService chapters) : Controller
{
public async Task<IActionResult> Details(int id)
{
var model = await chapters.GetDetailAsync(id);
return model is null ? NotFound() : View(model);
}
public async Task<IActionResult> Create(int bookId)
{
var model = await chapters.GetCreateAsync(bookId);
return model is null ? NotFound() : View("Edit", model);
}
public async Task<IActionResult> Edit(int id)
{
var model = await chapters.GetEditAsync(id);
return model is null ? NotFound() : View(model);
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Save(ChapterEditViewModel model)
{
if (!ModelState.IsValid)
{
return View("Edit", model);
}
var chapterId = await chapters.SaveAsync(model);
return RedirectToAction(nameof(Details), new { id = chapterId });
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Archive(int id, int bookId)
{
await chapters.ArchiveAsync(id);
return RedirectToAction("Details", "Books", new { id = bookId });
}
}

View File

@ -0,0 +1,53 @@
using Microsoft.AspNetCore.Mvc;
using PlotLine.Services;
using PlotLine.ViewModels;
namespace PlotLine.Controllers;
public sealed class CharactersController(ICharacterService characters) : Controller
{
public async Task<IActionResult> Index(int projectId)
{
var model = await characters.GetCharactersAsync(projectId);
return model is null ? NotFound() : View(model);
}
public async Task<IActionResult> Create(int projectId)
{
var model = await characters.GetCreateCharacterAsync(projectId);
return model is null ? NotFound() : View("Edit", model);
}
public async Task<IActionResult> Edit(int id)
{
var model = await characters.GetEditCharacterAsync(id);
return model is null ? NotFound() : View(model);
}
public async Task<IActionResult> Details(int id)
{
var model = await characters.GetCharacterDetailAsync(id);
return model is null ? NotFound() : View(model);
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Save(CharacterEditViewModel model)
{
if (!ModelState.IsValid)
{
return View("Edit", model);
}
var characterId = await characters.SaveCharacterAsync(model);
return RedirectToAction(nameof(Details), new { id = characterId });
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Archive(int id, int projectId)
{
await characters.ArchiveCharacterAsync(id);
return RedirectToAction(nameof(Index), new { projectId });
}
}

View File

@ -0,0 +1,27 @@
using Microsoft.AspNetCore.Mvc;
using PlotLine.Models;
using System.Diagnostics;
namespace PlotLine.Controllers
{
public class HomeController : Controller
{
public IActionResult Index()
{
return View();
}
public IActionResult Privacy()
{
return View();
}
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
public IActionResult Error()
{
return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
}
}
}

View File

@ -0,0 +1,69 @@
using Microsoft.AspNetCore.Mvc;
using PlotLine.Services;
using PlotLine.ViewModels;
namespace PlotLine.Controllers;
public sealed class LocationsController(ILocationService locations) : Controller
{
public async Task<IActionResult> Index(int projectId)
{
var model = await locations.GetLocationsAsync(projectId);
return model is null ? NotFound() : View(model);
}
public async Task<IActionResult> Create(int projectId)
{
var model = await locations.GetCreateAsync(projectId);
return model is null ? NotFound() : View("Edit", model);
}
public async Task<IActionResult> Edit(int id)
{
var model = await locations.GetEditAsync(id);
return model is null ? NotFound() : View(model);
}
public async Task<IActionResult> Details(int id)
{
var model = await locations.GetDetailAsync(id);
return model is null ? NotFound() : View(model);
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Save(LocationEditViewModel model)
{
if (!ModelState.IsValid)
{
return View("Edit", model);
}
var locationId = await locations.SaveAsync(model);
return RedirectToAction(nameof(Details), new { id = locationId });
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Archive(int id, int projectId)
{
await locations.ArchiveAsync(id);
return RedirectToAction(nameof(Index), new { projectId });
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> AddRelationship(LocationRelationshipEditViewModel model)
{
await locations.SaveRelationshipAsync(model);
return RedirectToAction(nameof(Details), new { id = model.FromLocationID });
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> ArchiveRelationship(int id, int returnLocationId)
{
await locations.ArchiveRelationshipAsync(id);
return RedirectToAction(nameof(Details), new { id = returnLocationId });
}
}

View File

@ -0,0 +1,47 @@
using Microsoft.AspNetCore.Mvc;
using PlotLine.Services;
using PlotLine.ViewModels;
namespace PlotLine.Controllers;
public sealed class PlotLinesController(IPlotService plots) : Controller
{
public async Task<IActionResult> Index(int projectId)
{
var model = await plots.GetPlotLinesAsync(projectId);
return model is null ? NotFound() : View(model);
}
public async Task<IActionResult> Create(int projectId)
{
var model = await plots.GetCreatePlotLineAsync(projectId);
return model is null ? NotFound() : View("Edit", model);
}
public async Task<IActionResult> Edit(int id)
{
var model = await plots.GetEditPlotLineAsync(id);
return model is null ? NotFound() : View(model);
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Save(PlotLineEditViewModel model)
{
if (!ModelState.IsValid)
{
return View("Edit", model);
}
await plots.SavePlotLineAsync(model);
return RedirectToAction(nameof(Index), new { projectId = model.ProjectID });
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Archive(int id, int projectId)
{
await plots.ArchivePlotLineAsync(id);
return RedirectToAction(nameof(Index), new { projectId });
}
}

View File

@ -0,0 +1,47 @@
using Microsoft.AspNetCore.Mvc;
using PlotLine.Services;
using PlotLine.ViewModels;
namespace PlotLine.Controllers;
public sealed class PlotThreadsController(IPlotService plots) : Controller
{
public async Task<IActionResult> Index(int projectId)
{
var model = await plots.GetPlotThreadsAsync(projectId);
return model is null ? NotFound() : View(model);
}
public async Task<IActionResult> Create(int projectId, int? plotLineId)
{
var model = await plots.GetCreatePlotThreadAsync(projectId, plotLineId);
return model is null ? NotFound() : View("Edit", model);
}
public async Task<IActionResult> Edit(int id)
{
var model = await plots.GetEditPlotThreadAsync(id);
return model is null ? NotFound() : View(model);
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Save(PlotThreadEditViewModel model)
{
if (!ModelState.IsValid)
{
return View("Edit", model);
}
await plots.SavePlotThreadAsync(model);
return RedirectToAction(nameof(Index), new { projectId = model.ProjectID });
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Archive(int id, int projectId)
{
await plots.ArchivePlotThreadAsync(id);
return RedirectToAction(nameof(Index), new { projectId });
}
}

View File

@ -0,0 +1,45 @@
using Microsoft.AspNetCore.Mvc;
using PlotLine.Services;
using PlotLine.ViewModels;
namespace PlotLine.Controllers;
public sealed class ProjectsController(IProjectService projects) : Controller
{
public async Task<IActionResult> Index() => View(await projects.ListAsync());
public async Task<IActionResult> Details(int id)
{
var model = await projects.GetDetailAsync(id);
return model is null ? NotFound() : View(model);
}
public IActionResult Create() => View("Edit", new ProjectEditViewModel());
public async Task<IActionResult> Edit(int id)
{
var model = await projects.GetEditAsync(id);
return model is null ? NotFound() : View(model);
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Save(ProjectEditViewModel model)
{
if (!ModelState.IsValid)
{
return View("Edit", model);
}
var projectId = await projects.SaveAsync(model);
return RedirectToAction(nameof(Details), new { id = projectId });
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Archive(int id)
{
await projects.ArchiveAsync(id);
return RedirectToAction(nameof(Index));
}
}

View File

@ -0,0 +1,268 @@
using Microsoft.AspNetCore.Mvc;
using PlotLine.Services;
using PlotLine.ViewModels;
namespace PlotLine.Controllers;
public sealed class ScenesController(ISceneService scenes, IPlotService plots, IAssetService assets, ICharacterService characters, ILocationService locations) : Controller
{
public async Task<IActionResult> Create(int chapterId)
{
var model = await scenes.GetCreateAsync(chapterId);
return model is null ? NotFound() : View("Edit", model);
}
public async Task<IActionResult> Edit(int id)
{
var model = await scenes.GetEditAsync(id);
return model is null ? NotFound() : View(model);
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Save(SceneEditViewModel model)
{
if (!ModelState.IsValid)
{
return View("Edit", model);
}
var sceneId = await scenes.SaveAsync(model);
if (model.ReturnToTimeline && model.ReturnProjectID.HasValue)
{
return RedirectToAction("Index", "Timeline", new
{
projectId = model.ReturnProjectID.Value,
bookId = model.ReturnBookID,
selectedSceneId = sceneId
});
}
return RedirectToAction(nameof(Edit), new { id = sceneId });
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Archive(int id, int chapterId)
{
await scenes.ArchiveAsync(id);
return RedirectToAction("Details", "Chapters", new { id = chapterId });
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> MoveUp(int id, int chapterId)
{
await scenes.MoveAsync(id, "Up");
return RedirectToAction("Details", "Chapters", new { id = chapterId });
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> MoveDown(int id, int chapterId)
{
await scenes.MoveAsync(id, "Down");
return RedirectToAction("Details", "Chapters", new { id = chapterId });
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> AddThreadEvent(ThreadEventCreateViewModel model)
{
await plots.AddThreadEventAsync(model);
if (model.ReturnProjectID.HasValue)
{
return RedirectToAction("Index", "Timeline", new
{
projectId = model.ReturnProjectID.Value,
bookId = model.ReturnBookID,
selectedSceneId = model.SceneID
});
}
return RedirectToAction(nameof(Edit), new { id = model.SceneID });
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> DeleteThreadEvent(int id, int sceneId, int? projectId, int? bookId)
{
await plots.DeleteThreadEventAsync(id);
if (projectId.HasValue)
{
return RedirectToAction("Index", "Timeline", new { projectId = projectId.Value, bookId, selectedSceneId = sceneId });
}
return RedirectToAction(nameof(Edit), new { id = sceneId });
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> AddAssetEvent(AssetEventCreateViewModel model)
{
await assets.AddAssetEventAsync(model);
if (model.ReturnProjectID.HasValue)
{
return RedirectToAction("Index", "Timeline", new
{
projectId = model.ReturnProjectID.Value,
bookId = model.ReturnBookID,
selectedSceneId = model.SceneID
});
}
return RedirectToAction(nameof(Edit), new { id = model.SceneID });
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> DeleteAssetEvent(int id, int sceneId, int? projectId, int? bookId)
{
await assets.DeleteAssetEventAsync(id);
if (projectId.HasValue)
{
return RedirectToAction("Index", "Timeline", new { projectId = projectId.Value, bookId, selectedSceneId = sceneId });
}
return RedirectToAction(nameof(Edit), new { id = sceneId });
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> AddAssetCustodyEvent(AssetCustodyCreateViewModel model)
{
await assets.AddCustodyEventAsync(model);
if (model.ReturnProjectID.HasValue)
{
return RedirectToAction("Index", "Timeline", new
{
projectId = model.ReturnProjectID.Value,
bookId = model.ReturnBookID,
selectedSceneId = model.SceneID
});
}
return RedirectToAction(nameof(Edit), new { id = model.SceneID });
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> AddSceneAssetLocation(SceneAssetLocationCreateViewModel model)
{
await locations.SaveSceneAssetLocationAsync(model);
return RedirectForScene(model.SceneID, model.ReturnProjectID, model.ReturnBookID);
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> DeleteSceneAssetLocation(int id, int sceneId, int? projectId, int? bookId)
{
await locations.DeleteSceneAssetLocationAsync(id);
return RedirectForScene(sceneId, projectId, bookId);
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> DeleteAssetCustodyEvent(int id, int sceneId, int? projectId, int? bookId)
{
await assets.DeleteCustodyEventAsync(id);
if (projectId.HasValue)
{
return RedirectToAction("Index", "Timeline", new { projectId = projectId.Value, bookId, selectedSceneId = sceneId });
}
return RedirectToAction(nameof(Edit), new { id = sceneId });
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> AddSceneCharacter(SceneCharacterCreateViewModel model)
{
await characters.AddSceneCharacterAsync(model);
return RedirectForScene(model.SceneID, model.ReturnProjectID, model.ReturnBookID);
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> DeleteSceneCharacter(int id, int sceneId, int? projectId, int? bookId)
{
await characters.DeleteSceneCharacterAsync(id);
return RedirectForScene(sceneId, projectId, bookId);
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> AddSceneDependency(SceneDependencyCreateViewModel model)
{
await scenes.AddDependencyAsync(model);
return RedirectForScene(model.CurrentSceneID, model.ReturnProjectID, model.ReturnBookID);
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> DeleteSceneDependency(int id, int sceneId, int? projectId, int? bookId)
{
await scenes.DeleteDependencyAsync(id);
return RedirectForScene(sceneId, projectId, bookId);
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> PreviewMove(SceneMoveRequestViewModel model)
{
var preview = await scenes.GetMovePreviewAsync(model);
return preview is null ? NotFound() : View("MovePreview", preview);
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> ConfirmMove(SceneMoveRequestViewModel model)
{
await scenes.MoveToChapterAsync(model);
return RedirectForScene(model.SceneID, model.ReturnProjectID, model.ReturnBookID);
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> RenumberChapter(int chapterId, int? projectId, int? bookId, int? sceneId)
{
await scenes.RenumberChapterAsync(chapterId);
return sceneId.HasValue
? RedirectForScene(sceneId.Value, projectId, bookId)
: RedirectToAction("Details", "Chapters", new { id = chapterId });
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> AddCharacterAttributeEvent(CharacterAttributeEventCreateViewModel model)
{
await characters.AddAttributeEventAsync(model);
return RedirectForScene(model.SceneID, model.ReturnProjectID, model.ReturnBookID);
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> AddCharacterKnowledge(CharacterKnowledgeCreateViewModel model)
{
await characters.AddKnowledgeAsync(model);
return RedirectForScene(model.SceneID, model.ReturnProjectID, model.ReturnBookID);
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> AddRelationshipEvent(RelationshipEventCreateViewModel model)
{
await characters.AddRelationshipEventAsync(model);
return RedirectForScene(model.SceneID, model.ReturnProjectID, model.ReturnBookID);
}
private IActionResult RedirectForScene(int sceneId, int? projectId, int? bookId)
{
if (projectId.HasValue)
{
return RedirectToAction("Index", "Timeline", new { projectId = projectId.Value, bookId, selectedSceneId = sceneId });
}
return RedirectToAction(nameof(Edit), new { id = sceneId });
}
}

View File

@ -0,0 +1,69 @@
using Microsoft.AspNetCore.Mvc;
using PlotLine.Services;
using PlotLine.ViewModels;
namespace PlotLine.Controllers;
public sealed class StoryAssetsController(IAssetService assets) : Controller
{
public async Task<IActionResult> Index(int projectId)
{
var model = await assets.GetAssetsAsync(projectId);
return model is null ? NotFound() : View(model);
}
public async Task<IActionResult> Create(int projectId)
{
var model = await assets.GetCreateAssetAsync(projectId);
return model is null ? NotFound() : View("Edit", model);
}
public async Task<IActionResult> Edit(int id)
{
var model = await assets.GetEditAssetAsync(id);
return model is null ? NotFound() : View(model);
}
public async Task<IActionResult> Details(int id)
{
var model = await assets.GetAssetDetailAsync(id);
return model is null ? NotFound() : View(model);
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Save(StoryAssetEditViewModel model)
{
if (!ModelState.IsValid)
{
return View("Edit", model);
}
var assetId = await assets.SaveAssetAsync(model);
return RedirectToAction(nameof(Details), new { id = assetId });
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Archive(int id, int projectId)
{
await assets.ArchiveAssetAsync(id);
return RedirectToAction(nameof(Index), new { projectId });
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> AddDependency(AssetDependencyEditViewModel model, int returnAssetId)
{
await assets.SaveDependencyAsync(model);
return RedirectToAction(nameof(Details), new { id = returnAssetId });
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> DeleteDependency(int id, int returnAssetId)
{
await assets.DeleteDependencyAsync(id);
return RedirectToAction(nameof(Details), new { id = returnAssetId });
}
}

View File

@ -0,0 +1,13 @@
using Microsoft.AspNetCore.Mvc;
using PlotLine.Services;
namespace PlotLine.Controllers;
public sealed class TimelineController(ITimelineService timeline) : Controller
{
public async Task<IActionResult> Index(int projectId, int? bookId, int? selectedSceneId)
{
var model = await timeline.GetAsync(projectId, bookId, selectedSceneId);
return model is null ? NotFound() : View(model);
}
}

View File

@ -0,0 +1,86 @@
using Microsoft.AspNetCore.Mvc;
using PlotLine.Services;
namespace PlotLine.Controllers;
public sealed class WarningsController(IContinuityValidationService validation) : Controller
{
public async Task<IActionResult> Index(
int projectId,
int? bookId,
int? sceneId,
int? warningTypeId,
int? warningSeverityId,
string? entityType,
bool includeDismissed = false,
bool includeIntentional = false)
{
var model = await validation.GetDashboardAsync(projectId, bookId, sceneId, warningTypeId, warningSeverityId, entityType, includeDismissed, includeIntentional);
return model is null ? NotFound() : View(model);
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> ValidateProject(int projectId, int? bookId)
{
var summary = await validation.ValidateProjectAsync(projectId);
var model = await validation.GetDashboardAsync(projectId, bookId, null, null, null, null, false, false, summary);
return model is null ? NotFound() : View("Index", model);
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> ValidateBook(int projectId, int bookId)
{
var summary = await validation.ValidateBookAsync(bookId);
var model = await validation.GetDashboardAsync(projectId, bookId, null, null, null, null, false, false, summary);
return model is null ? NotFound() : View("Index", model);
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> ValidateScene(int projectId, int? bookId, int sceneId, bool returnToTimeline = false)
{
await validation.ValidateSceneAsync(sceneId);
if (returnToTimeline)
{
return RedirectToAction("Index", "Timeline", new { projectId, bookId, selectedSceneId = sceneId });
}
return RedirectToAction(nameof(Index), new { projectId, bookId, sceneId });
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Dismiss(int id, int projectId, int? bookId, int? sceneId, bool returnToTimeline = false)
{
await validation.DismissAsync(id);
return RedirectAfterAction(projectId, bookId, sceneId, returnToTimeline);
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> MarkIntentional(int id, int projectId, int? bookId, int? sceneId, bool returnToTimeline = false)
{
await validation.MarkIntentionalAsync(id);
return RedirectAfterAction(projectId, bookId, sceneId, returnToTimeline);
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Restore(int id, int projectId, int? bookId, int? sceneId)
{
await validation.RestoreAsync(id);
return RedirectToAction(nameof(Index), new { projectId, bookId, sceneId, includeDismissed = true, includeIntentional = true });
}
private IActionResult RedirectAfterAction(int projectId, int? bookId, int? sceneId, bool returnToTimeline)
{
if (returnToTimeline && sceneId.HasValue)
{
return RedirectToAction("Index", "Timeline", new { projectId, bookId, selectedSceneId = sceneId.Value });
}
return RedirectToAction(nameof(Index), new { projectId, bookId, sceneId });
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,17 @@
using System.Data;
using Microsoft.Data.SqlClient;
namespace PlotLine.Data;
public interface ISqlConnectionFactory
{
IDbConnection CreateConnection();
}
public sealed class SqlConnectionFactory(IConfiguration configuration) : ISqlConnectionFactory
{
private readonly string _connectionString = configuration.GetConnectionString("DefaultConnection")
?? throw new InvalidOperationException("DefaultConnection is not configured.");
public IDbConnection CreateConnection() => new SqlConnection(_connectionString);
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,9 @@
namespace PlotLine.Models
{
public class ErrorViewModel
{
public string? RequestId { get; set; }
public bool ShowRequestId => !string.IsNullOrEmpty(RequestId);
}
}

14
PlotLine/PlotLine.csproj Normal file
View File

@ -0,0 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Dapper" Version="2.1.66" />
<PackageReference Include="Microsoft.Data.SqlClient" Version="6.1.3" />
</ItemGroup>
</Project>

67
PlotLine/Program.cs Normal file
View File

@ -0,0 +1,67 @@
using PlotLine.Data;
using PlotLine.Services;
using Microsoft.AspNetCore.DataProtection;
namespace PlotLine;
public class Program
{
public static void Main(string[] args)
{
var builder = WebApplication.CreateBuilder(args);
builder.Logging.ClearProviders();
builder.Logging.AddConsole();
builder.Logging.AddDebug();
builder.Services.AddControllersWithViews();
builder.Services.AddDataProtection()
.PersistKeysToFileSystem(new DirectoryInfo(Path.Combine(builder.Environment.ContentRootPath, "App_Data", "DataProtectionKeys")));
builder.Services.AddSingleton<ISqlConnectionFactory, SqlConnectionFactory>();
builder.Services.AddScoped<IProjectRepository, ProjectRepository>();
builder.Services.AddScoped<IBookRepository, BookRepository>();
builder.Services.AddScoped<IChapterRepository, ChapterRepository>();
builder.Services.AddScoped<ISceneRepository, SceneRepository>();
builder.Services.AddScoped<ILookupRepository, LookupRepository>();
builder.Services.AddScoped<ITimelineRepository, TimelineRepository>();
builder.Services.AddScoped<IPlotRepository, PlotRepository>();
builder.Services.AddScoped<IAssetRepository, AssetRepository>();
builder.Services.AddScoped<ICharacterRepository, CharacterRepository>();
builder.Services.AddScoped<ILocationRepository, LocationRepository>();
builder.Services.AddScoped<IWarningRepository, WarningRepository>();
builder.Services.AddScoped<ISceneDependencyRepository, SceneDependencyRepository>();
builder.Services.AddScoped<IAnalyticsRepository, AnalyticsRepository>();
builder.Services.AddScoped<IProjectService, ProjectService>();
builder.Services.AddScoped<IBookService, BookService>();
builder.Services.AddScoped<IChapterService, ChapterService>();
builder.Services.AddScoped<ISceneService, SceneService>();
builder.Services.AddScoped<ITimelineService, TimelineService>();
builder.Services.AddScoped<IPlotService, PlotService>();
builder.Services.AddScoped<IAssetService, AssetService>();
builder.Services.AddScoped<ICharacterService, CharacterService>();
builder.Services.AddScoped<ILocationService, LocationService>();
builder.Services.AddScoped<IContinuityValidationService, ContinuityValidationService>();
builder.Services.AddScoped<IAnalyticsService, AnalyticsService>();
var app = builder.Build();
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Home/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseRouting();
app.UseAuthorization();
app.MapStaticAssets();
app.MapControllerRoute(
name: "default",
pattern: "{controller=Projects}/{action=Index}/{id?}")
.WithStaticAssets();
app.Run();
}
}

View File

@ -0,0 +1,23 @@
{
"$schema": "https://json.schemastore.org/launchsettings.json",
"profiles": {
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "http://localhost:5229",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"https": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "https://localhost:7296;http://localhost:5229",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,650 @@
SET ANSI_NULLS ON;
GO
SET QUOTED_IDENTIFIER ON;
GO
IF OBJECT_ID(N'dbo.ScenePurposes', N'U') IS NULL
BEGIN
CREATE TABLE dbo.ScenePurposes
(
SceneID int NOT NULL,
ScenePurposeTypeID int NOT NULL,
CONSTRAINT PK_ScenePurposes PRIMARY KEY (SceneID, ScenePurposeTypeID)
);
END;
GO
IF OBJECT_ID(N'dbo.SceneOutcomes', N'U') IS NULL
BEGIN
CREATE TABLE dbo.SceneOutcomes
(
SceneOutcomeID int IDENTITY(1,1) NOT NULL CONSTRAINT PK_SceneOutcomes PRIMARY KEY,
SceneID int NOT NULL,
OutcomeDescription nvarchar(1000) NOT NULL,
OutcomeTypeID int NULL,
CreatedDate datetime2 NOT NULL CONSTRAINT DF_SceneOutcomes_CreatedDate DEFAULT SYSUTCDATETIME(),
UpdatedDate datetime2 NOT NULL CONSTRAINT DF_SceneOutcomes_UpdatedDate DEFAULT SYSUTCDATETIME()
);
END;
GO
IF OBJECT_ID(N'dbo.ScenePurposeTypes', N'U') IS NULL
BEGIN
CREATE TABLE dbo.ScenePurposeTypes
(
ScenePurposeTypeID int IDENTITY(1,1) NOT NULL CONSTRAINT PK_ScenePurposeTypes PRIMARY KEY,
PurposeName nvarchar(100) NOT NULL,
SortOrder int NOT NULL,
IsArchived bit NOT NULL CONSTRAINT DF_ScenePurposeTypes_IsArchived DEFAULT 0,
CONSTRAINT UQ_ScenePurposeTypes_PurposeName UNIQUE (PurposeName)
);
END;
GO
IF OBJECT_ID(N'dbo.TimeConfidences', N'U') IS NULL
BEGIN
CREATE TABLE dbo.TimeConfidences
(
TimeConfidenceID int IDENTITY(1,1) NOT NULL CONSTRAINT PK_TimeConfidences PRIMARY KEY,
TimeConfidenceName nvarchar(100) NOT NULL,
SortOrder int NOT NULL,
CONSTRAINT UQ_TimeConfidences_TimeConfidenceName UNIQUE (TimeConfidenceName)
);
END;
GO
IF OBJECT_ID(N'dbo.DurationUnits', N'U') IS NULL
BEGIN
CREATE TABLE dbo.DurationUnits
(
DurationUnitID int IDENTITY(1,1) NOT NULL CONSTRAINT PK_DurationUnits PRIMARY KEY,
DurationUnitName nvarchar(100) NOT NULL,
SortOrder int NOT NULL,
CONSTRAINT UQ_DurationUnits_DurationUnitName UNIQUE (DurationUnitName)
);
END;
GO
IF OBJECT_ID(N'dbo.TimeModes', N'U') IS NULL
BEGIN
CREATE TABLE dbo.TimeModes
(
TimeModeID int IDENTITY(1,1) NOT NULL CONSTRAINT PK_TimeModes PRIMARY KEY,
TimeModeName nvarchar(100) NOT NULL,
SortOrder int NOT NULL,
CONSTRAINT UQ_TimeModes_TimeModeName UNIQUE (TimeModeName)
);
END;
GO
IF OBJECT_ID(N'dbo.RevisionStatuses', N'U') IS NULL
BEGIN
CREATE TABLE dbo.RevisionStatuses
(
RevisionStatusID int IDENTITY(1,1) NOT NULL CONSTRAINT PK_RevisionStatuses PRIMARY KEY,
StatusName nvarchar(100) NOT NULL,
SortOrder int NOT NULL,
CONSTRAINT UQ_RevisionStatuses_StatusName UNIQUE (StatusName)
);
END;
GO
IF OBJECT_ID(N'dbo.Projects', N'U') IS NULL
BEGIN
CREATE TABLE dbo.Projects
(
ProjectID int IDENTITY(1,1) NOT NULL CONSTRAINT PK_Projects PRIMARY KEY,
ProjectName nvarchar(200) NOT NULL,
Description nvarchar(max) NULL,
CreatedDate datetime2 NOT NULL CONSTRAINT DF_Projects_CreatedDate DEFAULT SYSUTCDATETIME(),
UpdatedDate datetime2 NOT NULL CONSTRAINT DF_Projects_UpdatedDate DEFAULT SYSUTCDATETIME(),
IsArchived bit NOT NULL CONSTRAINT DF_Projects_IsArchived DEFAULT 0
);
END;
GO
IF OBJECT_ID(N'dbo.Books', N'U') IS NULL
BEGIN
CREATE TABLE dbo.Books
(
BookID int IDENTITY(1,1) NOT NULL CONSTRAINT PK_Books PRIMARY KEY,
ProjectID int NOT NULL,
BookTitle nvarchar(200) NOT NULL,
BookNumber int NOT NULL,
Description nvarchar(max) NULL,
SortOrder int NOT NULL,
CreatedDate datetime2 NOT NULL CONSTRAINT DF_Books_CreatedDate DEFAULT SYSUTCDATETIME(),
UpdatedDate datetime2 NOT NULL CONSTRAINT DF_Books_UpdatedDate DEFAULT SYSUTCDATETIME(),
IsArchived bit NOT NULL CONSTRAINT DF_Books_IsArchived DEFAULT 0,
CONSTRAINT FK_Books_Projects FOREIGN KEY (ProjectID) REFERENCES dbo.Projects(ProjectID)
);
END;
GO
IF OBJECT_ID(N'dbo.Chapters', N'U') IS NULL
BEGIN
CREATE TABLE dbo.Chapters
(
ChapterID int IDENTITY(1,1) NOT NULL CONSTRAINT PK_Chapters PRIMARY KEY,
BookID int NOT NULL,
ChapterNumber decimal(9,2) NOT NULL,
ChapterTitle nvarchar(200) NOT NULL,
POVCharacterID int NULL,
Summary nvarchar(max) NULL,
SortOrder int NOT NULL,
RevisionStatusID int NOT NULL,
CreatedDate datetime2 NOT NULL CONSTRAINT DF_Chapters_CreatedDate DEFAULT SYSUTCDATETIME(),
UpdatedDate datetime2 NOT NULL CONSTRAINT DF_Chapters_UpdatedDate DEFAULT SYSUTCDATETIME(),
IsArchived bit NOT NULL CONSTRAINT DF_Chapters_IsArchived DEFAULT 0,
CONSTRAINT FK_Chapters_Books FOREIGN KEY (BookID) REFERENCES dbo.Books(BookID),
CONSTRAINT FK_Chapters_RevisionStatuses FOREIGN KEY (RevisionStatusID) REFERENCES dbo.RevisionStatuses(RevisionStatusID)
);
END;
GO
IF OBJECT_ID(N'dbo.Scenes', N'U') IS NULL
BEGIN
CREATE TABLE dbo.Scenes
(
SceneID int IDENTITY(1,1) NOT NULL CONSTRAINT PK_Scenes PRIMARY KEY,
ChapterID int NOT NULL,
SceneNumber decimal(9,2) NOT NULL,
SceneTitle nvarchar(200) NOT NULL,
Summary nvarchar(max) NULL,
POVCharacterID int NULL,
PrimaryLocationID int NULL,
SortOrder int NOT NULL,
TimeModeID int NOT NULL,
StartDateTime datetime2 NULL,
EndDateTime datetime2 NULL,
DurationAmount decimal(9,2) NULL,
DurationUnitID int NULL,
RelativeTimeText nvarchar(200) NULL,
TimeConfidenceID int NOT NULL,
ScenePurposeNotes nvarchar(max) NULL,
SceneOutcomeNotes nvarchar(max) NULL,
RevisionStatusID int NOT NULL,
CreatedDate datetime2 NOT NULL CONSTRAINT DF_Scenes_CreatedDate DEFAULT SYSUTCDATETIME(),
UpdatedDate datetime2 NOT NULL CONSTRAINT DF_Scenes_UpdatedDate DEFAULT SYSUTCDATETIME(),
IsArchived bit NOT NULL CONSTRAINT DF_Scenes_IsArchived DEFAULT 0,
CONSTRAINT FK_Scenes_Chapters FOREIGN KEY (ChapterID) REFERENCES dbo.Chapters(ChapterID),
CONSTRAINT FK_Scenes_TimeModes FOREIGN KEY (TimeModeID) REFERENCES dbo.TimeModes(TimeModeID),
CONSTRAINT FK_Scenes_DurationUnits FOREIGN KEY (DurationUnitID) REFERENCES dbo.DurationUnits(DurationUnitID),
CONSTRAINT FK_Scenes_TimeConfidences FOREIGN KEY (TimeConfidenceID) REFERENCES dbo.TimeConfidences(TimeConfidenceID),
CONSTRAINT FK_Scenes_RevisionStatuses FOREIGN KEY (RevisionStatusID) REFERENCES dbo.RevisionStatuses(RevisionStatusID)
);
END;
GO
IF NOT EXISTS (SELECT 1 FROM sys.foreign_keys WHERE name = N'FK_ScenePurposes_Scenes')
ALTER TABLE dbo.ScenePurposes ADD CONSTRAINT FK_ScenePurposes_Scenes FOREIGN KEY (SceneID) REFERENCES dbo.Scenes(SceneID);
GO
IF NOT EXISTS (SELECT 1 FROM sys.foreign_keys WHERE name = N'FK_ScenePurposes_ScenePurposeTypes')
ALTER TABLE dbo.ScenePurposes ADD CONSTRAINT FK_ScenePurposes_ScenePurposeTypes FOREIGN KEY (ScenePurposeTypeID) REFERENCES dbo.ScenePurposeTypes(ScenePurposeTypeID);
GO
IF NOT EXISTS (SELECT 1 FROM sys.foreign_keys WHERE name = N'FK_SceneOutcomes_Scenes')
ALTER TABLE dbo.SceneOutcomes ADD CONSTRAINT FK_SceneOutcomes_Scenes FOREIGN KEY (SceneID) REFERENCES dbo.Scenes(SceneID);
GO
IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = N'IX_Books_ProjectID_SortOrder' AND object_id = OBJECT_ID(N'dbo.Books'))
CREATE INDEX IX_Books_ProjectID_SortOrder ON dbo.Books(ProjectID, IsArchived, SortOrder);
GO
IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = N'IX_Chapters_BookID_SortOrder' AND object_id = OBJECT_ID(N'dbo.Chapters'))
CREATE INDEX IX_Chapters_BookID_SortOrder ON dbo.Chapters(BookID, IsArchived, SortOrder);
GO
IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = N'IX_Scenes_ChapterID_SortOrder' AND object_id = OBJECT_ID(N'dbo.Scenes'))
CREATE INDEX IX_Scenes_ChapterID_SortOrder ON dbo.Scenes(ChapterID, IsArchived, SortOrder);
GO
MERGE dbo.RevisionStatuses AS target
USING (VALUES
(N'Planned', 10), (N'Outlined', 20), (N'Drafted', 30), (N'Needs Work', 40),
(N'Revised', 50), (N'Polished', 60), (N'Locked', 70), (N'Cut Candidate', 80)
) AS source (StatusName, SortOrder)
ON target.StatusName = source.StatusName
WHEN NOT MATCHED THEN INSERT (StatusName, SortOrder) VALUES (source.StatusName, source.SortOrder);
GO
MERGE dbo.TimeModes AS target
USING (VALUES
(N'Exact DateTime', 10), (N'Exact Date', 20), (N'Relative', 30), (N'Approximate', 40),
(N'Unknown', 50), (N'Flashback', 60), (N'Timeless / Memory', 70)
) AS source (TimeModeName, SortOrder)
ON target.TimeModeName = source.TimeModeName
WHEN NOT MATCHED THEN INSERT (TimeModeName, SortOrder) VALUES (source.TimeModeName, source.SortOrder);
GO
MERGE dbo.DurationUnits AS target
USING (VALUES
(N'Minutes', 10), (N'Hours', 20), (N'Days', 30), (N'Weeks', 40),
(N'Months', 50), (N'Years', 60), (N'Unknown', 70)
) AS source (DurationUnitName, SortOrder)
ON target.DurationUnitName = source.DurationUnitName
WHEN NOT MATCHED THEN INSERT (DurationUnitName, SortOrder) VALUES (source.DurationUnitName, source.SortOrder);
GO
MERGE dbo.TimeConfidences AS target
USING (VALUES
(N'Confirmed', 10), (N'Implied', 20), (N'Estimated', 30), (N'Unknown', 40)
) AS source (TimeConfidenceName, SortOrder)
ON target.TimeConfidenceName = source.TimeConfidenceName
WHEN NOT MATCHED THEN INSERT (TimeConfidenceName, SortOrder) VALUES (source.TimeConfidenceName, source.SortOrder);
GO
MERGE dbo.ScenePurposeTypes AS target
USING (VALUES
(N'Advances Plot', 10), (N'Character Development', 20), (N'Relationship Development', 30),
(N'Setup', 40), (N'Payoff', 50), (N'Reveal', 60), (N'Exposition', 70),
(N'Mystery', 80), (N'Emotional Recovery', 90), (N'Comic Relief', 100),
(N'Action', 110), (N'Transition / Travel', 120), (N'Location Change', 130),
(N'Time Passage', 140), (N'Consequence', 150), (N'Reversal', 160)
) AS source (PurposeName, SortOrder)
ON target.PurposeName = source.PurposeName
WHEN NOT MATCHED THEN INSERT (PurposeName, SortOrder) VALUES (source.PurposeName, source.SortOrder);
GO
CREATE OR ALTER PROCEDURE dbo.Project_List
AS
BEGIN
SET NOCOUNT ON;
SELECT ProjectID, ProjectName, Description, CreatedDate, UpdatedDate, IsArchived
FROM dbo.Projects
WHERE IsArchived = 0
ORDER BY ProjectName;
END;
GO
CREATE OR ALTER PROCEDURE dbo.Project_Get
@ProjectID int
AS
BEGIN
SET NOCOUNT ON;
SELECT ProjectID, ProjectName, Description, CreatedDate, UpdatedDate, IsArchived
FROM dbo.Projects
WHERE ProjectID = @ProjectID AND IsArchived = 0;
END;
GO
CREATE OR ALTER PROCEDURE dbo.Project_Save
@ProjectID int = NULL,
@ProjectName nvarchar(200),
@Description nvarchar(max) = NULL
AS
BEGIN
SET NOCOUNT ON;
IF @ProjectID IS NULL OR @ProjectID = 0
BEGIN
INSERT dbo.Projects (ProjectName, Description)
VALUES (@ProjectName, @Description);
SELECT CAST(SCOPE_IDENTITY() AS int) AS ProjectID;
RETURN;
END;
UPDATE dbo.Projects
SET ProjectName = @ProjectName,
Description = @Description,
UpdatedDate = SYSUTCDATETIME()
WHERE ProjectID = @ProjectID;
SELECT @ProjectID AS ProjectID;
END;
GO
CREATE OR ALTER PROCEDURE dbo.Project_Archive
@ProjectID int
AS
BEGIN
SET NOCOUNT ON;
UPDATE dbo.Projects SET IsArchived = 1, UpdatedDate = SYSUTCDATETIME() WHERE ProjectID = @ProjectID;
END;
GO
CREATE OR ALTER PROCEDURE dbo.Book_ListByProject
@ProjectID int
AS
BEGIN
SET NOCOUNT ON;
SELECT BookID, ProjectID, BookTitle, BookNumber, Description, SortOrder, CreatedDate, UpdatedDate, IsArchived
FROM dbo.Books
WHERE ProjectID = @ProjectID AND IsArchived = 0
ORDER BY SortOrder, BookNumber, BookTitle;
END;
GO
CREATE OR ALTER PROCEDURE dbo.Book_Get
@BookID int
AS
BEGIN
SET NOCOUNT ON;
SELECT BookID, ProjectID, BookTitle, BookNumber, Description, SortOrder, CreatedDate, UpdatedDate, IsArchived
FROM dbo.Books
WHERE BookID = @BookID AND IsArchived = 0;
END;
GO
CREATE OR ALTER PROCEDURE dbo.Book_Save
@BookID int = NULL,
@ProjectID int,
@BookTitle nvarchar(200),
@BookNumber int,
@Description nvarchar(max) = NULL
AS
BEGIN
SET NOCOUNT ON;
IF @BookID IS NULL OR @BookID = 0
BEGIN
DECLARE @NextBookOrder int = ISNULL((SELECT MAX(SortOrder) FROM dbo.Books WHERE ProjectID = @ProjectID), 0) + 10;
INSERT dbo.Books (ProjectID, BookTitle, BookNumber, Description, SortOrder)
VALUES (@ProjectID, @BookTitle, @BookNumber, @Description, @NextBookOrder);
SELECT CAST(SCOPE_IDENTITY() AS int) AS BookID;
RETURN;
END;
UPDATE dbo.Books
SET BookTitle = @BookTitle,
BookNumber = @BookNumber,
Description = @Description,
UpdatedDate = SYSUTCDATETIME()
WHERE BookID = @BookID;
SELECT @BookID AS BookID;
END;
GO
CREATE OR ALTER PROCEDURE dbo.Book_Archive
@BookID int
AS
BEGIN
SET NOCOUNT ON;
UPDATE dbo.Books SET IsArchived = 1, UpdatedDate = SYSUTCDATETIME() WHERE BookID = @BookID;
END;
GO
CREATE OR ALTER PROCEDURE dbo.Chapter_ListByBook
@BookID int
AS
BEGIN
SET NOCOUNT ON;
SELECT c.ChapterID, c.BookID, c.ChapterNumber, c.ChapterTitle, c.POVCharacterID, c.Summary,
c.SortOrder, c.RevisionStatusID, rs.StatusName AS RevisionStatusName,
c.CreatedDate, c.UpdatedDate, c.IsArchived
FROM dbo.Chapters c
INNER JOIN dbo.RevisionStatuses rs ON rs.RevisionStatusID = c.RevisionStatusID
WHERE c.BookID = @BookID AND c.IsArchived = 0
ORDER BY c.SortOrder, c.ChapterNumber, c.ChapterTitle;
END;
GO
CREATE OR ALTER PROCEDURE dbo.Chapter_Get
@ChapterID int
AS
BEGIN
SET NOCOUNT ON;
SELECT c.ChapterID, c.BookID, c.ChapterNumber, c.ChapterTitle, c.POVCharacterID, c.Summary,
c.SortOrder, c.RevisionStatusID, rs.StatusName AS RevisionStatusName,
c.CreatedDate, c.UpdatedDate, c.IsArchived
FROM dbo.Chapters c
INNER JOIN dbo.RevisionStatuses rs ON rs.RevisionStatusID = c.RevisionStatusID
WHERE c.ChapterID = @ChapterID AND c.IsArchived = 0;
END;
GO
CREATE OR ALTER PROCEDURE dbo.Chapter_Save
@ChapterID int = NULL,
@BookID int,
@ChapterNumber decimal(9,2),
@ChapterTitle nvarchar(200),
@Summary nvarchar(max) = NULL,
@RevisionStatusID int
AS
BEGIN
SET NOCOUNT ON;
IF @ChapterID IS NULL OR @ChapterID = 0
BEGIN
DECLARE @NextChapterOrder int = ISNULL((SELECT MAX(SortOrder) FROM dbo.Chapters WHERE BookID = @BookID), 0) + 10;
INSERT dbo.Chapters (BookID, ChapterNumber, ChapterTitle, Summary, SortOrder, RevisionStatusID)
VALUES (@BookID, @ChapterNumber, @ChapterTitle, @Summary, @NextChapterOrder, @RevisionStatusID);
SELECT CAST(SCOPE_IDENTITY() AS int) AS ChapterID;
RETURN;
END;
UPDATE dbo.Chapters
SET ChapterNumber = @ChapterNumber,
ChapterTitle = @ChapterTitle,
Summary = @Summary,
RevisionStatusID = @RevisionStatusID,
UpdatedDate = SYSUTCDATETIME()
WHERE ChapterID = @ChapterID;
SELECT @ChapterID AS ChapterID;
END;
GO
CREATE OR ALTER PROCEDURE dbo.Chapter_Archive
@ChapterID int
AS
BEGIN
SET NOCOUNT ON;
UPDATE dbo.Chapters SET IsArchived = 1, UpdatedDate = SYSUTCDATETIME() WHERE ChapterID = @ChapterID;
END;
GO
CREATE OR ALTER PROCEDURE dbo.Scene_ListByChapter
@ChapterID int
AS
BEGIN
SET NOCOUNT ON;
SELECT s.SceneID, s.ChapterID, s.SceneNumber, s.SceneTitle, s.Summary, s.POVCharacterID,
s.PrimaryLocationID, s.SortOrder, s.TimeModeID, tm.TimeModeName, s.StartDateTime,
s.EndDateTime, s.DurationAmount, s.DurationUnitID, du.DurationUnitName, s.RelativeTimeText,
s.TimeConfidenceID, tc.TimeConfidenceName, s.ScenePurposeNotes, s.SceneOutcomeNotes,
s.RevisionStatusID, rs.StatusName AS RevisionStatusName, s.CreatedDate, s.UpdatedDate, s.IsArchived
FROM dbo.Scenes s
INNER JOIN dbo.TimeModes tm ON tm.TimeModeID = s.TimeModeID
LEFT JOIN dbo.DurationUnits du ON du.DurationUnitID = s.DurationUnitID
INNER JOIN dbo.TimeConfidences tc ON tc.TimeConfidenceID = s.TimeConfidenceID
INNER JOIN dbo.RevisionStatuses rs ON rs.RevisionStatusID = s.RevisionStatusID
WHERE s.ChapterID = @ChapterID AND s.IsArchived = 0
ORDER BY s.SortOrder, s.SceneNumber, s.SceneTitle;
END;
GO
CREATE OR ALTER PROCEDURE dbo.Scene_Get
@SceneID int
AS
BEGIN
SET NOCOUNT ON;
SELECT s.SceneID, s.ChapterID, s.SceneNumber, s.SceneTitle, s.Summary, s.POVCharacterID,
s.PrimaryLocationID, s.SortOrder, s.TimeModeID, tm.TimeModeName, s.StartDateTime,
s.EndDateTime, s.DurationAmount, s.DurationUnitID, du.DurationUnitName, s.RelativeTimeText,
s.TimeConfidenceID, tc.TimeConfidenceName, s.ScenePurposeNotes, s.SceneOutcomeNotes,
s.RevisionStatusID, rs.StatusName AS RevisionStatusName, s.CreatedDate, s.UpdatedDate, s.IsArchived
FROM dbo.Scenes s
INNER JOIN dbo.TimeModes tm ON tm.TimeModeID = s.TimeModeID
LEFT JOIN dbo.DurationUnits du ON du.DurationUnitID = s.DurationUnitID
INNER JOIN dbo.TimeConfidences tc ON tc.TimeConfidenceID = s.TimeConfidenceID
INNER JOIN dbo.RevisionStatuses rs ON rs.RevisionStatusID = s.RevisionStatusID
WHERE s.SceneID = @SceneID AND s.IsArchived = 0;
SELECT ScenePurposeTypeID
FROM dbo.ScenePurposes
WHERE SceneID = @SceneID;
END;
GO
CREATE OR ALTER PROCEDURE dbo.Scene_Save
@SceneID int = NULL,
@ChapterID int,
@SceneNumber decimal(9,2),
@SceneTitle nvarchar(200),
@Summary nvarchar(max) = NULL,
@TimeModeID int,
@StartDateTime datetime2 = NULL,
@EndDateTime datetime2 = NULL,
@DurationAmount decimal(9,2) = NULL,
@DurationUnitID int = NULL,
@RelativeTimeText nvarchar(200) = NULL,
@TimeConfidenceID int,
@ScenePurposeNotes nvarchar(max) = NULL,
@SceneOutcomeNotes nvarchar(max) = NULL,
@RevisionStatusID int
AS
BEGIN
SET NOCOUNT ON;
IF @SceneID IS NULL OR @SceneID = 0
BEGIN
DECLARE @NextSceneOrder int = ISNULL((SELECT MAX(SortOrder) FROM dbo.Scenes WHERE ChapterID = @ChapterID), 0) + 10;
INSERT dbo.Scenes
(
ChapterID, SceneNumber, SceneTitle, Summary, SortOrder, TimeModeID, StartDateTime,
EndDateTime, DurationAmount, DurationUnitID, RelativeTimeText, TimeConfidenceID,
ScenePurposeNotes, SceneOutcomeNotes, RevisionStatusID
)
VALUES
(
@ChapterID, @SceneNumber, @SceneTitle, @Summary, @NextSceneOrder, @TimeModeID, @StartDateTime,
@EndDateTime, @DurationAmount, @DurationUnitID, @RelativeTimeText, @TimeConfidenceID,
@ScenePurposeNotes, @SceneOutcomeNotes, @RevisionStatusID
);
SELECT CAST(SCOPE_IDENTITY() AS int) AS SceneID;
RETURN;
END;
UPDATE dbo.Scenes
SET SceneNumber = @SceneNumber,
SceneTitle = @SceneTitle,
Summary = @Summary,
TimeModeID = @TimeModeID,
StartDateTime = @StartDateTime,
EndDateTime = @EndDateTime,
DurationAmount = @DurationAmount,
DurationUnitID = @DurationUnitID,
RelativeTimeText = @RelativeTimeText,
TimeConfidenceID = @TimeConfidenceID,
ScenePurposeNotes = @ScenePurposeNotes,
SceneOutcomeNotes = @SceneOutcomeNotes,
RevisionStatusID = @RevisionStatusID,
UpdatedDate = SYSUTCDATETIME()
WHERE SceneID = @SceneID;
SELECT @SceneID AS SceneID;
END;
GO
CREATE OR ALTER PROCEDURE dbo.Scene_SavePurposes
@SceneID int,
@PurposeIds nvarchar(max)
AS
BEGIN
SET NOCOUNT ON;
DELETE FROM dbo.ScenePurposes WHERE SceneID = @SceneID;
INSERT dbo.ScenePurposes (SceneID, ScenePurposeTypeID)
SELECT @SceneID, TRY_CAST(value AS int)
FROM STRING_SPLIT(ISNULL(@PurposeIds, N''), N',')
WHERE TRY_CAST(value AS int) IS NOT NULL;
END;
GO
CREATE OR ALTER PROCEDURE dbo.Scene_Archive
@SceneID int
AS
BEGIN
SET NOCOUNT ON;
UPDATE dbo.Scenes SET IsArchived = 1, UpdatedDate = SYSUTCDATETIME() WHERE SceneID = @SceneID;
END;
GO
CREATE OR ALTER PROCEDURE dbo.Scene_Move
@SceneID int,
@Direction nvarchar(10)
AS
BEGIN
SET NOCOUNT ON;
DECLARE @ChapterID int;
DECLARE @CurrentSortOrder int;
DECLARE @SwapSceneID int;
DECLARE @SwapSortOrder int;
SELECT @ChapterID = ChapterID, @CurrentSortOrder = SortOrder
FROM dbo.Scenes
WHERE SceneID = @SceneID AND IsArchived = 0;
IF @ChapterID IS NULL RETURN;
IF @Direction = N'Up'
BEGIN
SELECT TOP (1) @SwapSceneID = SceneID, @SwapSortOrder = SortOrder
FROM dbo.Scenes
WHERE ChapterID = @ChapterID AND IsArchived = 0 AND SortOrder < @CurrentSortOrder
ORDER BY SortOrder DESC;
END
ELSE
BEGIN
SELECT TOP (1) @SwapSceneID = SceneID, @SwapSortOrder = SortOrder
FROM dbo.Scenes
WHERE ChapterID = @ChapterID AND IsArchived = 0 AND SortOrder > @CurrentSortOrder
ORDER BY SortOrder ASC;
END;
IF @SwapSceneID IS NULL RETURN;
UPDATE dbo.Scenes SET SortOrder = @SwapSortOrder, UpdatedDate = SYSUTCDATETIME() WHERE SceneID = @SceneID;
UPDATE dbo.Scenes SET SortOrder = @CurrentSortOrder, UpdatedDate = SYSUTCDATETIME() WHERE SceneID = @SwapSceneID;
END;
GO
CREATE OR ALTER PROCEDURE dbo.Lookup_All
AS
BEGIN
SET NOCOUNT ON;
SELECT RevisionStatusID, StatusName, SortOrder FROM dbo.RevisionStatuses ORDER BY SortOrder;
SELECT TimeModeID, TimeModeName, SortOrder FROM dbo.TimeModes ORDER BY SortOrder;
SELECT DurationUnitID, DurationUnitName, SortOrder FROM dbo.DurationUnits ORDER BY SortOrder;
SELECT TimeConfidenceID, TimeConfidenceName, SortOrder FROM dbo.TimeConfidences ORDER BY SortOrder;
SELECT ScenePurposeTypeID, PurposeName, SortOrder FROM dbo.ScenePurposeTypes WHERE IsArchived = 0 ORDER BY SortOrder;
END;
GO
CREATE OR ALTER PROCEDURE dbo.Timeline_GetByProject
@ProjectID int,
@BookID int = NULL
AS
BEGIN
SET NOCOUNT ON;
SELECT ProjectID, ProjectName, Description, CreatedDate, UpdatedDate, IsArchived
FROM dbo.Projects
WHERE ProjectID = @ProjectID AND IsArchived = 0;
SELECT BookID, ProjectID, BookTitle, BookNumber, Description, SortOrder, CreatedDate, UpdatedDate, IsArchived
FROM dbo.Books
WHERE ProjectID = @ProjectID AND IsArchived = 0 AND (@BookID IS NULL OR BookID = @BookID)
ORDER BY SortOrder, BookNumber;
SELECT c.ChapterID, c.BookID, c.ChapterNumber, c.ChapterTitle, c.POVCharacterID, c.Summary,
c.SortOrder, c.RevisionStatusID, rs.StatusName AS RevisionStatusName,
c.CreatedDate, c.UpdatedDate, c.IsArchived
FROM dbo.Chapters c
INNER JOIN dbo.Books b ON b.BookID = c.BookID
INNER JOIN dbo.RevisionStatuses rs ON rs.RevisionStatusID = c.RevisionStatusID
WHERE b.ProjectID = @ProjectID AND b.IsArchived = 0 AND c.IsArchived = 0 AND (@BookID IS NULL OR b.BookID = @BookID)
ORDER BY b.SortOrder, c.SortOrder, c.ChapterNumber;
SELECT s.SceneID, s.ChapterID, s.SceneNumber, s.SceneTitle, s.Summary, s.POVCharacterID,
s.PrimaryLocationID, s.SortOrder, s.TimeModeID, tm.TimeModeName, s.StartDateTime,
s.EndDateTime, s.DurationAmount, s.DurationUnitID, du.DurationUnitName, s.RelativeTimeText,
s.TimeConfidenceID, tc.TimeConfidenceName, s.ScenePurposeNotes, s.SceneOutcomeNotes,
s.RevisionStatusID, rs.StatusName AS RevisionStatusName, s.CreatedDate, s.UpdatedDate, s.IsArchived
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.TimeModes tm ON tm.TimeModeID = s.TimeModeID
LEFT JOIN dbo.DurationUnits du ON du.DurationUnitID = s.DurationUnitID
INNER JOIN dbo.TimeConfidences tc ON tc.TimeConfidenceID = s.TimeConfidenceID
INNER JOIN dbo.RevisionStatuses rs ON rs.RevisionStatusID = s.RevisionStatusID
WHERE b.ProjectID = @ProjectID AND b.IsArchived = 0 AND c.IsArchived = 0 AND s.IsArchived = 0 AND (@BookID IS NULL OR b.BookID = @BookID)
ORDER BY b.SortOrder, c.SortOrder, s.SortOrder, s.SceneNumber;
END;
GO

View File

@ -0,0 +1,258 @@
SET ANSI_NULLS ON;
GO
SET QUOTED_IDENTIFIER ON;
GO
IF OBJECT_ID(N'dbo.SceneMetricTypes', N'U') IS NULL
BEGIN
CREATE TABLE dbo.SceneMetricTypes
(
MetricTypeID int IDENTITY(1,1) NOT NULL CONSTRAINT PK_SceneMetricTypes PRIMARY KEY,
ProjectID int NULL,
MetricName nvarchar(100) NOT NULL,
Description nvarchar(500) NULL,
MinValue int NOT NULL CONSTRAINT DF_SceneMetricTypes_MinValue DEFAULT 1,
MaxValue int NOT NULL CONSTRAINT DF_SceneMetricTypes_MaxValue DEFAULT 10,
DefaultValue int NOT NULL CONSTRAINT DF_SceneMetricTypes_DefaultValue DEFAULT 5,
SortOrder int NOT NULL,
IsActive bit NOT NULL CONSTRAINT DF_SceneMetricTypes_IsActive DEFAULT 1,
CONSTRAINT FK_SceneMetricTypes_Projects FOREIGN KEY (ProjectID) REFERENCES dbo.Projects(ProjectID),
CONSTRAINT CK_SceneMetricTypes_Range CHECK (MinValue < MaxValue AND DefaultValue BETWEEN MinValue AND MaxValue)
);
END;
GO
IF OBJECT_ID(N'dbo.SceneMetricValues', N'U') IS NULL
BEGIN
CREATE TABLE dbo.SceneMetricValues
(
SceneID int NOT NULL,
MetricTypeID int NOT NULL,
Value int NOT NULL,
Notes nvarchar(500) NULL,
CONSTRAINT PK_SceneMetricValues PRIMARY KEY (SceneID, MetricTypeID),
CONSTRAINT FK_SceneMetricValues_Scenes FOREIGN KEY (SceneID) REFERENCES dbo.Scenes(SceneID),
CONSTRAINT FK_SceneMetricValues_SceneMetricTypes FOREIGN KEY (MetricTypeID) REFERENCES dbo.SceneMetricTypes(MetricTypeID),
CONSTRAINT CK_SceneMetricValues_Value CHECK (Value BETWEEN 1 AND 10)
);
END;
GO
IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = N'IX_SceneMetricTypes_ProjectID_SortOrder' AND object_id = OBJECT_ID(N'dbo.SceneMetricTypes'))
CREATE INDEX IX_SceneMetricTypes_ProjectID_SortOrder ON dbo.SceneMetricTypes(ProjectID, IsActive, SortOrder);
GO
MERGE dbo.SceneMetricTypes AS target
USING (VALUES
(N'Overall Intensity', N'General dramatic intensity for the scene.', 1, 10, 5, 10),
(N'Tension', N'Pressure, suspense, uncertainty, or conflict.', 1, 10, 5, 20),
(N'Emotional Weight', N'Emotional load carried by the scene.', 1, 10, 5, 30),
(N'Action', N'Physical action, movement, or event energy.', 1, 10, 3, 40),
(N'Darkness', N'Darkness, bleakness, fear, or threat.', 1, 10, 3, 50),
(N'Romance', N'Romantic focus or intimacy.', 1, 10, 1, 60),
(N'Sexual Charge', N'Sexual tension or sensual charge.', 1, 10, 1, 70),
(N'Violence', N'Violent action, injury, or threat.', 1, 10, 1, 80),
(N'Hope / Lightness', N'Hope, relief, warmth, or tonal lightness.', 1, 10, 4, 90),
(N'Comedy', N'Humour or comic relief.', 1, 10, 1, 100),
(N'Mystery', N'Questions, clue energy, and mystery pressure.', 1, 10, 5, 110)
) AS source (MetricName, Description, MinValue, MaxValue, DefaultValue, SortOrder)
ON target.ProjectID IS NULL AND target.MetricName = source.MetricName
WHEN NOT MATCHED THEN
INSERT (ProjectID, MetricName, Description, MinValue, MaxValue, DefaultValue, SortOrder, IsActive)
VALUES (NULL, source.MetricName, source.Description, source.MinValue, source.MaxValue, source.DefaultValue, source.SortOrder, 1);
GO
CREATE OR ALTER PROCEDURE dbo.SceneMetricType_List
@ProjectID int = NULL
AS
BEGIN
SET NOCOUNT ON;
SELECT MetricTypeID, ProjectID, MetricName, Description, MinValue, MaxValue, DefaultValue, SortOrder, IsActive
FROM dbo.SceneMetricTypes
WHERE IsActive = 1 AND (ProjectID IS NULL OR ProjectID = @ProjectID)
ORDER BY SortOrder, MetricName;
END;
GO
CREATE OR ALTER PROCEDURE dbo.SceneMetric_EnsureDefaults
@SceneID int
AS
BEGIN
SET NOCOUNT ON;
DECLARE @ProjectID 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
WHERE s.SceneID = @SceneID;
INSERT dbo.SceneMetricValues (SceneID, MetricTypeID, Value, Notes)
SELECT @SceneID, mt.MetricTypeID, mt.DefaultValue, NULL
FROM dbo.SceneMetricTypes mt
WHERE mt.IsActive = 1
AND (mt.ProjectID IS NULL OR mt.ProjectID = @ProjectID)
AND NOT EXISTS
(
SELECT 1
FROM dbo.SceneMetricValues smv
WHERE smv.SceneID = @SceneID AND smv.MetricTypeID = mt.MetricTypeID
);
END;
GO
CREATE OR ALTER PROCEDURE dbo.SceneMetric_ListByScene
@SceneID int
AS
BEGIN
SET NOCOUNT ON;
EXEC dbo.SceneMetric_EnsureDefaults @SceneID = @SceneID;
SELECT mt.MetricTypeID, mt.ProjectID, mt.MetricName, mt.Description, mt.MinValue, mt.MaxValue,
mt.DefaultValue, mt.SortOrder, mt.IsActive, smv.SceneID, smv.Value, smv.Notes
FROM dbo.SceneMetricTypes mt
INNER JOIN dbo.SceneMetricValues smv ON smv.MetricTypeID = mt.MetricTypeID AND smv.SceneID = @SceneID
WHERE mt.IsActive = 1
ORDER BY mt.SortOrder, mt.MetricName;
END;
GO
CREATE OR ALTER PROCEDURE dbo.SceneMetric_SaveValue
@SceneID int,
@MetricTypeID int,
@Value int,
@Notes nvarchar(500) = NULL
AS
BEGIN
SET NOCOUNT ON;
DECLARE @MinValue int;
DECLARE @MaxValue int;
SELECT @MinValue = MinValue, @MaxValue = MaxValue
FROM dbo.SceneMetricTypes
WHERE MetricTypeID = @MetricTypeID AND IsActive = 1;
IF @MinValue IS NULL
THROW 50001, 'Metric type does not exist.', 1;
IF @Value < @MinValue OR @Value > @MaxValue
THROW 50002, 'Metric value is outside the configured range.', 1;
MERGE dbo.SceneMetricValues AS target
USING (SELECT @SceneID AS SceneID, @MetricTypeID AS MetricTypeID) AS source
ON target.SceneID = source.SceneID AND target.MetricTypeID = source.MetricTypeID
WHEN MATCHED THEN
UPDATE SET Value = @Value, Notes = @Notes
WHEN NOT MATCHED THEN
INSERT (SceneID, MetricTypeID, Value, Notes)
VALUES (@SceneID, @MetricTypeID, @Value, @Notes);
END;
GO
CREATE OR ALTER PROCEDURE dbo.Scene_Get
@SceneID int
AS
BEGIN
SET NOCOUNT ON;
EXEC dbo.SceneMetric_EnsureDefaults @SceneID = @SceneID;
SELECT s.SceneID, s.ChapterID, s.SceneNumber, s.SceneTitle, s.Summary, s.POVCharacterID,
s.PrimaryLocationID, s.SortOrder, s.TimeModeID, tm.TimeModeName, s.StartDateTime,
s.EndDateTime, s.DurationAmount, s.DurationUnitID, du.DurationUnitName, s.RelativeTimeText,
s.TimeConfidenceID, tc.TimeConfidenceName, s.ScenePurposeNotes, s.SceneOutcomeNotes,
s.RevisionStatusID, rs.StatusName AS RevisionStatusName, s.CreatedDate, s.UpdatedDate, s.IsArchived
FROM dbo.Scenes s
INNER JOIN dbo.TimeModes tm ON tm.TimeModeID = s.TimeModeID
LEFT JOIN dbo.DurationUnits du ON du.DurationUnitID = s.DurationUnitID
INNER JOIN dbo.TimeConfidences tc ON tc.TimeConfidenceID = s.TimeConfidenceID
INNER JOIN dbo.RevisionStatuses rs ON rs.RevisionStatusID = s.RevisionStatusID
WHERE s.SceneID = @SceneID AND s.IsArchived = 0;
SELECT sp.ScenePurposeTypeID
FROM dbo.ScenePurposes sp
WHERE sp.SceneID = @SceneID;
SELECT mt.MetricTypeID, mt.ProjectID, mt.MetricName, mt.Description, mt.MinValue, mt.MaxValue,
mt.DefaultValue, mt.SortOrder, mt.IsActive, smv.SceneID, smv.Value, smv.Notes
FROM dbo.SceneMetricTypes mt
INNER JOIN dbo.SceneMetricValues smv ON smv.MetricTypeID = mt.MetricTypeID AND smv.SceneID = @SceneID
WHERE mt.IsActive = 1
ORDER BY mt.SortOrder, mt.MetricName;
END;
GO
CREATE OR ALTER PROCEDURE dbo.Timeline_GetByProject
@ProjectID int,
@BookID int = NULL
AS
BEGIN
SET NOCOUNT ON;
SELECT ProjectID, ProjectName, Description, CreatedDate, UpdatedDate, IsArchived
FROM dbo.Projects
WHERE ProjectID = @ProjectID AND IsArchived = 0;
SELECT BookID, ProjectID, BookTitle, BookNumber, Description, SortOrder, CreatedDate, UpdatedDate, IsArchived
FROM dbo.Books
WHERE ProjectID = @ProjectID AND IsArchived = 0 AND (@BookID IS NULL OR BookID = @BookID)
ORDER BY SortOrder, BookNumber;
SELECT c.ChapterID, c.BookID, c.ChapterNumber, c.ChapterTitle, c.POVCharacterID, c.Summary,
c.SortOrder, c.RevisionStatusID, rs.StatusName AS RevisionStatusName,
c.CreatedDate, c.UpdatedDate, c.IsArchived
FROM dbo.Chapters c
INNER JOIN dbo.Books b ON b.BookID = c.BookID
INNER JOIN dbo.RevisionStatuses rs ON rs.RevisionStatusID = c.RevisionStatusID
WHERE b.ProjectID = @ProjectID AND b.IsArchived = 0 AND c.IsArchived = 0 AND (@BookID IS NULL OR b.BookID = @BookID)
ORDER BY b.SortOrder, c.SortOrder, c.ChapterNumber;
SELECT s.SceneID, s.ChapterID, s.SceneNumber, s.SceneTitle, s.Summary, s.POVCharacterID,
s.PrimaryLocationID, s.SortOrder, s.TimeModeID, tm.TimeModeName, s.StartDateTime,
s.EndDateTime, s.DurationAmount, s.DurationUnitID, du.DurationUnitName, s.RelativeTimeText,
s.TimeConfidenceID, tc.TimeConfidenceName, s.ScenePurposeNotes, s.SceneOutcomeNotes,
s.RevisionStatusID, rs.StatusName AS RevisionStatusName, s.CreatedDate, s.UpdatedDate, s.IsArchived
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.TimeModes tm ON tm.TimeModeID = s.TimeModeID
LEFT JOIN dbo.DurationUnits du ON du.DurationUnitID = s.DurationUnitID
INNER JOIN dbo.TimeConfidences tc ON tc.TimeConfidenceID = s.TimeConfidenceID
INNER JOIN dbo.RevisionStatuses rs ON rs.RevisionStatusID = s.RevisionStatusID
WHERE b.ProjectID = @ProjectID AND b.IsArchived = 0 AND c.IsArchived = 0 AND s.IsArchived = 0 AND (@BookID IS NULL OR b.BookID = @BookID)
ORDER BY b.SortOrder, c.SortOrder, s.SortOrder, s.SceneNumber;
SELECT sp.SceneID, spt.ScenePurposeTypeID, spt.PurposeName, spt.SortOrder
FROM dbo.ScenePurposes sp
INNER JOIN dbo.ScenePurposeTypes spt ON spt.ScenePurposeTypeID = sp.ScenePurposeTypeID
INNER JOIN dbo.Scenes s ON s.SceneID = sp.SceneID
INNER JOIN dbo.Chapters c ON c.ChapterID = s.ChapterID
INNER JOIN dbo.Books b ON b.BookID = c.BookID
WHERE b.ProjectID = @ProjectID AND b.IsArchived = 0 AND c.IsArchived = 0 AND s.IsArchived = 0 AND (@BookID IS NULL OR b.BookID = @BookID)
ORDER BY sp.SceneID, spt.SortOrder;
SELECT MetricTypeID, ProjectID, MetricName, Description, MinValue, MaxValue, DefaultValue, SortOrder, IsActive
FROM dbo.SceneMetricTypes
WHERE IsActive = 1 AND (ProjectID IS NULL OR ProjectID = @ProjectID)
ORDER BY SortOrder, MetricName;
SELECT s.SceneID, mt.MetricTypeID, mt.MetricName, COALESCE(smv.Value, mt.DefaultValue) AS Value, smv.Notes
FROM dbo.Scenes s
INNER JOIN dbo.Chapters c ON c.ChapterID = s.ChapterID
INNER JOIN dbo.Books b ON b.BookID = c.BookID
CROSS JOIN dbo.SceneMetricTypes mt
LEFT JOIN dbo.SceneMetricValues smv ON smv.SceneID = s.SceneID AND smv.MetricTypeID = mt.MetricTypeID
WHERE b.ProjectID = @ProjectID
AND b.IsArchived = 0
AND c.IsArchived = 0
AND s.IsArchived = 0
AND mt.IsActive = 1
AND (mt.ProjectID IS NULL OR mt.ProjectID = @ProjectID)
AND (@BookID IS NULL OR b.BookID = @BookID)
ORDER BY b.SortOrder, c.SortOrder, s.SortOrder, mt.SortOrder;
END;
GO

View File

@ -0,0 +1,27 @@
SET ANSI_NULLS ON;
GO
SET QUOTED_IDENTIFIER ON;
GO
CREATE OR ALTER PROCEDURE dbo.Scene_SavePurposes
@SceneID int,
@PurposeIds nvarchar(max)
AS
BEGIN
SET NOCOUNT ON;
DELETE FROM dbo.ScenePurposes WHERE SceneID = @SceneID;
INSERT dbo.ScenePurposes (SceneID, ScenePurposeTypeID)
SELECT DISTINCT @SceneID, parsed.ScenePurposeTypeID
FROM
(
SELECT TRY_CAST(value AS int) AS ScenePurposeTypeID
FROM STRING_SPLIT(ISNULL(@PurposeIds, N''), N',')
) parsed
INNER JOIN dbo.ScenePurposeTypes spt ON spt.ScenePurposeTypeID = parsed.ScenePurposeTypeID
WHERE parsed.ScenePurposeTypeID IS NOT NULL
AND parsed.ScenePurposeTypeID > 0
AND spt.IsArchived = 0;
END;
GO

View File

@ -0,0 +1,603 @@
SET ANSI_NULLS ON;
GO
SET QUOTED_IDENTIFIER ON;
GO
IF OBJECT_ID(N'dbo.PlotLineTypes', N'U') IS NULL
BEGIN
CREATE TABLE dbo.PlotLineTypes
(
PlotLineTypeID int IDENTITY(1,1) NOT NULL CONSTRAINT PK_PlotLineTypes PRIMARY KEY,
TypeName nvarchar(100) NOT NULL,
SortOrder int NOT NULL,
IsActive bit NOT NULL CONSTRAINT DF_PlotLineTypes_IsActive DEFAULT 1,
CONSTRAINT UQ_PlotLineTypes_TypeName UNIQUE (TypeName)
);
END;
GO
IF OBJECT_ID(N'dbo.PlotLines', N'U') IS NULL
BEGIN
CREATE TABLE dbo.PlotLines
(
PlotLineID int IDENTITY(1,1) NOT NULL CONSTRAINT PK_PlotLines PRIMARY KEY,
ProjectID int NOT NULL,
BookID int NULL,
PlotLineName nvarchar(200) NOT NULL,
PlotLineTypeID int NOT NULL,
Description nvarchar(max) NULL,
ParentPlotLineID int NULL,
SortOrder int NOT NULL,
Colour nvarchar(20) NOT NULL CONSTRAINT DF_PlotLines_Colour DEFAULT N'#2f6f63',
IsMainPlot bit NOT NULL CONSTRAINT DF_PlotLines_IsMainPlot DEFAULT 0,
IsVisibleOnTimeline bit NOT NULL CONSTRAINT DF_PlotLines_IsVisibleOnTimeline DEFAULT 1,
CreatedDate datetime2 NOT NULL CONSTRAINT DF_PlotLines_CreatedDate DEFAULT SYSUTCDATETIME(),
UpdatedDate datetime2 NOT NULL CONSTRAINT DF_PlotLines_UpdatedDate DEFAULT SYSUTCDATETIME(),
IsArchived bit NOT NULL CONSTRAINT DF_PlotLines_IsArchived DEFAULT 0,
CONSTRAINT FK_PlotLines_Projects FOREIGN KEY (ProjectID) REFERENCES dbo.Projects(ProjectID),
CONSTRAINT FK_PlotLines_Books FOREIGN KEY (BookID) REFERENCES dbo.Books(BookID),
CONSTRAINT FK_PlotLines_PlotLineTypes FOREIGN KEY (PlotLineTypeID) REFERENCES dbo.PlotLineTypes(PlotLineTypeID),
CONSTRAINT FK_PlotLines_ParentPlotLines FOREIGN KEY (ParentPlotLineID) REFERENCES dbo.PlotLines(PlotLineID)
);
END;
GO
IF OBJECT_ID(N'dbo.ThreadTypes', N'U') IS NULL
BEGIN
CREATE TABLE dbo.ThreadTypes
(
ThreadTypeID int IDENTITY(1,1) NOT NULL CONSTRAINT PK_ThreadTypes PRIMARY KEY,
TypeName nvarchar(100) NOT NULL,
SortOrder int NOT NULL,
IsActive bit NOT NULL CONSTRAINT DF_ThreadTypes_IsActive DEFAULT 1,
CONSTRAINT UQ_ThreadTypes_TypeName UNIQUE (TypeName)
);
END;
GO
IF OBJECT_ID(N'dbo.ThreadStatuses', N'U') IS NULL
BEGIN
CREATE TABLE dbo.ThreadStatuses
(
ThreadStatusID int IDENTITY(1,1) NOT NULL CONSTRAINT PK_ThreadStatuses PRIMARY KEY,
StatusName nvarchar(100) NOT NULL,
SortOrder int NOT NULL,
IsOpenStatus bit NOT NULL,
IsResolvedStatus bit NOT NULL,
IsActive bit NOT NULL CONSTRAINT DF_ThreadStatuses_IsActive DEFAULT 1,
CONSTRAINT UQ_ThreadStatuses_StatusName UNIQUE (StatusName)
);
END;
GO
IF OBJECT_ID(N'dbo.PlotThreads', N'U') IS NULL
BEGIN
CREATE TABLE dbo.PlotThreads
(
PlotThreadID int IDENTITY(1,1) NOT NULL CONSTRAINT PK_PlotThreads PRIMARY KEY,
PlotLineID int NOT NULL,
ThreadTitle nvarchar(200) NOT NULL,
ThreadTypeID int NOT NULL,
ThreadStatusID int NOT NULL,
Importance int NOT NULL CONSTRAINT DF_PlotThreads_Importance DEFAULT 5,
Summary nvarchar(max) NULL,
IntroducedSceneID int NULL,
PlannedResolutionSceneID int NULL,
ActualResolutionSceneID int NULL,
CreatedDate datetime2 NOT NULL CONSTRAINT DF_PlotThreads_CreatedDate DEFAULT SYSUTCDATETIME(),
UpdatedDate datetime2 NOT NULL CONSTRAINT DF_PlotThreads_UpdatedDate DEFAULT SYSUTCDATETIME(),
IsArchived bit NOT NULL CONSTRAINT DF_PlotThreads_IsArchived DEFAULT 0,
CONSTRAINT FK_PlotThreads_PlotLines FOREIGN KEY (PlotLineID) REFERENCES dbo.PlotLines(PlotLineID),
CONSTRAINT FK_PlotThreads_ThreadTypes FOREIGN KEY (ThreadTypeID) REFERENCES dbo.ThreadTypes(ThreadTypeID),
CONSTRAINT FK_PlotThreads_ThreadStatuses FOREIGN KEY (ThreadStatusID) REFERENCES dbo.ThreadStatuses(ThreadStatusID),
CONSTRAINT FK_PlotThreads_IntroducedScene FOREIGN KEY (IntroducedSceneID) REFERENCES dbo.Scenes(SceneID),
CONSTRAINT FK_PlotThreads_PlannedResolutionScene FOREIGN KEY (PlannedResolutionSceneID) REFERENCES dbo.Scenes(SceneID),
CONSTRAINT FK_PlotThreads_ActualResolutionScene FOREIGN KEY (ActualResolutionSceneID) REFERENCES dbo.Scenes(SceneID),
CONSTRAINT CK_PlotThreads_Importance CHECK (Importance BETWEEN 1 AND 10)
);
END;
GO
IF OBJECT_ID(N'dbo.ThreadEventTypes', N'U') IS NULL
BEGIN
CREATE TABLE dbo.ThreadEventTypes
(
ThreadEventTypeID int IDENTITY(1,1) NOT NULL CONSTRAINT PK_ThreadEventTypes PRIMARY KEY,
TypeName nvarchar(100) NOT NULL,
MarkerText nvarchar(20) NOT NULL,
SortOrder int NOT NULL,
IsActive bit NOT NULL CONSTRAINT DF_ThreadEventTypes_IsActive DEFAULT 1,
CONSTRAINT UQ_ThreadEventTypes_TypeName UNIQUE (TypeName)
);
END;
GO
IF OBJECT_ID(N'dbo.ThreadEvents', N'U') IS NULL
BEGIN
CREATE TABLE dbo.ThreadEvents
(
ThreadEventID int IDENTITY(1,1) NOT NULL CONSTRAINT PK_ThreadEvents PRIMARY KEY,
PlotThreadID int NOT NULL,
SceneID int NOT NULL,
EventTypeID int NOT NULL,
EventTitle nvarchar(200) NOT NULL,
EventDescription nvarchar(max) NULL,
CreatedDate datetime2 NOT NULL CONSTRAINT DF_ThreadEvents_CreatedDate DEFAULT SYSUTCDATETIME(),
UpdatedDate datetime2 NOT NULL CONSTRAINT DF_ThreadEvents_UpdatedDate DEFAULT SYSUTCDATETIME(),
CONSTRAINT FK_ThreadEvents_PlotThreads FOREIGN KEY (PlotThreadID) REFERENCES dbo.PlotThreads(PlotThreadID),
CONSTRAINT FK_ThreadEvents_Scenes FOREIGN KEY (SceneID) REFERENCES dbo.Scenes(SceneID),
CONSTRAINT FK_ThreadEvents_ThreadEventTypes FOREIGN KEY (EventTypeID) REFERENCES dbo.ThreadEventTypes(ThreadEventTypeID)
);
END;
GO
IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = N'IX_PlotLines_ProjectID_SortOrder' AND object_id = OBJECT_ID(N'dbo.PlotLines'))
CREATE INDEX IX_PlotLines_ProjectID_SortOrder ON dbo.PlotLines(ProjectID, IsArchived, IsVisibleOnTimeline, SortOrder);
GO
IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = N'IX_PlotThreads_PlotLineID' AND object_id = OBJECT_ID(N'dbo.PlotThreads'))
CREATE INDEX IX_PlotThreads_PlotLineID ON dbo.PlotThreads(PlotLineID, IsArchived, ThreadStatusID);
GO
IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = N'IX_ThreadEvents_SceneID' AND object_id = OBJECT_ID(N'dbo.ThreadEvents'))
CREATE INDEX IX_ThreadEvents_SceneID ON dbo.ThreadEvents(SceneID, PlotThreadID, EventTypeID);
GO
MERGE dbo.PlotLineTypes AS target
USING (VALUES
(N'Main Plot', 10), (N'Side Plot', 20), (N'Mystery', 30), (N'Emotional Arc', 40),
(N'Relationship Arc', 50), (N'Character Arc', 60), (N'Object Trail', 70),
(N'Investigation', 80), (N'Location Plot', 90), (N'Theme', 100)
) AS source (TypeName, SortOrder)
ON target.TypeName = source.TypeName
WHEN NOT MATCHED THEN INSERT (TypeName, SortOrder, IsActive) VALUES (source.TypeName, source.SortOrder, 1);
GO
MERGE dbo.ThreadTypes AS target
USING (VALUES
(N'Question', 10), (N'Clue', 20), (N'Mystery', 30), (N'Suspicion', 40), (N'Promise', 50),
(N'Threat', 60), (N'Emotional Issue', 70), (N'Relationship Issue', 80), (N'Secret', 90),
(N'Continuity Item', 100), (N'Payoff Setup', 110)
) AS source (TypeName, SortOrder)
ON target.TypeName = source.TypeName
WHEN NOT MATCHED THEN INSERT (TypeName, SortOrder, IsActive) VALUES (source.TypeName, source.SortOrder, 1);
GO
MERGE dbo.ThreadStatuses AS target
USING (VALUES
(N'Planned', 10, 1, 0), (N'Introduced', 20, 1, 0), (N'Developing', 30, 1, 0),
(N'Dormant', 40, 1, 0), (N'Ready For Payoff', 50, 1, 0),
(N'Resolved', 60, 0, 1), (N'Abandoned', 70, 0, 1)
) AS source (StatusName, SortOrder, IsOpenStatus, IsResolvedStatus)
ON target.StatusName = source.StatusName
WHEN NOT MATCHED THEN
INSERT (StatusName, SortOrder, IsOpenStatus, IsResolvedStatus, IsActive)
VALUES (source.StatusName, source.SortOrder, source.IsOpenStatus, source.IsResolvedStatus, 1);
GO
MERGE dbo.ThreadEventTypes AS target
USING (VALUES
(N'Introduced', N'I', 10), (N'Mentioned', N'M', 20), (N'Developed', N'D', 30),
(N'Complicated', N'C', 40), (N'Clue Planted', N'clue', 50), (N'Question Raised', N'?', 60),
(N'Question Answered', N'A', 70), (N'False Lead', N'F', 80), (N'Revealed', N'Rv', 90),
(N'Payoff', N'P', 100), (N'Resolved', NCHAR(10003), 110), (N'Contradicted', N'!', 120),
(N'Retconned', N'Rt', 130), (N'Note', N'N', 140)
) AS source (TypeName, MarkerText, SortOrder)
ON target.TypeName = source.TypeName
WHEN NOT MATCHED THEN
INSERT (TypeName, MarkerText, SortOrder, IsActive)
VALUES (source.TypeName, source.MarkerText, source.SortOrder, 1);
GO
CREATE OR ALTER PROCEDURE dbo.PlotLookup_All
AS
BEGIN
SET NOCOUNT ON;
SELECT PlotLineTypeID, TypeName, SortOrder, IsActive FROM dbo.PlotLineTypes WHERE IsActive = 1 ORDER BY SortOrder;
SELECT ThreadTypeID, TypeName, SortOrder, IsActive FROM dbo.ThreadTypes WHERE IsActive = 1 ORDER BY SortOrder;
SELECT ThreadStatusID, StatusName, SortOrder, IsOpenStatus, IsResolvedStatus, IsActive FROM dbo.ThreadStatuses WHERE IsActive = 1 ORDER BY SortOrder;
SELECT ThreadEventTypeID, TypeName, MarkerText, SortOrder, IsActive FROM dbo.ThreadEventTypes WHERE IsActive = 1 ORDER BY SortOrder;
END;
GO
CREATE OR ALTER PROCEDURE dbo.PlotLine_ListByProject
@ProjectID int
AS
BEGIN
SET NOCOUNT ON;
SELECT pl.PlotLineID, pl.ProjectID, pl.BookID, b.BookTitle, pl.PlotLineName, pl.PlotLineTypeID,
plt.TypeName AS PlotLineTypeName, pl.Description, pl.ParentPlotLineID, parent.PlotLineName AS ParentPlotLineName,
pl.SortOrder, pl.Colour, pl.IsMainPlot, pl.IsVisibleOnTimeline, pl.CreatedDate, pl.UpdatedDate, pl.IsArchived
FROM dbo.PlotLines pl
INNER JOIN dbo.PlotLineTypes plt ON plt.PlotLineTypeID = pl.PlotLineTypeID
LEFT JOIN dbo.Books b ON b.BookID = pl.BookID
LEFT JOIN dbo.PlotLines parent ON parent.PlotLineID = pl.ParentPlotLineID
WHERE pl.ProjectID = @ProjectID AND pl.IsArchived = 0
ORDER BY pl.SortOrder, pl.PlotLineName;
END;
GO
CREATE OR ALTER PROCEDURE dbo.PlotLine_Get
@PlotLineID int
AS
BEGIN
SET NOCOUNT ON;
SELECT pl.PlotLineID, pl.ProjectID, pl.BookID, b.BookTitle, pl.PlotLineName, pl.PlotLineTypeID,
plt.TypeName AS PlotLineTypeName, pl.Description, pl.ParentPlotLineID, parent.PlotLineName AS ParentPlotLineName,
pl.SortOrder, pl.Colour, pl.IsMainPlot, pl.IsVisibleOnTimeline, pl.CreatedDate, pl.UpdatedDate, pl.IsArchived
FROM dbo.PlotLines pl
INNER JOIN dbo.PlotLineTypes plt ON plt.PlotLineTypeID = pl.PlotLineTypeID
LEFT JOIN dbo.Books b ON b.BookID = pl.BookID
LEFT JOIN dbo.PlotLines parent ON parent.PlotLineID = pl.ParentPlotLineID
WHERE pl.PlotLineID = @PlotLineID AND pl.IsArchived = 0;
END;
GO
CREATE OR ALTER PROCEDURE dbo.PlotLine_Save
@PlotLineID int = NULL,
@ProjectID int,
@BookID int = NULL,
@PlotLineName nvarchar(200),
@PlotLineTypeID int,
@Description nvarchar(max) = NULL,
@ParentPlotLineID int = NULL,
@SortOrder int = NULL,
@Colour nvarchar(20),
@IsMainPlot bit,
@IsVisibleOnTimeline bit
AS
BEGIN
SET NOCOUNT ON;
IF @PlotLineID IS NULL OR @PlotLineID = 0
BEGIN
DECLARE @NextSortOrder int = COALESCE(@SortOrder, ISNULL((SELECT MAX(SortOrder) FROM dbo.PlotLines WHERE ProjectID = @ProjectID), 0) + 10);
INSERT dbo.PlotLines (ProjectID, BookID, PlotLineName, PlotLineTypeID, Description, ParentPlotLineID, SortOrder, Colour, IsMainPlot, IsVisibleOnTimeline)
VALUES (@ProjectID, @BookID, @PlotLineName, @PlotLineTypeID, @Description, @ParentPlotLineID, @NextSortOrder, @Colour, @IsMainPlot, @IsVisibleOnTimeline);
SELECT CAST(SCOPE_IDENTITY() AS int);
RETURN;
END;
UPDATE dbo.PlotLines
SET BookID = @BookID,
PlotLineName = @PlotLineName,
PlotLineTypeID = @PlotLineTypeID,
Description = @Description,
ParentPlotLineID = @ParentPlotLineID,
SortOrder = COALESCE(@SortOrder, SortOrder),
Colour = @Colour,
IsMainPlot = @IsMainPlot,
IsVisibleOnTimeline = @IsVisibleOnTimeline,
UpdatedDate = SYSUTCDATETIME()
WHERE PlotLineID = @PlotLineID;
SELECT @PlotLineID;
END;
GO
CREATE OR ALTER PROCEDURE dbo.PlotLine_Archive
@PlotLineID int
AS
BEGIN
SET NOCOUNT ON;
UPDATE dbo.PlotLines SET IsArchived = 1, UpdatedDate = SYSUTCDATETIME() WHERE PlotLineID = @PlotLineID;
END;
GO
CREATE OR ALTER PROCEDURE dbo.PlotThread_ListByProject
@ProjectID int
AS
BEGIN
SET NOCOUNT ON;
SELECT pt.PlotThreadID, pt.PlotLineID, pl.ProjectID, pl.PlotLineName, pt.ThreadTitle,
pt.ThreadTypeID, tt.TypeName AS ThreadTypeName, pt.ThreadStatusID, ts.StatusName AS ThreadStatusName,
pt.Importance, pt.Summary, pt.IntroducedSceneID, pt.PlannedResolutionSceneID, pt.ActualResolutionSceneID,
pt.CreatedDate, pt.UpdatedDate, pt.IsArchived
FROM dbo.PlotThreads pt
INNER JOIN dbo.PlotLines pl ON pl.PlotLineID = pt.PlotLineID
INNER JOIN dbo.ThreadTypes tt ON tt.ThreadTypeID = pt.ThreadTypeID
INNER JOIN dbo.ThreadStatuses ts ON ts.ThreadStatusID = pt.ThreadStatusID
WHERE pl.ProjectID = @ProjectID AND pl.IsArchived = 0 AND pt.IsArchived = 0
ORDER BY pl.SortOrder, pt.Importance DESC, pt.ThreadTitle;
END;
GO
CREATE OR ALTER PROCEDURE dbo.PlotThread_ListByPlotLine
@PlotLineID int
AS
BEGIN
SET NOCOUNT ON;
SELECT pt.PlotThreadID, pt.PlotLineID, pl.ProjectID, pl.PlotLineName, pt.ThreadTitle,
pt.ThreadTypeID, tt.TypeName AS ThreadTypeName, pt.ThreadStatusID, ts.StatusName AS ThreadStatusName,
pt.Importance, pt.Summary, pt.IntroducedSceneID, pt.PlannedResolutionSceneID, pt.ActualResolutionSceneID,
pt.CreatedDate, pt.UpdatedDate, pt.IsArchived
FROM dbo.PlotThreads pt
INNER JOIN dbo.PlotLines pl ON pl.PlotLineID = pt.PlotLineID
INNER JOIN dbo.ThreadTypes tt ON tt.ThreadTypeID = pt.ThreadTypeID
INNER JOIN dbo.ThreadStatuses ts ON ts.ThreadStatusID = pt.ThreadStatusID
WHERE pt.PlotLineID = @PlotLineID AND pt.IsArchived = 0
ORDER BY pt.Importance DESC, pt.ThreadTitle;
END;
GO
CREATE OR ALTER PROCEDURE dbo.PlotThread_Get
@PlotThreadID int
AS
BEGIN
SET NOCOUNT ON;
SELECT pt.PlotThreadID, pt.PlotLineID, pl.ProjectID, pl.PlotLineName, pt.ThreadTitle,
pt.ThreadTypeID, tt.TypeName AS ThreadTypeName, pt.ThreadStatusID, ts.StatusName AS ThreadStatusName,
pt.Importance, pt.Summary, pt.IntroducedSceneID, pt.PlannedResolutionSceneID, pt.ActualResolutionSceneID,
pt.CreatedDate, pt.UpdatedDate, pt.IsArchived
FROM dbo.PlotThreads pt
INNER JOIN dbo.PlotLines pl ON pl.PlotLineID = pt.PlotLineID
INNER JOIN dbo.ThreadTypes tt ON tt.ThreadTypeID = pt.ThreadTypeID
INNER JOIN dbo.ThreadStatuses ts ON ts.ThreadStatusID = pt.ThreadStatusID
WHERE pt.PlotThreadID = @PlotThreadID AND pt.IsArchived = 0;
END;
GO
CREATE OR ALTER PROCEDURE dbo.PlotThread_Save
@PlotThreadID int = NULL,
@PlotLineID int,
@ThreadTitle nvarchar(200),
@ThreadTypeID int,
@ThreadStatusID int,
@Importance int,
@Summary nvarchar(max) = NULL,
@IntroducedSceneID int = NULL,
@PlannedResolutionSceneID int = NULL,
@ActualResolutionSceneID int = NULL
AS
BEGIN
SET NOCOUNT ON;
IF @PlotThreadID IS NULL OR @PlotThreadID = 0
BEGIN
INSERT dbo.PlotThreads (PlotLineID, ThreadTitle, ThreadTypeID, ThreadStatusID, Importance, Summary, IntroducedSceneID, PlannedResolutionSceneID, ActualResolutionSceneID)
VALUES (@PlotLineID, @ThreadTitle, @ThreadTypeID, @ThreadStatusID, @Importance, @Summary, @IntroducedSceneID, @PlannedResolutionSceneID, @ActualResolutionSceneID);
SELECT CAST(SCOPE_IDENTITY() AS int);
RETURN;
END;
UPDATE dbo.PlotThreads
SET PlotLineID = @PlotLineID,
ThreadTitle = @ThreadTitle,
ThreadTypeID = @ThreadTypeID,
ThreadStatusID = @ThreadStatusID,
Importance = @Importance,
Summary = @Summary,
IntroducedSceneID = @IntroducedSceneID,
PlannedResolutionSceneID = @PlannedResolutionSceneID,
ActualResolutionSceneID = @ActualResolutionSceneID,
UpdatedDate = SYSUTCDATETIME()
WHERE PlotThreadID = @PlotThreadID;
SELECT @PlotThreadID;
END;
GO
CREATE OR ALTER PROCEDURE dbo.PlotThread_Archive
@PlotThreadID int
AS
BEGIN
SET NOCOUNT ON;
UPDATE dbo.PlotThreads SET IsArchived = 1, UpdatedDate = SYSUTCDATETIME() WHERE PlotThreadID = @PlotThreadID;
END;
GO
CREATE OR ALTER PROCEDURE dbo.ThreadEvent_ListByScene
@SceneID int
AS
BEGIN
SET NOCOUNT ON;
SELECT te.ThreadEventID, te.PlotThreadID, pt.ThreadTitle, pt.PlotLineID, pl.PlotLineName, pl.Colour,
te.SceneID, te.EventTypeID, tet.TypeName AS EventTypeName, tet.MarkerText, te.EventTitle,
te.EventDescription, te.CreatedDate, te.UpdatedDate
FROM dbo.ThreadEvents te
INNER JOIN dbo.PlotThreads pt ON pt.PlotThreadID = te.PlotThreadID
INNER JOIN dbo.PlotLines pl ON pl.PlotLineID = pt.PlotLineID
INNER JOIN dbo.ThreadEventTypes tet ON tet.ThreadEventTypeID = te.EventTypeID
WHERE te.SceneID = @SceneID AND pt.IsArchived = 0 AND pl.IsArchived = 0
ORDER BY pl.SortOrder, pt.Importance DESC, te.CreatedDate;
END;
GO
CREATE OR ALTER PROCEDURE dbo.ThreadEvent_ListByPlotThread
@PlotThreadID int
AS
BEGIN
SET NOCOUNT ON;
SELECT te.ThreadEventID, te.PlotThreadID, pt.ThreadTitle, pt.PlotLineID, pl.PlotLineName, pl.Colour,
te.SceneID, te.EventTypeID, tet.TypeName AS EventTypeName, tet.MarkerText, te.EventTitle,
te.EventDescription, te.CreatedDate, te.UpdatedDate
FROM dbo.ThreadEvents te
INNER JOIN dbo.PlotThreads pt ON pt.PlotThreadID = te.PlotThreadID
INNER JOIN dbo.PlotLines pl ON pl.PlotLineID = pt.PlotLineID
INNER JOIN dbo.ThreadEventTypes tet ON tet.ThreadEventTypeID = te.EventTypeID
WHERE te.PlotThreadID = @PlotThreadID
ORDER BY te.CreatedDate;
END;
GO
CREATE OR ALTER PROCEDURE dbo.ThreadEvent_Save
@ThreadEventID int = NULL,
@PlotThreadID int,
@SceneID int,
@EventTypeID int,
@EventTitle nvarchar(200),
@EventDescription nvarchar(max) = NULL
AS
BEGIN
SET NOCOUNT ON;
IF @ThreadEventID IS NULL OR @ThreadEventID = 0
BEGIN
INSERT dbo.ThreadEvents (PlotThreadID, SceneID, EventTypeID, EventTitle, EventDescription)
VALUES (@PlotThreadID, @SceneID, @EventTypeID, @EventTitle, @EventDescription);
SET @ThreadEventID = CAST(SCOPE_IDENTITY() AS int);
END
ELSE
BEGIN
UPDATE dbo.ThreadEvents
SET PlotThreadID = @PlotThreadID,
SceneID = @SceneID,
EventTypeID = @EventTypeID,
EventTitle = @EventTitle,
EventDescription = @EventDescription,
UpdatedDate = SYSUTCDATETIME()
WHERE ThreadEventID = @ThreadEventID;
END;
DECLARE @EventTypeName nvarchar(100);
SELECT @EventTypeName = TypeName FROM dbo.ThreadEventTypes WHERE ThreadEventTypeID = @EventTypeID;
IF @EventTypeName = N'Introduced'
BEGIN
UPDATE pt
SET IntroducedSceneID = COALESCE(IntroducedSceneID, @SceneID),
ThreadStatusID = CASE WHEN ts.StatusName = N'Planned' THEN introduced.ThreadStatusID ELSE pt.ThreadStatusID END,
UpdatedDate = SYSUTCDATETIME()
FROM dbo.PlotThreads pt
INNER JOIN dbo.ThreadStatuses ts ON ts.ThreadStatusID = pt.ThreadStatusID
CROSS APPLY (SELECT ThreadStatusID FROM dbo.ThreadStatuses WHERE StatusName = N'Introduced') introduced
WHERE pt.PlotThreadID = @PlotThreadID;
END;
IF @EventTypeName IN (N'Resolved', N'Payoff')
BEGIN
UPDATE pt
SET ActualResolutionSceneID = COALESCE(ActualResolutionSceneID, @SceneID),
ThreadStatusID = CASE WHEN @EventTypeName = N'Resolved' THEN resolved.ThreadStatusID ELSE pt.ThreadStatusID END,
UpdatedDate = SYSUTCDATETIME()
FROM dbo.PlotThreads pt
CROSS APPLY (SELECT ThreadStatusID FROM dbo.ThreadStatuses WHERE StatusName = N'Resolved') resolved
WHERE pt.PlotThreadID = @PlotThreadID;
END;
SELECT @ThreadEventID;
END;
GO
CREATE OR ALTER PROCEDURE dbo.ThreadEvent_Delete
@ThreadEventID int
AS
BEGIN
SET NOCOUNT ON;
DELETE FROM dbo.ThreadEvents WHERE ThreadEventID = @ThreadEventID;
END;
GO
CREATE OR ALTER PROCEDURE dbo.SceneOption_ListByProject
@ProjectID int
AS
BEGIN
SET NOCOUNT ON;
SELECT s.SceneID, b.BookTitle, c.ChapterNumber, c.ChapterTitle, s.SceneNumber, s.SceneTitle
FROM dbo.Scenes s
INNER JOIN dbo.Chapters c ON c.ChapterID = s.ChapterID
INNER JOIN dbo.Books b ON b.BookID = c.BookID
WHERE b.ProjectID = @ProjectID AND b.IsArchived = 0 AND c.IsArchived = 0 AND s.IsArchived = 0
ORDER BY b.SortOrder, c.SortOrder, s.SortOrder;
END;
GO
CREATE OR ALTER PROCEDURE dbo.Timeline_GetByProject
@ProjectID int,
@BookID int = NULL
AS
BEGIN
SET NOCOUNT ON;
SELECT ProjectID, ProjectName, Description, CreatedDate, UpdatedDate, IsArchived
FROM dbo.Projects
WHERE ProjectID = @ProjectID AND IsArchived = 0;
SELECT BookID, ProjectID, BookTitle, BookNumber, Description, SortOrder, CreatedDate, UpdatedDate, IsArchived
FROM dbo.Books
WHERE ProjectID = @ProjectID AND IsArchived = 0 AND (@BookID IS NULL OR BookID = @BookID)
ORDER BY SortOrder, BookNumber;
SELECT c.ChapterID, c.BookID, c.ChapterNumber, c.ChapterTitle, c.POVCharacterID, c.Summary,
c.SortOrder, c.RevisionStatusID, rs.StatusName AS RevisionStatusName,
c.CreatedDate, c.UpdatedDate, c.IsArchived
FROM dbo.Chapters c
INNER JOIN dbo.Books b ON b.BookID = c.BookID
INNER JOIN dbo.RevisionStatuses rs ON rs.RevisionStatusID = c.RevisionStatusID
WHERE b.ProjectID = @ProjectID AND b.IsArchived = 0 AND c.IsArchived = 0 AND (@BookID IS NULL OR b.BookID = @BookID)
ORDER BY b.SortOrder, c.SortOrder, c.ChapterNumber;
SELECT s.SceneID, s.ChapterID, s.SceneNumber, s.SceneTitle, s.Summary, s.POVCharacterID,
s.PrimaryLocationID, s.SortOrder, s.TimeModeID, tm.TimeModeName, s.StartDateTime,
s.EndDateTime, s.DurationAmount, s.DurationUnitID, du.DurationUnitName, s.RelativeTimeText,
s.TimeConfidenceID, tc.TimeConfidenceName, s.ScenePurposeNotes, s.SceneOutcomeNotes,
s.RevisionStatusID, rs.StatusName AS RevisionStatusName, s.CreatedDate, s.UpdatedDate, s.IsArchived
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.TimeModes tm ON tm.TimeModeID = s.TimeModeID
LEFT JOIN dbo.DurationUnits du ON du.DurationUnitID = s.DurationUnitID
INNER JOIN dbo.TimeConfidences tc ON tc.TimeConfidenceID = s.TimeConfidenceID
INNER JOIN dbo.RevisionStatuses rs ON rs.RevisionStatusID = s.RevisionStatusID
WHERE b.ProjectID = @ProjectID AND b.IsArchived = 0 AND c.IsArchived = 0 AND s.IsArchived = 0 AND (@BookID IS NULL OR b.BookID = @BookID)
ORDER BY b.SortOrder, c.SortOrder, s.SortOrder, s.SceneNumber;
SELECT sp.SceneID, spt.ScenePurposeTypeID, spt.PurposeName, spt.SortOrder
FROM dbo.ScenePurposes sp
INNER JOIN dbo.ScenePurposeTypes spt ON spt.ScenePurposeTypeID = sp.ScenePurposeTypeID
INNER JOIN dbo.Scenes s ON s.SceneID = sp.SceneID
INNER JOIN dbo.Chapters c ON c.ChapterID = s.ChapterID
INNER JOIN dbo.Books b ON b.BookID = c.BookID
WHERE b.ProjectID = @ProjectID AND b.IsArchived = 0 AND c.IsArchived = 0 AND s.IsArchived = 0 AND (@BookID IS NULL OR b.BookID = @BookID)
ORDER BY sp.SceneID, spt.SortOrder;
SELECT MetricTypeID, ProjectID, MetricName, Description, MinValue, MaxValue, DefaultValue, SortOrder, IsActive
FROM dbo.SceneMetricTypes
WHERE IsActive = 1 AND (ProjectID IS NULL OR ProjectID = @ProjectID)
ORDER BY SortOrder, MetricName;
SELECT s.SceneID, mt.MetricTypeID, mt.MetricName, COALESCE(smv.Value, mt.DefaultValue) AS Value, smv.Notes
FROM dbo.Scenes s
INNER JOIN dbo.Chapters c ON c.ChapterID = s.ChapterID
INNER JOIN dbo.Books b ON b.BookID = c.BookID
CROSS JOIN dbo.SceneMetricTypes mt
LEFT JOIN dbo.SceneMetricValues smv ON smv.SceneID = s.SceneID AND smv.MetricTypeID = mt.MetricTypeID
WHERE b.ProjectID = @ProjectID
AND b.IsArchived = 0
AND c.IsArchived = 0
AND s.IsArchived = 0
AND mt.IsActive = 1
AND (mt.ProjectID IS NULL OR mt.ProjectID = @ProjectID)
AND (@BookID IS NULL OR b.BookID = @BookID)
ORDER BY b.SortOrder, c.SortOrder, s.SortOrder, mt.SortOrder;
SELECT pl.PlotLineID, pl.ProjectID, pl.BookID, b.BookTitle, pl.PlotLineName, pl.PlotLineTypeID,
plt.TypeName AS PlotLineTypeName, pl.Description, pl.ParentPlotLineID, parent.PlotLineName AS ParentPlotLineName,
pl.SortOrder, pl.Colour, pl.IsMainPlot, pl.IsVisibleOnTimeline, pl.CreatedDate, pl.UpdatedDate, pl.IsArchived
FROM dbo.PlotLines pl
INNER JOIN dbo.PlotLineTypes plt ON plt.PlotLineTypeID = pl.PlotLineTypeID
LEFT JOIN dbo.Books b ON b.BookID = pl.BookID
LEFT JOIN dbo.PlotLines parent ON parent.PlotLineID = pl.ParentPlotLineID
WHERE pl.ProjectID = @ProjectID
AND pl.IsArchived = 0
AND pl.IsVisibleOnTimeline = 1
AND (@BookID IS NULL OR pl.BookID IS NULL OR pl.BookID = @BookID)
ORDER BY pl.SortOrder, pl.PlotLineName;
SELECT te.ThreadEventID, te.PlotThreadID, pt.ThreadTitle, pt.PlotLineID, pl.PlotLineName, pl.Colour,
te.SceneID, te.EventTypeID, tet.TypeName AS EventTypeName, tet.MarkerText, te.EventTitle,
te.EventDescription, te.CreatedDate, te.UpdatedDate
FROM dbo.ThreadEvents te
INNER JOIN dbo.PlotThreads pt ON pt.PlotThreadID = te.PlotThreadID
INNER JOIN dbo.PlotLines pl ON pl.PlotLineID = pt.PlotLineID
INNER JOIN dbo.ThreadEventTypes tet ON tet.ThreadEventTypeID = te.EventTypeID
INNER JOIN dbo.Scenes s ON s.SceneID = te.SceneID
INNER JOIN dbo.Chapters c ON c.ChapterID = s.ChapterID
INNER JOIN dbo.Books b ON b.BookID = c.BookID
WHERE b.ProjectID = @ProjectID
AND b.IsArchived = 0
AND c.IsArchived = 0
AND s.IsArchived = 0
AND pl.IsArchived = 0
AND pl.IsVisibleOnTimeline = 1
AND pt.IsArchived = 0
AND (@BookID IS NULL OR b.BookID = @BookID)
ORDER BY pl.SortOrder, b.SortOrder, c.SortOrder, s.SortOrder, te.CreatedDate;
END;
GO

View File

@ -0,0 +1,709 @@
SET ANSI_NULLS ON;
GO
SET QUOTED_IDENTIFIER ON;
GO
IF OBJECT_ID(N'dbo.AssetKinds', N'U') IS NULL
BEGIN
CREATE TABLE dbo.AssetKinds
(
AssetKindID int IDENTITY(1,1) NOT NULL CONSTRAINT PK_AssetKinds PRIMARY KEY,
KindName nvarchar(100) NOT NULL,
SortOrder int NOT NULL,
IsActive bit NOT NULL CONSTRAINT DF_AssetKinds_IsActive DEFAULT 1,
CONSTRAINT UQ_AssetKinds_KindName UNIQUE (KindName)
);
END;
GO
IF OBJECT_ID(N'dbo.AssetStates', N'U') IS NULL
BEGIN
CREATE TABLE dbo.AssetStates
(
AssetStateID int IDENTITY(1,1) NOT NULL CONSTRAINT PK_AssetStates PRIMARY KEY,
ProjectID int NULL,
AssetKindID int NULL,
StateName nvarchar(100) NOT NULL,
SortOrder int NOT NULL,
Description nvarchar(max) NULL,
IsResolvedState bit NOT NULL CONSTRAINT DF_AssetStates_IsResolvedState DEFAULT 0,
IsActive bit NOT NULL CONSTRAINT DF_AssetStates_IsActive DEFAULT 1,
CONSTRAINT FK_AssetStates_Projects FOREIGN KEY (ProjectID) REFERENCES dbo.Projects(ProjectID),
CONSTRAINT FK_AssetStates_AssetKinds FOREIGN KEY (AssetKindID) REFERENCES dbo.AssetKinds(AssetKindID)
);
END;
GO
IF OBJECT_ID(N'dbo.StoryAssets', N'U') IS NULL
BEGIN
CREATE TABLE dbo.StoryAssets
(
StoryAssetID int IDENTITY(1,1) NOT NULL CONSTRAINT PK_StoryAssets PRIMARY KEY,
ProjectID int NOT NULL,
AssetName nvarchar(200) NOT NULL,
AssetKindID int NOT NULL,
Description nvarchar(max) NULL,
Importance int NOT NULL CONSTRAINT DF_StoryAssets_Importance DEFAULT 5,
CurrentStateID int NULL,
CurrentLocationID int NULL,
IsResolved bit NOT NULL CONSTRAINT DF_StoryAssets_IsResolved DEFAULT 0,
CreatedDate datetime2 NOT NULL CONSTRAINT DF_StoryAssets_CreatedDate DEFAULT SYSUTCDATETIME(),
UpdatedDate datetime2 NOT NULL CONSTRAINT DF_StoryAssets_UpdatedDate DEFAULT SYSUTCDATETIME(),
IsArchived bit NOT NULL CONSTRAINT DF_StoryAssets_IsArchived DEFAULT 0,
CONSTRAINT FK_StoryAssets_Projects FOREIGN KEY (ProjectID) REFERENCES dbo.Projects(ProjectID),
CONSTRAINT FK_StoryAssets_AssetKinds FOREIGN KEY (AssetKindID) REFERENCES dbo.AssetKinds(AssetKindID),
CONSTRAINT FK_StoryAssets_CurrentState FOREIGN KEY (CurrentStateID) REFERENCES dbo.AssetStates(AssetStateID),
CONSTRAINT CK_StoryAssets_Importance CHECK (Importance BETWEEN 1 AND 10)
);
END;
GO
IF OBJECT_ID(N'dbo.AssetEventTypes', N'U') IS NULL
BEGIN
CREATE TABLE dbo.AssetEventTypes
(
AssetEventTypeID int IDENTITY(1,1) NOT NULL CONSTRAINT PK_AssetEventTypes PRIMARY KEY,
TypeName nvarchar(100) NOT NULL,
MarkerText nvarchar(10) NOT NULL,
SortOrder int NOT NULL,
IsActive bit NOT NULL CONSTRAINT DF_AssetEventTypes_IsActive DEFAULT 1,
CONSTRAINT UQ_AssetEventTypes_TypeName UNIQUE (TypeName)
);
END;
GO
IF OBJECT_ID(N'dbo.AssetEvents', N'U') IS NULL
BEGIN
CREATE TABLE dbo.AssetEvents
(
AssetEventID int IDENTITY(1,1) NOT NULL CONSTRAINT PK_AssetEvents PRIMARY KEY,
StoryAssetID int NOT NULL,
SceneID int NOT NULL,
AssetEventTypeID int NOT NULL,
FromStateID int NULL,
ToStateID int NULL,
EventTitle nvarchar(200) NOT NULL,
EventDescription nvarchar(max) NULL,
CreatedDate datetime2 NOT NULL CONSTRAINT DF_AssetEvents_CreatedDate DEFAULT SYSUTCDATETIME(),
UpdatedDate datetime2 NOT NULL CONSTRAINT DF_AssetEvents_UpdatedDate DEFAULT SYSUTCDATETIME(),
CONSTRAINT FK_AssetEvents_StoryAssets FOREIGN KEY (StoryAssetID) REFERENCES dbo.StoryAssets(StoryAssetID),
CONSTRAINT FK_AssetEvents_Scenes FOREIGN KEY (SceneID) REFERENCES dbo.Scenes(SceneID),
CONSTRAINT FK_AssetEvents_AssetEventTypes FOREIGN KEY (AssetEventTypeID) REFERENCES dbo.AssetEventTypes(AssetEventTypeID),
CONSTRAINT FK_AssetEvents_FromState FOREIGN KEY (FromStateID) REFERENCES dbo.AssetStates(AssetStateID),
CONSTRAINT FK_AssetEvents_ToState FOREIGN KEY (ToStateID) REFERENCES dbo.AssetStates(AssetStateID)
);
END;
GO
IF OBJECT_ID(N'dbo.AssetDependencyTypes', N'U') IS NULL
BEGIN
CREATE TABLE dbo.AssetDependencyTypes
(
AssetDependencyTypeID int IDENTITY(1,1) NOT NULL CONSTRAINT PK_AssetDependencyTypes PRIMARY KEY,
TypeName nvarchar(100) NOT NULL,
SortOrder int NOT NULL,
IsValidatingDependency bit NOT NULL CONSTRAINT DF_AssetDependencyTypes_IsValidatingDependency DEFAULT 0,
IsActive bit NOT NULL CONSTRAINT DF_AssetDependencyTypes_IsActive DEFAULT 1,
CONSTRAINT UQ_AssetDependencyTypes_TypeName UNIQUE (TypeName)
);
END;
GO
IF OBJECT_ID(N'dbo.AssetDependencies', N'U') IS NULL
BEGIN
CREATE TABLE dbo.AssetDependencies
(
AssetDependencyID int IDENTITY(1,1) NOT NULL CONSTRAINT PK_AssetDependencies PRIMARY KEY,
SourceAssetID int NOT NULL,
TargetAssetID int NOT NULL,
AssetDependencyTypeID int NOT NULL,
SourceRequiredStateID int NULL,
TargetBlockedStateID int NULL,
Description nvarchar(max) NULL,
CreatedDate datetime2 NOT NULL CONSTRAINT DF_AssetDependencies_CreatedDate DEFAULT SYSUTCDATETIME(),
UpdatedDate datetime2 NOT NULL CONSTRAINT DF_AssetDependencies_UpdatedDate DEFAULT SYSUTCDATETIME(),
IsArchived bit NOT NULL CONSTRAINT DF_AssetDependencies_IsArchived DEFAULT 0,
CONSTRAINT FK_AssetDependencies_SourceAsset FOREIGN KEY (SourceAssetID) REFERENCES dbo.StoryAssets(StoryAssetID),
CONSTRAINT FK_AssetDependencies_TargetAsset FOREIGN KEY (TargetAssetID) REFERENCES dbo.StoryAssets(StoryAssetID),
CONSTRAINT FK_AssetDependencies_Type FOREIGN KEY (AssetDependencyTypeID) REFERENCES dbo.AssetDependencyTypes(AssetDependencyTypeID),
CONSTRAINT FK_AssetDependencies_SourceState FOREIGN KEY (SourceRequiredStateID) REFERENCES dbo.AssetStates(AssetStateID),
CONSTRAINT FK_AssetDependencies_TargetState FOREIGN KEY (TargetBlockedStateID) REFERENCES dbo.AssetStates(AssetStateID)
);
END;
GO
IF OBJECT_ID(N'dbo.AssetCustodyEventTypes', N'U') IS NULL
BEGIN
CREATE TABLE dbo.AssetCustodyEventTypes
(
AssetCustodyEventTypeID int IDENTITY(1,1) NOT NULL CONSTRAINT PK_AssetCustodyEventTypes PRIMARY KEY,
TypeName nvarchar(100) NOT NULL,
SortOrder int NOT NULL,
IsActive bit NOT NULL CONSTRAINT DF_AssetCustodyEventTypes_IsActive DEFAULT 1,
CONSTRAINT UQ_AssetCustodyEventTypes_TypeName UNIQUE (TypeName)
);
END;
GO
IF OBJECT_ID(N'dbo.CustodyRoles', N'U') IS NULL
BEGIN
CREATE TABLE dbo.CustodyRoles
(
CustodyRoleID int IDENTITY(1,1) NOT NULL CONSTRAINT PK_CustodyRoles PRIMARY KEY,
RoleName nvarchar(100) NOT NULL,
SortOrder int NOT NULL,
IsActive bit NOT NULL CONSTRAINT DF_CustodyRoles_IsActive DEFAULT 1,
CONSTRAINT UQ_CustodyRoles_RoleName UNIQUE (RoleName)
);
END;
GO
IF OBJECT_ID(N'dbo.AssetCustodyEvents', N'U') IS NULL
BEGIN
CREATE TABLE dbo.AssetCustodyEvents
(
AssetCustodyEventID int IDENTITY(1,1) NOT NULL CONSTRAINT PK_AssetCustodyEvents PRIMARY KEY,
StoryAssetID int NOT NULL,
SceneID int NOT NULL,
AssetCustodyEventTypeID int NOT NULL,
Description nvarchar(max) NULL,
CreatedDate datetime2 NOT NULL CONSTRAINT DF_AssetCustodyEvents_CreatedDate DEFAULT SYSUTCDATETIME(),
UpdatedDate datetime2 NOT NULL CONSTRAINT DF_AssetCustodyEvents_UpdatedDate DEFAULT SYSUTCDATETIME(),
CONSTRAINT FK_AssetCustodyEvents_StoryAssets FOREIGN KEY (StoryAssetID) REFERENCES dbo.StoryAssets(StoryAssetID),
CONSTRAINT FK_AssetCustodyEvents_Scenes FOREIGN KEY (SceneID) REFERENCES dbo.Scenes(SceneID),
CONSTRAINT FK_AssetCustodyEvents_Type FOREIGN KEY (AssetCustodyEventTypeID) REFERENCES dbo.AssetCustodyEventTypes(AssetCustodyEventTypeID)
);
END;
GO
IF OBJECT_ID(N'dbo.AssetCustodyEventCharacters', N'U') IS NULL
BEGIN
CREATE TABLE dbo.AssetCustodyEventCharacters
(
AssetCustodyEventCharacterID int IDENTITY(1,1) NOT NULL CONSTRAINT PK_AssetCustodyEventCharacters PRIMARY KEY,
AssetCustodyEventID int NOT NULL,
CharacterID int NULL,
CharacterNameText nvarchar(200) NULL,
CustodyRoleID int NOT NULL,
CONSTRAINT FK_AssetCustodyEventCharacters_Events FOREIGN KEY (AssetCustodyEventID) REFERENCES dbo.AssetCustodyEvents(AssetCustodyEventID) ON DELETE CASCADE,
CONSTRAINT FK_AssetCustodyEventCharacters_Roles FOREIGN KEY (CustodyRoleID) REFERENCES dbo.CustodyRoles(CustodyRoleID)
);
END;
GO
IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = N'IX_StoryAssets_ProjectID' AND object_id = OBJECT_ID(N'dbo.StoryAssets'))
CREATE INDEX IX_StoryAssets_ProjectID ON dbo.StoryAssets(ProjectID, IsArchived, Importance);
GO
IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = N'IX_AssetEvents_SceneID' AND object_id = OBJECT_ID(N'dbo.AssetEvents'))
CREATE INDEX IX_AssetEvents_SceneID ON dbo.AssetEvents(SceneID, StoryAssetID, AssetEventTypeID);
GO
IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = N'IX_AssetDependencies_TargetAssetID' AND object_id = OBJECT_ID(N'dbo.AssetDependencies'))
CREATE INDEX IX_AssetDependencies_TargetAssetID ON dbo.AssetDependencies(TargetAssetID, IsArchived, AssetDependencyTypeID);
GO
IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = N'IX_AssetCustodyEvents_SceneID' AND object_id = OBJECT_ID(N'dbo.AssetCustodyEvents'))
CREATE INDEX IX_AssetCustodyEvents_SceneID ON dbo.AssetCustodyEvents(SceneID, StoryAssetID);
GO
MERGE dbo.AssetKinds AS target
USING (VALUES
(N'Physical Object', 10), (N'Clothing', 20), (N'Document', 30), (N'Evidence', 40),
(N'Clue', 50), (N'Secret', 60), (N'Fact', 70), (N'Emotional Fact', 80),
(N'Relationship Truth', 90), (N'Plan', 100), (N'Threat', 110), (N'Promise', 120),
(N'Location Item', 130), (N'Vehicle', 140), (N'Money Trail', 150), (N'Other', 160)
) AS source (KindName, SortOrder)
ON target.KindName = source.KindName
WHEN NOT MATCHED THEN INSERT (KindName, SortOrder) VALUES (source.KindName, source.SortOrder);
GO
MERGE dbo.AssetStates AS target
USING (VALUES
(N'Unknown', 10, 0), (N'Mentioned', 20, 0), (N'Known To Exist', 30, 0), (N'Found', 40, 0),
(N'Possessed', 50, 0), (N'Investigated', 60, 0), (N'Identified', 70, 0), (N'Used', 80, 0),
(N'Lost', 90, 0), (N'Hidden', 100, 0), (N'Revealed', 110, 0), (N'Opened', 120, 0),
(N'Destroyed', 130, 1), (N'Resolved', 140, 1)
) AS source (StateName, SortOrder, IsResolvedState)
ON target.ProjectID IS NULL AND target.AssetKindID IS NULL AND target.StateName = source.StateName
WHEN NOT MATCHED THEN INSERT (StateName, SortOrder, IsResolvedState) VALUES (source.StateName, source.SortOrder, source.IsResolvedState);
GO
MERGE dbo.AssetEventTypes AS target
USING (VALUES
(N'Mentioned', N'M', 10), (N'First Mentioned', N'I', 20), (N'Found', N'F', 30),
(N'Lost', N'L', 40), (N'Given', N'G', 50), (N'Taken', N'T', 60), (N'Stolen', N'S', 70),
(N'Hidden', N'H', 80), (N'Moved', N'Mv', 90), (N'Used', N'U', 100), (N'Opened', N'O', 110),
(N'Destroyed', N'X', 120), (N'Revealed', N'R', 130), (N'Investigated', N'?', 140),
(N'State Changed', N'C', 150), (N'Custody Changed', N'Cc', 160), (N'Location Changed', N'Loc', 170),
(N'Note', N'N', 180)
) AS source (TypeName, MarkerText, SortOrder)
ON target.TypeName = source.TypeName
WHEN NOT MATCHED THEN INSERT (TypeName, MarkerText, SortOrder) VALUES (source.TypeName, source.MarkerText, source.SortOrder);
GO
MERGE dbo.AssetDependencyTypes AS target
USING (VALUES
(N'Requires', 10, 1), (N'Unlocks', 20, 1), (N'Reveals', 30, 0), (N'Leads To', 40, 0),
(N'Contains', 50, 0), (N'Contradicts', 60, 0), (N'Supports', 70, 0), (N'Related To', 80, 0)
) AS source (TypeName, SortOrder, IsValidatingDependency)
ON target.TypeName = source.TypeName
WHEN NOT MATCHED THEN INSERT (TypeName, SortOrder, IsValidatingDependency) VALUES (source.TypeName, source.SortOrder, source.IsValidatingDependency);
GO
MERGE dbo.AssetCustodyEventTypes AS target
USING (VALUES
(N'Has', 10), (N'Shared Custody', 20), (N'Given To', 30), (N'Taken From', 40),
(N'Stolen By', 50), (N'Hidden By', 60), (N'Found By', 70), (N'Used By', 80),
(N'Observed By', 90), (N'Left With', 100), (N'Lost By', 110), (N'Returned To', 120)
) AS source (TypeName, SortOrder)
ON target.TypeName = source.TypeName
WHEN NOT MATCHED THEN INSERT (TypeName, SortOrder) VALUES (source.TypeName, source.SortOrder);
GO
MERGE dbo.CustodyRoles AS target
USING (VALUES
(N'Has Custody', 10), (N'Shared Custody', 20), (N'Handles', 30), (N'Uses', 40),
(N'Observes', 50), (N'Transfers From', 60), (N'Transfers To', 70), (N'Steals From', 80),
(N'Hides', 90), (N'Finds', 100)
) AS source (RoleName, SortOrder)
ON target.RoleName = source.RoleName
WHEN NOT MATCHED THEN INSERT (RoleName, SortOrder) VALUES (source.RoleName, source.SortOrder);
GO
CREATE OR ALTER PROCEDURE dbo.AssetLookup_All
AS
BEGIN
SET NOCOUNT ON;
SELECT AssetKindID, KindName, SortOrder, IsActive FROM dbo.AssetKinds WHERE IsActive = 1 ORDER BY SortOrder;
SELECT AssetStateID, ProjectID, AssetKindID, StateName, SortOrder, Description, IsResolvedState, IsActive FROM dbo.AssetStates WHERE IsActive = 1 ORDER BY SortOrder;
SELECT AssetEventTypeID, TypeName, MarkerText, SortOrder, IsActive FROM dbo.AssetEventTypes WHERE IsActive = 1 ORDER BY SortOrder;
SELECT AssetDependencyTypeID, TypeName, SortOrder, IsValidatingDependency, IsActive FROM dbo.AssetDependencyTypes WHERE IsActive = 1 ORDER BY SortOrder;
SELECT AssetCustodyEventTypeID, TypeName, SortOrder, IsActive FROM dbo.AssetCustodyEventTypes WHERE IsActive = 1 ORDER BY SortOrder;
SELECT CustodyRoleID, RoleName, SortOrder, IsActive FROM dbo.CustodyRoles WHERE IsActive = 1 ORDER BY SortOrder;
END;
GO
CREATE OR ALTER PROCEDURE dbo.AssetKind_List AS BEGIN SET NOCOUNT ON; SELECT AssetKindID, KindName, SortOrder, IsActive FROM dbo.AssetKinds WHERE IsActive = 1 ORDER BY SortOrder; END;
GO
CREATE OR ALTER PROCEDURE dbo.AssetState_List AS BEGIN SET NOCOUNT ON; SELECT AssetStateID, ProjectID, AssetKindID, StateName, SortOrder, Description, IsResolvedState, IsActive FROM dbo.AssetStates WHERE IsActive = 1 ORDER BY SortOrder; END;
GO
CREATE OR ALTER PROCEDURE dbo.AssetEventType_List AS BEGIN SET NOCOUNT ON; SELECT AssetEventTypeID, TypeName, MarkerText, SortOrder, IsActive FROM dbo.AssetEventTypes WHERE IsActive = 1 ORDER BY SortOrder; END;
GO
CREATE OR ALTER PROCEDURE dbo.AssetDependencyType_List AS BEGIN SET NOCOUNT ON; SELECT AssetDependencyTypeID, TypeName, SortOrder, IsValidatingDependency, IsActive FROM dbo.AssetDependencyTypes WHERE IsActive = 1 ORDER BY SortOrder; END;
GO
CREATE OR ALTER PROCEDURE dbo.AssetCustodyEventType_List AS BEGIN SET NOCOUNT ON; SELECT AssetCustodyEventTypeID, TypeName, SortOrder, IsActive FROM dbo.AssetCustodyEventTypes WHERE IsActive = 1 ORDER BY SortOrder; END;
GO
CREATE OR ALTER PROCEDURE dbo.CustodyRole_List AS BEGIN SET NOCOUNT ON; SELECT CustodyRoleID, RoleName, SortOrder, IsActive FROM dbo.CustodyRoles WHERE IsActive = 1 ORDER BY SortOrder; END;
GO
CREATE OR ALTER PROCEDURE dbo.StoryAsset_ListByProject
@ProjectID int
AS
BEGIN
SET NOCOUNT ON;
SELECT sa.StoryAssetID, sa.ProjectID, sa.AssetName, sa.AssetKindID, ak.KindName, sa.Description, sa.Importance,
sa.CurrentStateID, ast.StateName AS CurrentStateName, sa.CurrentLocationID, sa.IsResolved,
sa.CreatedDate, sa.UpdatedDate, sa.IsArchived
FROM dbo.StoryAssets sa
INNER JOIN dbo.AssetKinds ak ON ak.AssetKindID = sa.AssetKindID
LEFT JOIN dbo.AssetStates ast ON ast.AssetStateID = sa.CurrentStateID
WHERE sa.ProjectID = @ProjectID AND sa.IsArchived = 0
ORDER BY sa.Importance DESC, sa.AssetName;
END;
GO
CREATE OR ALTER PROCEDURE dbo.StoryAsset_Get
@StoryAssetID int
AS
BEGIN
SET NOCOUNT ON;
SELECT sa.StoryAssetID, sa.ProjectID, sa.AssetName, sa.AssetKindID, ak.KindName, sa.Description, sa.Importance,
sa.CurrentStateID, ast.StateName AS CurrentStateName, sa.CurrentLocationID, sa.IsResolved,
sa.CreatedDate, sa.UpdatedDate, sa.IsArchived
FROM dbo.StoryAssets sa
INNER JOIN dbo.AssetKinds ak ON ak.AssetKindID = sa.AssetKindID
LEFT JOIN dbo.AssetStates ast ON ast.AssetStateID = sa.CurrentStateID
WHERE sa.StoryAssetID = @StoryAssetID AND sa.IsArchived = 0;
END;
GO
CREATE OR ALTER PROCEDURE dbo.StoryAsset_Save
@StoryAssetID int = NULL,
@ProjectID int,
@AssetName nvarchar(200),
@AssetKindID int,
@Description nvarchar(max) = NULL,
@Importance int,
@CurrentStateID int = NULL,
@IsResolved bit
AS
BEGIN
SET NOCOUNT ON;
IF @StoryAssetID IS NULL OR @StoryAssetID = 0
BEGIN
INSERT dbo.StoryAssets (ProjectID, AssetName, AssetKindID, Description, Importance, CurrentStateID, IsResolved)
VALUES (@ProjectID, @AssetName, @AssetKindID, @Description, @Importance, @CurrentStateID, @IsResolved);
SET @StoryAssetID = CAST(SCOPE_IDENTITY() AS int);
END
ELSE
BEGIN
UPDATE dbo.StoryAssets
SET AssetName = @AssetName,
AssetKindID = @AssetKindID,
Description = @Description,
Importance = @Importance,
CurrentStateID = @CurrentStateID,
IsResolved = @IsResolved,
UpdatedDate = SYSUTCDATETIME()
WHERE StoryAssetID = @StoryAssetID;
END
SELECT @StoryAssetID;
END;
GO
CREATE OR ALTER PROCEDURE dbo.StoryAsset_Archive
@StoryAssetID int
AS
BEGIN
SET NOCOUNT ON;
UPDATE dbo.StoryAssets SET IsArchived = 1, UpdatedDate = SYSUTCDATETIME() WHERE StoryAssetID = @StoryAssetID;
END;
GO
CREATE OR ALTER PROCEDURE dbo.AssetEvent_ListByScene
@SceneID int
AS
BEGIN
SET NOCOUNT ON;
SELECT ae.AssetEventID, ae.StoryAssetID, sa.ProjectID, sa.AssetName, sa.AssetKindID, ak.KindName,
ae.SceneID, ae.AssetEventTypeID, aet.TypeName AS AssetEventTypeName, aet.MarkerText,
ae.FromStateID, fs.StateName AS FromStateName, ae.ToStateID, ts.StateName AS ToStateName,
ae.EventTitle, ae.EventDescription, ae.CreatedDate, ae.UpdatedDate
FROM dbo.AssetEvents ae
INNER JOIN dbo.StoryAssets sa ON sa.StoryAssetID = ae.StoryAssetID
INNER JOIN dbo.AssetKinds ak ON ak.AssetKindID = sa.AssetKindID
INNER JOIN dbo.AssetEventTypes aet ON aet.AssetEventTypeID = ae.AssetEventTypeID
LEFT JOIN dbo.AssetStates fs ON fs.AssetStateID = ae.FromStateID
LEFT JOIN dbo.AssetStates ts ON ts.AssetStateID = ae.ToStateID
WHERE ae.SceneID = @SceneID
ORDER BY sa.AssetName, ae.AssetEventID;
END;
GO
CREATE OR ALTER PROCEDURE dbo.AssetEvent_ListByAsset
@StoryAssetID int
AS
BEGIN
SET NOCOUNT ON;
SELECT ae.AssetEventID, ae.StoryAssetID, sa.ProjectID, sa.AssetName, sa.AssetKindID, ak.KindName,
ae.SceneID, ae.AssetEventTypeID, aet.TypeName AS AssetEventTypeName, aet.MarkerText,
ae.FromStateID, fs.StateName AS FromStateName, ae.ToStateID, ts.StateName AS ToStateName,
ae.EventTitle, ae.EventDescription, ae.CreatedDate, ae.UpdatedDate,
b.BookTitle, c.ChapterNumber, s.SceneNumber, s.SceneTitle
FROM dbo.AssetEvents ae
INNER JOIN dbo.StoryAssets sa ON sa.StoryAssetID = ae.StoryAssetID
INNER JOIN dbo.AssetKinds ak ON ak.AssetKindID = sa.AssetKindID
INNER JOIN dbo.AssetEventTypes aet ON aet.AssetEventTypeID = ae.AssetEventTypeID
INNER JOIN dbo.Scenes s ON s.SceneID = ae.SceneID
INNER JOIN dbo.Chapters c ON c.ChapterID = s.ChapterID
INNER JOIN dbo.Books b ON b.BookID = c.BookID
LEFT JOIN dbo.AssetStates fs ON fs.AssetStateID = ae.FromStateID
LEFT JOIN dbo.AssetStates ts ON ts.AssetStateID = ae.ToStateID
WHERE ae.StoryAssetID = @StoryAssetID
ORDER BY b.SortOrder, c.SortOrder, s.SortOrder, ae.AssetEventID;
END;
GO
CREATE OR ALTER PROCEDURE dbo.AssetEvent_Save
@AssetEventID int = NULL,
@StoryAssetID int,
@SceneID int,
@AssetEventTypeID int,
@FromStateID int = NULL,
@ToStateID int = NULL,
@EventTitle nvarchar(200),
@EventDescription nvarchar(max) = NULL
AS
BEGIN
SET NOCOUNT ON;
IF @AssetEventID IS NULL OR @AssetEventID = 0
BEGIN
INSERT dbo.AssetEvents (StoryAssetID, SceneID, AssetEventTypeID, FromStateID, ToStateID, EventTitle, EventDescription)
VALUES (@StoryAssetID, @SceneID, @AssetEventTypeID, @FromStateID, @ToStateID, @EventTitle, @EventDescription);
SET @AssetEventID = CAST(SCOPE_IDENTITY() AS int);
END
ELSE
BEGIN
UPDATE dbo.AssetEvents
SET StoryAssetID = @StoryAssetID,
SceneID = @SceneID,
AssetEventTypeID = @AssetEventTypeID,
FromStateID = @FromStateID,
ToStateID = @ToStateID,
EventTitle = @EventTitle,
EventDescription = @EventDescription,
UpdatedDate = SYSUTCDATETIME()
WHERE AssetEventID = @AssetEventID;
END
IF @ToStateID IS NOT NULL
BEGIN
UPDATE dbo.StoryAssets
SET CurrentStateID = @ToStateID,
IsResolved = CASE WHEN EXISTS (SELECT 1 FROM dbo.AssetStates WHERE AssetStateID = @ToStateID AND IsResolvedState = 1) THEN 1 ELSE IsResolved END,
UpdatedDate = SYSUTCDATETIME()
WHERE StoryAssetID = @StoryAssetID;
END
SELECT @AssetEventID;
END;
GO
CREATE OR ALTER PROCEDURE dbo.AssetEvent_Delete
@AssetEventID int
AS
BEGIN
SET NOCOUNT ON;
DELETE FROM dbo.AssetEvents WHERE AssetEventID = @AssetEventID;
END;
GO
CREATE OR ALTER PROCEDURE dbo.AssetDependency_ListByAsset
@StoryAssetID int
AS
BEGIN
SET NOCOUNT ON;
SELECT ad.AssetDependencyID, ad.SourceAssetID, source.AssetName AS SourceAssetName,
ad.TargetAssetID, target.AssetName AS TargetAssetName,
ad.AssetDependencyTypeID, adt.TypeName AS DependencyTypeName, adt.IsValidatingDependency,
ad.SourceRequiredStateID, srs.StateName AS SourceRequiredStateName,
ad.TargetBlockedStateID, tbs.StateName AS TargetBlockedStateName,
ad.Description, ad.CreatedDate, ad.UpdatedDate, ad.IsArchived
FROM dbo.AssetDependencies ad
INNER JOIN dbo.StoryAssets source ON source.StoryAssetID = ad.SourceAssetID
INNER JOIN dbo.StoryAssets target ON target.StoryAssetID = ad.TargetAssetID
INNER JOIN dbo.AssetDependencyTypes adt ON adt.AssetDependencyTypeID = ad.AssetDependencyTypeID
LEFT JOIN dbo.AssetStates srs ON srs.AssetStateID = ad.SourceRequiredStateID
LEFT JOIN dbo.AssetStates tbs ON tbs.AssetStateID = ad.TargetBlockedStateID
WHERE ad.IsArchived = 0 AND (@StoryAssetID IN (ad.SourceAssetID, ad.TargetAssetID))
ORDER BY ad.AssetDependencyID DESC;
END;
GO
CREATE OR ALTER PROCEDURE dbo.AssetDependency_Save
@AssetDependencyID int = NULL,
@SourceAssetID int,
@TargetAssetID int,
@AssetDependencyTypeID int,
@SourceRequiredStateID int = NULL,
@TargetBlockedStateID int = NULL,
@Description nvarchar(max) = NULL
AS
BEGIN
SET NOCOUNT ON;
IF @AssetDependencyID IS NULL OR @AssetDependencyID = 0
BEGIN
INSERT dbo.AssetDependencies (SourceAssetID, TargetAssetID, AssetDependencyTypeID, SourceRequiredStateID, TargetBlockedStateID, Description)
VALUES (@SourceAssetID, @TargetAssetID, @AssetDependencyTypeID, @SourceRequiredStateID, @TargetBlockedStateID, @Description);
SET @AssetDependencyID = CAST(SCOPE_IDENTITY() AS int);
END
ELSE
BEGIN
UPDATE dbo.AssetDependencies
SET SourceAssetID = @SourceAssetID,
TargetAssetID = @TargetAssetID,
AssetDependencyTypeID = @AssetDependencyTypeID,
SourceRequiredStateID = @SourceRequiredStateID,
TargetBlockedStateID = @TargetBlockedStateID,
Description = @Description,
UpdatedDate = SYSUTCDATETIME()
WHERE AssetDependencyID = @AssetDependencyID;
END
SELECT @AssetDependencyID;
END;
GO
CREATE OR ALTER PROCEDURE dbo.AssetDependency_Delete
@AssetDependencyID int
AS
BEGIN
SET NOCOUNT ON;
UPDATE dbo.AssetDependencies SET IsArchived = 1, UpdatedDate = SYSUTCDATETIME() WHERE AssetDependencyID = @AssetDependencyID;
END;
GO
CREATE OR ALTER PROCEDURE dbo.AssetCustodyEvent_ListByScene
@SceneID int
AS
BEGIN
SET NOCOUNT ON;
SELECT ace.AssetCustodyEventID, ace.StoryAssetID, sa.AssetName, ace.SceneID,
ace.AssetCustodyEventTypeID, acet.TypeName AS CustodyEventTypeName, ace.Description,
STRING_AGG(CONCAT(aec.CharacterNameText, N' (', cr.RoleName, N')'), N', ') AS CustodianSummary,
ace.CreatedDate, ace.UpdatedDate
FROM dbo.AssetCustodyEvents ace
INNER JOIN dbo.StoryAssets sa ON sa.StoryAssetID = ace.StoryAssetID
INNER JOIN dbo.AssetCustodyEventTypes acet ON acet.AssetCustodyEventTypeID = ace.AssetCustodyEventTypeID
LEFT JOIN dbo.AssetCustodyEventCharacters aec ON aec.AssetCustodyEventID = ace.AssetCustodyEventID
LEFT JOIN dbo.CustodyRoles cr ON cr.CustodyRoleID = aec.CustodyRoleID
WHERE ace.SceneID = @SceneID
GROUP BY ace.AssetCustodyEventID, ace.StoryAssetID, sa.AssetName, ace.SceneID, ace.AssetCustodyEventTypeID, acet.TypeName, ace.Description, ace.CreatedDate, ace.UpdatedDate
ORDER BY sa.AssetName, ace.AssetCustodyEventID;
END;
GO
CREATE OR ALTER PROCEDURE dbo.AssetCustodyEvent_ListByAsset
@StoryAssetID int
AS
BEGIN
SET NOCOUNT ON;
SELECT ace.AssetCustodyEventID, ace.StoryAssetID, sa.AssetName, ace.SceneID,
ace.AssetCustodyEventTypeID, acet.TypeName AS CustodyEventTypeName, ace.Description,
STRING_AGG(CONCAT(aec.CharacterNameText, N' (', cr.RoleName, N')'), N', ') AS CustodianSummary,
ace.CreatedDate, ace.UpdatedDate, b.BookTitle, c.ChapterNumber, s.SceneNumber, s.SceneTitle
FROM dbo.AssetCustodyEvents ace
INNER JOIN dbo.StoryAssets sa ON sa.StoryAssetID = ace.StoryAssetID
INNER JOIN dbo.AssetCustodyEventTypes acet ON acet.AssetCustodyEventTypeID = ace.AssetCustodyEventTypeID
INNER JOIN dbo.Scenes s ON s.SceneID = ace.SceneID
INNER JOIN dbo.Chapters c ON c.ChapterID = s.ChapterID
INNER JOIN dbo.Books b ON b.BookID = c.BookID
LEFT JOIN dbo.AssetCustodyEventCharacters aec ON aec.AssetCustodyEventID = ace.AssetCustodyEventID
LEFT JOIN dbo.CustodyRoles cr ON cr.CustodyRoleID = aec.CustodyRoleID
WHERE ace.StoryAssetID = @StoryAssetID
GROUP BY ace.AssetCustodyEventID, ace.StoryAssetID, sa.AssetName, ace.SceneID, ace.AssetCustodyEventTypeID, acet.TypeName, ace.Description, ace.CreatedDate, ace.UpdatedDate, b.BookTitle, c.ChapterNumber, s.SceneNumber, s.SceneTitle, b.SortOrder, c.SortOrder, s.SortOrder
ORDER BY b.SortOrder, c.SortOrder, s.SortOrder, ace.AssetCustodyEventID;
END;
GO
CREATE OR ALTER PROCEDURE dbo.AssetCustodyEvent_Save
@AssetCustodyEventID int = NULL,
@StoryAssetID int,
@SceneID int,
@AssetCustodyEventTypeID int,
@Description nvarchar(max) = NULL,
@CustodianNames nvarchar(max) = NULL,
@CustodyRoleID int
AS
BEGIN
SET NOCOUNT ON;
IF @AssetCustodyEventID IS NULL OR @AssetCustodyEventID = 0
BEGIN
INSERT dbo.AssetCustodyEvents (StoryAssetID, SceneID, AssetCustodyEventTypeID, Description)
VALUES (@StoryAssetID, @SceneID, @AssetCustodyEventTypeID, @Description);
SET @AssetCustodyEventID = CAST(SCOPE_IDENTITY() AS int);
END
ELSE
BEGIN
UPDATE dbo.AssetCustodyEvents
SET StoryAssetID = @StoryAssetID,
SceneID = @SceneID,
AssetCustodyEventTypeID = @AssetCustodyEventTypeID,
Description = @Description,
UpdatedDate = SYSUTCDATETIME()
WHERE AssetCustodyEventID = @AssetCustodyEventID;
END
DELETE FROM dbo.AssetCustodyEventCharacters WHERE AssetCustodyEventID = @AssetCustodyEventID;
INSERT dbo.AssetCustodyEventCharacters (AssetCustodyEventID, CharacterNameText, CustodyRoleID)
SELECT @AssetCustodyEventID, LTRIM(RTRIM(value)), @CustodyRoleID
FROM STRING_SPLIT(REPLACE(ISNULL(@CustodianNames, N''), CHAR(10), N','), N',')
WHERE LTRIM(RTRIM(value)) <> N'';
SELECT @AssetCustodyEventID;
END;
GO
CREATE OR ALTER PROCEDURE dbo.AssetCustodyEvent_Delete
@AssetCustodyEventID int
AS
BEGIN
SET NOCOUNT ON;
DELETE FROM dbo.AssetCustodyEvents WHERE AssetCustodyEventID = @AssetCustodyEventID;
END;
GO
CREATE OR ALTER PROCEDURE dbo.AssetTimeline_GetByProject
@ProjectID int,
@BookID int = NULL
AS
BEGIN
SET NOCOUNT ON;
SELECT sa.StoryAssetID, sa.ProjectID, sa.AssetName, sa.AssetKindID, ak.KindName, sa.Description, sa.Importance,
sa.CurrentStateID, ast.StateName AS CurrentStateName, sa.CurrentLocationID, sa.IsResolved,
sa.CreatedDate, sa.UpdatedDate, sa.IsArchived
FROM dbo.StoryAssets sa
INNER JOIN dbo.AssetKinds ak ON ak.AssetKindID = sa.AssetKindID
LEFT JOIN dbo.AssetStates ast ON ast.AssetStateID = sa.CurrentStateID
WHERE sa.ProjectID = @ProjectID AND sa.IsArchived = 0 AND (sa.Importance >= 5 OR sa.IsResolved = 0)
ORDER BY sa.Importance DESC, sa.AssetName;
SELECT ae.AssetEventID, ae.StoryAssetID, sa.ProjectID, sa.AssetName, sa.AssetKindID, ak.KindName,
ae.SceneID, ae.AssetEventTypeID, aet.TypeName AS AssetEventTypeName, aet.MarkerText,
ae.FromStateID, fs.StateName AS FromStateName, ae.ToStateID, ts.StateName AS ToStateName,
ae.EventTitle, ae.EventDescription, ae.CreatedDate, ae.UpdatedDate
FROM dbo.AssetEvents ae
INNER JOIN dbo.StoryAssets sa ON sa.StoryAssetID = ae.StoryAssetID
INNER JOIN dbo.AssetKinds ak ON ak.AssetKindID = sa.AssetKindID
INNER JOIN dbo.AssetEventTypes aet ON aet.AssetEventTypeID = ae.AssetEventTypeID
INNER JOIN dbo.Scenes s ON s.SceneID = ae.SceneID
INNER JOIN dbo.Chapters c ON c.ChapterID = s.ChapterID
INNER JOIN dbo.Books b ON b.BookID = c.BookID
LEFT JOIN dbo.AssetStates fs ON fs.AssetStateID = ae.FromStateID
LEFT JOIN dbo.AssetStates ts ON ts.AssetStateID = ae.ToStateID
WHERE sa.ProjectID = @ProjectID AND sa.IsArchived = 0 AND (@BookID IS NULL OR b.BookID = @BookID)
ORDER BY b.SortOrder, c.SortOrder, s.SortOrder, sa.AssetName;
END;
GO
CREATE OR ALTER PROCEDURE dbo.AssetDependency_CheckForEvent
@StoryAssetID int,
@SceneID int,
@ToStateID int = NULL
AS
BEGIN
SET NOCOUNT ON;
;WITH CurrentScene AS
(
SELECT b.SortOrder AS BookSortOrder, c.SortOrder AS ChapterSortOrder, s.SortOrder AS SceneSortOrder
FROM dbo.Scenes s
INNER JOIN dbo.Chapters c ON c.ChapterID = s.ChapterID
INNER JOIN dbo.Books b ON b.BookID = c.BookID
WHERE s.SceneID = @SceneID
)
SELECT ad.AssetDependencyID, source.AssetName AS SourceAssetName, target.AssetName AS TargetAssetName,
adt.TypeName AS DependencyTypeName, srs.StateName AS SourceRequiredStateName,
tbs.StateName AS TargetBlockedStateName, ad.Description
FROM dbo.AssetDependencies ad
INNER JOIN dbo.AssetDependencyTypes adt ON adt.AssetDependencyTypeID = ad.AssetDependencyTypeID
INNER JOIN dbo.StoryAssets source ON source.StoryAssetID = ad.SourceAssetID
INNER JOIN dbo.StoryAssets target ON target.StoryAssetID = ad.TargetAssetID
LEFT JOIN dbo.AssetStates srs ON srs.AssetStateID = ad.SourceRequiredStateID
LEFT JOIN dbo.AssetStates tbs ON tbs.AssetStateID = ad.TargetBlockedStateID
CROSS JOIN CurrentScene cs
WHERE ad.IsArchived = 0
AND adt.IsValidatingDependency = 1
AND ad.TargetAssetID = @StoryAssetID
AND (@ToStateID IS NULL OR ad.TargetBlockedStateID IS NULL OR ad.TargetBlockedStateID = @ToStateID)
AND ad.SourceRequiredStateID IS NOT NULL
AND NOT EXISTS
(
SELECT 1
FROM dbo.AssetEvents ae
INNER JOIN dbo.Scenes s ON s.SceneID = ae.SceneID
INNER JOIN dbo.Chapters c ON c.ChapterID = s.ChapterID
INNER JOIN dbo.Books b ON b.BookID = c.BookID
WHERE ae.StoryAssetID = ad.SourceAssetID
AND ae.ToStateID = ad.SourceRequiredStateID
AND (
b.SortOrder < cs.BookSortOrder
OR (b.SortOrder = cs.BookSortOrder AND c.SortOrder < cs.ChapterSortOrder)
OR (b.SortOrder = cs.BookSortOrder AND c.SortOrder = cs.ChapterSortOrder AND s.SortOrder < cs.SceneSortOrder)
)
);
END;
GO

View File

@ -0,0 +1,821 @@
SET ANSI_NULLS ON;
GO
SET QUOTED_IDENTIFIER ON;
GO
IF OBJECT_ID(N'dbo.CharacterRoleInSceneTypes', N'U') IS NULL
BEGIN
CREATE TABLE dbo.CharacterRoleInSceneTypes
(
CharacterRoleInSceneTypeID int IDENTITY(1,1) NOT NULL CONSTRAINT PK_CharacterRoleInSceneTypes PRIMARY KEY,
TypeName nvarchar(100) NOT NULL,
SortOrder int NOT NULL,
IsActive bit NOT NULL CONSTRAINT DF_CharacterRoleInSceneTypes_IsActive DEFAULT 1,
CONSTRAINT UQ_CharacterRoleInSceneTypes_TypeName UNIQUE (TypeName)
);
END;
GO
IF OBJECT_ID(N'dbo.PresenceTypes', N'U') IS NULL
BEGIN
CREATE TABLE dbo.PresenceTypes
(
PresenceTypeID int IDENTITY(1,1) NOT NULL CONSTRAINT PK_PresenceTypes PRIMARY KEY,
TypeName nvarchar(100) NOT NULL,
SortOrder int NOT NULL,
IsActive bit NOT NULL CONSTRAINT DF_PresenceTypes_IsActive DEFAULT 1,
CONSTRAINT UQ_PresenceTypes_TypeName UNIQUE (TypeName)
);
END;
GO
IF OBJECT_ID(N'dbo.Characters', N'U') IS NULL
BEGIN
CREATE TABLE dbo.Characters
(
CharacterID int IDENTITY(1,1) NOT NULL CONSTRAINT PK_Characters PRIMARY KEY,
ProjectID int NOT NULL,
CharacterName nvarchar(200) NOT NULL,
ShortName nvarchar(100) NULL,
Sex nvarchar(50) NULL,
BirthDate date NULL,
AgeAtSeriesStart int NULL,
AgeReferenceSceneID int NULL,
Height nvarchar(50) NULL,
EyeColour nvarchar(50) NULL,
DefaultDescription nvarchar(max) NULL,
CreatedDate datetime2 NOT NULL CONSTRAINT DF_Characters_CreatedDate DEFAULT SYSUTCDATETIME(),
UpdatedDate datetime2 NOT NULL CONSTRAINT DF_Characters_UpdatedDate DEFAULT SYSUTCDATETIME(),
IsArchived bit NOT NULL CONSTRAINT DF_Characters_IsArchived DEFAULT 0,
CONSTRAINT FK_Characters_Projects FOREIGN KEY (ProjectID) REFERENCES dbo.Projects(ProjectID),
CONSTRAINT FK_Characters_AgeReferenceScene FOREIGN KEY (AgeReferenceSceneID) REFERENCES dbo.Scenes(SceneID)
);
END;
GO
IF OBJECT_ID(N'dbo.SceneCharacters', N'U') IS NULL
BEGIN
CREATE TABLE dbo.SceneCharacters
(
SceneCharacterID int IDENTITY(1,1) NOT NULL CONSTRAINT PK_SceneCharacters PRIMARY KEY,
SceneID int NOT NULL,
CharacterID int NOT NULL,
RoleInSceneTypeID int NULL,
PresenceTypeID int NULL,
LocationID int NULL,
EntryLocationID int NULL,
ExitLocationID int NULL,
AppearanceNotes nvarchar(max) NULL,
OutfitDescription nvarchar(max) NULL,
PhysicalCondition nvarchar(max) NULL,
EmotionalState nvarchar(max) NULL,
KnowledgeNotes nvarchar(max) NULL,
CreatedDate datetime2 NOT NULL CONSTRAINT DF_SceneCharacters_CreatedDate DEFAULT SYSUTCDATETIME(),
UpdatedDate datetime2 NOT NULL CONSTRAINT DF_SceneCharacters_UpdatedDate DEFAULT SYSUTCDATETIME(),
CONSTRAINT FK_SceneCharacters_Scenes FOREIGN KEY (SceneID) REFERENCES dbo.Scenes(SceneID),
CONSTRAINT FK_SceneCharacters_Characters FOREIGN KEY (CharacterID) REFERENCES dbo.Characters(CharacterID),
CONSTRAINT FK_SceneCharacters_RoleTypes FOREIGN KEY (RoleInSceneTypeID) REFERENCES dbo.CharacterRoleInSceneTypes(CharacterRoleInSceneTypeID),
CONSTRAINT FK_SceneCharacters_PresenceTypes FOREIGN KEY (PresenceTypeID) REFERENCES dbo.PresenceTypes(PresenceTypeID)
);
END;
GO
IF OBJECT_ID(N'dbo.CharacterAttributeTypes', N'U') IS NULL
BEGIN
CREATE TABLE dbo.CharacterAttributeTypes
(
CharacterAttributeTypeID int IDENTITY(1,1) NOT NULL CONSTRAINT PK_CharacterAttributeTypes PRIMARY KEY,
ProjectID int NULL,
AttributeName nvarchar(100) NOT NULL,
DataType nvarchar(50) NOT NULL CONSTRAINT DF_CharacterAttributeTypes_DataType DEFAULT N'Text',
IsNormallyStable bit NOT NULL CONSTRAINT DF_CharacterAttributeTypes_IsNormallyStable DEFAULT 0,
SortOrder int NOT NULL,
IsActive bit NOT NULL CONSTRAINT DF_CharacterAttributeTypes_IsActive DEFAULT 1,
CONSTRAINT FK_CharacterAttributeTypes_Projects FOREIGN KEY (ProjectID) REFERENCES dbo.Projects(ProjectID)
);
END;
GO
IF OBJECT_ID(N'dbo.CharacterAttributeEvents', N'U') IS NULL
BEGIN
CREATE TABLE dbo.CharacterAttributeEvents
(
CharacterAttributeEventID int IDENTITY(1,1) NOT NULL CONSTRAINT PK_CharacterAttributeEvents PRIMARY KEY,
CharacterID int NOT NULL,
CharacterAttributeTypeID int NOT NULL,
SceneID int NOT NULL,
AttributeValue nvarchar(500) NOT NULL,
Description nvarchar(max) NULL,
CreatedDate datetime2 NOT NULL CONSTRAINT DF_CharacterAttributeEvents_CreatedDate DEFAULT SYSUTCDATETIME(),
UpdatedDate datetime2 NOT NULL CONSTRAINT DF_CharacterAttributeEvents_UpdatedDate DEFAULT SYSUTCDATETIME(),
CONSTRAINT FK_CharacterAttributeEvents_Characters FOREIGN KEY (CharacterID) REFERENCES dbo.Characters(CharacterID),
CONSTRAINT FK_CharacterAttributeEvents_Types FOREIGN KEY (CharacterAttributeTypeID) REFERENCES dbo.CharacterAttributeTypes(CharacterAttributeTypeID),
CONSTRAINT FK_CharacterAttributeEvents_Scenes FOREIGN KEY (SceneID) REFERENCES dbo.Scenes(SceneID)
);
END;
GO
IF OBJECT_ID(N'dbo.KnowledgeStates', N'U') IS NULL
BEGIN
CREATE TABLE dbo.KnowledgeStates
(
KnowledgeStateID int IDENTITY(1,1) NOT NULL CONSTRAINT PK_KnowledgeStates PRIMARY KEY,
StateName nvarchar(100) NOT NULL,
SortOrder int NOT NULL,
IsActive bit NOT NULL CONSTRAINT DF_KnowledgeStates_IsActive DEFAULT 1,
CONSTRAINT UQ_KnowledgeStates_StateName UNIQUE (StateName)
);
END;
GO
IF OBJECT_ID(N'dbo.CharacterKnowledge', N'U') IS NULL
BEGIN
CREATE TABLE dbo.CharacterKnowledge
(
CharacterKnowledgeID int IDENTITY(1,1) NOT NULL CONSTRAINT PK_CharacterKnowledge PRIMARY KEY,
CharacterID int NOT NULL,
StoryAssetID int NULL,
PlotThreadID int NULL,
SceneID int NOT NULL,
KnowledgeStateID int NOT NULL,
SourceEventID int NULL,
Description nvarchar(max) NULL,
CreatedDate datetime2 NOT NULL CONSTRAINT DF_CharacterKnowledge_CreatedDate DEFAULT SYSUTCDATETIME(),
UpdatedDate datetime2 NOT NULL CONSTRAINT DF_CharacterKnowledge_UpdatedDate DEFAULT SYSUTCDATETIME(),
CONSTRAINT FK_CharacterKnowledge_Characters FOREIGN KEY (CharacterID) REFERENCES dbo.Characters(CharacterID),
CONSTRAINT FK_CharacterKnowledge_StoryAssets FOREIGN KEY (StoryAssetID) REFERENCES dbo.StoryAssets(StoryAssetID),
CONSTRAINT FK_CharacterKnowledge_PlotThreads FOREIGN KEY (PlotThreadID) REFERENCES dbo.PlotThreads(PlotThreadID),
CONSTRAINT FK_CharacterKnowledge_Scenes FOREIGN KEY (SceneID) REFERENCES dbo.Scenes(SceneID),
CONSTRAINT FK_CharacterKnowledge_States FOREIGN KEY (KnowledgeStateID) REFERENCES dbo.KnowledgeStates(KnowledgeStateID)
);
END;
GO
IF OBJECT_ID(N'dbo.RelationshipTypes', N'U') IS NULL
BEGIN
CREATE TABLE dbo.RelationshipTypes
(
RelationshipTypeID int IDENTITY(1,1) NOT NULL CONSTRAINT PK_RelationshipTypes PRIMARY KEY,
TypeName nvarchar(100) NOT NULL,
IsPermanentDefault bit NOT NULL CONSTRAINT DF_RelationshipTypes_IsPermanentDefault DEFAULT 0,
SortOrder int NOT NULL,
IsActive bit NOT NULL CONSTRAINT DF_RelationshipTypes_IsActive DEFAULT 1,
CONSTRAINT UQ_RelationshipTypes_TypeName UNIQUE (TypeName)
);
END;
GO
IF OBJECT_ID(N'dbo.RelationshipStates', N'U') IS NULL
BEGIN
CREATE TABLE dbo.RelationshipStates
(
RelationshipStateID int IDENTITY(1,1) NOT NULL CONSTRAINT PK_RelationshipStates PRIMARY KEY,
StateName nvarchar(100) NOT NULL,
SortOrder int NOT NULL,
IsActive bit NOT NULL CONSTRAINT DF_RelationshipStates_IsActive DEFAULT 1,
CONSTRAINT UQ_RelationshipStates_StateName UNIQUE (StateName)
);
END;
GO
IF OBJECT_ID(N'dbo.CharacterRelationships', N'U') IS NULL
BEGIN
CREATE TABLE dbo.CharacterRelationships
(
CharacterRelationshipID int IDENTITY(1,1) NOT NULL CONSTRAINT PK_CharacterRelationships PRIMARY KEY,
ProjectID int NOT NULL,
CharacterAID int NOT NULL,
CharacterBID int NOT NULL,
RelationshipTypeID int NOT NULL,
IsPermanent bit NOT NULL CONSTRAINT DF_CharacterRelationships_IsPermanent DEFAULT 0,
StartSceneID int NULL,
EndSceneID int NULL,
IsKnownToReader bit NOT NULL CONSTRAINT DF_CharacterRelationships_IsKnownToReader DEFAULT 0,
Notes nvarchar(max) NULL,
CreatedDate datetime2 NOT NULL CONSTRAINT DF_CharacterRelationships_CreatedDate DEFAULT SYSUTCDATETIME(),
UpdatedDate datetime2 NOT NULL CONSTRAINT DF_CharacterRelationships_UpdatedDate DEFAULT SYSUTCDATETIME(),
IsArchived bit NOT NULL CONSTRAINT DF_CharacterRelationships_IsArchived DEFAULT 0,
CONSTRAINT FK_CharacterRelationships_Projects FOREIGN KEY (ProjectID) REFERENCES dbo.Projects(ProjectID),
CONSTRAINT FK_CharacterRelationships_CharacterA FOREIGN KEY (CharacterAID) REFERENCES dbo.Characters(CharacterID),
CONSTRAINT FK_CharacterRelationships_CharacterB FOREIGN KEY (CharacterBID) REFERENCES dbo.Characters(CharacterID),
CONSTRAINT FK_CharacterRelationships_Types FOREIGN KEY (RelationshipTypeID) REFERENCES dbo.RelationshipTypes(RelationshipTypeID),
CONSTRAINT FK_CharacterRelationships_StartScene FOREIGN KEY (StartSceneID) REFERENCES dbo.Scenes(SceneID),
CONSTRAINT FK_CharacterRelationships_EndScene FOREIGN KEY (EndSceneID) REFERENCES dbo.Scenes(SceneID)
);
END;
GO
IF OBJECT_ID(N'dbo.RelationshipEvents', N'U') IS NULL
BEGIN
CREATE TABLE dbo.RelationshipEvents
(
RelationshipEventID int IDENTITY(1,1) NOT NULL CONSTRAINT PK_RelationshipEvents PRIMARY KEY,
CharacterRelationshipID int NOT NULL,
SceneID int NOT NULL,
RelationshipStateID int NOT NULL,
Intensity int NULL,
IsKnownToOtherCharacter bit NOT NULL CONSTRAINT DF_RelationshipEvents_IsKnownToOtherCharacter DEFAULT 0,
Description nvarchar(max) NULL,
CreatedDate datetime2 NOT NULL CONSTRAINT DF_RelationshipEvents_CreatedDate DEFAULT SYSUTCDATETIME(),
UpdatedDate datetime2 NOT NULL CONSTRAINT DF_RelationshipEvents_UpdatedDate DEFAULT SYSUTCDATETIME(),
CONSTRAINT FK_RelationshipEvents_Relationships FOREIGN KEY (CharacterRelationshipID) REFERENCES dbo.CharacterRelationships(CharacterRelationshipID),
CONSTRAINT FK_RelationshipEvents_Scenes FOREIGN KEY (SceneID) REFERENCES dbo.Scenes(SceneID),
CONSTRAINT FK_RelationshipEvents_States FOREIGN KEY (RelationshipStateID) REFERENCES dbo.RelationshipStates(RelationshipStateID),
CONSTRAINT CK_RelationshipEvents_Intensity CHECK (Intensity IS NULL OR Intensity BETWEEN 1 AND 10)
);
END;
GO
IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = N'IX_Characters_ProjectID' AND object_id = OBJECT_ID(N'dbo.Characters'))
CREATE INDEX IX_Characters_ProjectID ON dbo.Characters(ProjectID, IsArchived, CharacterName);
GO
IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = N'IX_SceneCharacters_SceneID' AND object_id = OBJECT_ID(N'dbo.SceneCharacters'))
CREATE INDEX IX_SceneCharacters_SceneID ON dbo.SceneCharacters(SceneID, CharacterID);
GO
IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = N'IX_CharacterKnowledge_SceneID' AND object_id = OBJECT_ID(N'dbo.CharacterKnowledge'))
CREATE INDEX IX_CharacterKnowledge_SceneID ON dbo.CharacterKnowledge(SceneID, CharacterID);
GO
IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = N'IX_RelationshipEvents_SceneID' AND object_id = OBJECT_ID(N'dbo.RelationshipEvents'))
CREATE INDEX IX_RelationshipEvents_SceneID ON dbo.RelationshipEvents(SceneID, CharacterRelationshipID);
GO
MERGE dbo.CharacterRoleInSceneTypes AS target
USING (VALUES
(N'POV Character', 10), (N'Main Participant', 20), (N'Supporting Participant', 30), (N'Witness', 40),
(N'Background', 50), (N'Mentioned Only', 60), (N'Hidden', 70), (N'Overhearing', 80)
) AS source (TypeName, SortOrder)
ON target.TypeName = source.TypeName
WHEN NOT MATCHED THEN INSERT (TypeName, SortOrder) VALUES (source.TypeName, source.SortOrder);
GO
MERGE dbo.PresenceTypes AS target
USING (VALUES
(N'Present', 10), (N'Arrives', 20), (N'Leaves', 30), (N'Nearby', 40), (N'Hidden', 50),
(N'Overhearing', 60), (N'Mentioned Only', 70), (N'Off-screen', 80), (N'Unknown', 90)
) AS source (TypeName, SortOrder)
ON target.TypeName = source.TypeName
WHEN NOT MATCHED THEN INSERT (TypeName, SortOrder) VALUES (source.TypeName, source.SortOrder);
GO
MERGE dbo.CharacterAttributeTypes AS target
USING (VALUES
(N'Hair Colour', N'Text', 1, 10), (N'Hair Style', N'Text', 0, 20), (N'Injury', N'Text', 0, 30),
(N'Disguise', N'Text', 0, 40), (N'Public Name', N'Text', 1, 50), (N'Mental State', N'Text', 0, 60),
(N'Physical Condition', N'Text', 0, 70), (N'Reputation', N'Text', 0, 80), (N'Confidence', N'Number', 0, 90),
(N'Clothing Note', N'Text', 0, 100)
) AS source (AttributeName, DataType, IsNormallyStable, SortOrder)
ON target.ProjectID IS NULL AND target.AttributeName = source.AttributeName
WHEN NOT MATCHED THEN INSERT (AttributeName, DataType, IsNormallyStable, SortOrder) VALUES (source.AttributeName, source.DataType, source.IsNormallyStable, source.SortOrder);
GO
MERGE dbo.KnowledgeStates AS target
USING (VALUES
(N'Unaware', 10), (N'Suspects', 20), (N'Partially Knows', 30), (N'Knows', 40),
(N'Misunderstands', 50), (N'Hiding', 60), (N'Lying About', 70)
) AS source (StateName, SortOrder)
ON target.StateName = source.StateName
WHEN NOT MATCHED THEN INSERT (StateName, SortOrder) VALUES (source.StateName, source.SortOrder);
GO
MERGE dbo.RelationshipTypes AS target
USING (VALUES
(N'Parent', 1, 10), (N'Child', 1, 20), (N'Sibling', 1, 30), (N'Half-sibling', 1, 40),
(N'Adoptive Parent', 1, 50), (N'Biological Parent', 1, 60), (N'Guardian', 1, 70),
(N'Friend', 0, 80), (N'Enemy', 0, 90), (N'Lover', 0, 100), (N'Ex-lover', 0, 110),
(N'Spouse', 1, 120), (N'Ally', 0, 130), (N'Rival', 0, 140), (N'Protector', 0, 150),
(N'Suspicious Of', 0, 160), (N'Afraid Of', 0, 170), (N'Manipulating', 0, 180), (N'Estranged From', 0, 190)
) AS source (TypeName, IsPermanentDefault, SortOrder)
ON target.TypeName = source.TypeName
WHEN NOT MATCHED THEN INSERT (TypeName, IsPermanentDefault, SortOrder) VALUES (source.TypeName, source.IsPermanentDefault, source.SortOrder);
GO
MERGE dbo.RelationshipStates AS target
USING (VALUES
(N'Unknown', 10), (N'Positive', 20), (N'Neutral', 30), (N'Negative', 40), (N'Tense', 50),
(N'Friendly', 60), (N'Romantic', 70), (N'Sexual Tension', 80), (N'In Love', 90),
(N'Distrustful', 100), (N'Hostile', 110), (N'Protective', 120), (N'Estranged', 130),
(N'Broken', 140), (N'Reconciled', 150)
) AS source (StateName, SortOrder)
ON target.StateName = source.StateName
WHEN NOT MATCHED THEN INSERT (StateName, SortOrder) VALUES (source.StateName, source.SortOrder);
GO
CREATE OR ALTER PROCEDURE dbo.CharacterLookup_All
AS
BEGIN
SET NOCOUNT ON;
SELECT CharacterRoleInSceneTypeID, TypeName, SortOrder, IsActive FROM dbo.CharacterRoleInSceneTypes WHERE IsActive = 1 ORDER BY SortOrder;
SELECT PresenceTypeID, TypeName, SortOrder, IsActive FROM dbo.PresenceTypes WHERE IsActive = 1 ORDER BY SortOrder;
SELECT CharacterAttributeTypeID, ProjectID, AttributeName, DataType, IsNormallyStable, SortOrder, IsActive FROM dbo.CharacterAttributeTypes WHERE IsActive = 1 ORDER BY SortOrder;
SELECT KnowledgeStateID, StateName, SortOrder, IsActive FROM dbo.KnowledgeStates WHERE IsActive = 1 ORDER BY SortOrder;
SELECT RelationshipTypeID, TypeName, IsPermanentDefault, SortOrder, IsActive FROM dbo.RelationshipTypes WHERE IsActive = 1 ORDER BY SortOrder;
SELECT RelationshipStateID, StateName, SortOrder, IsActive FROM dbo.RelationshipStates WHERE IsActive = 1 ORDER BY SortOrder;
END;
GO
CREATE OR ALTER PROCEDURE dbo.Character_ListByProject
@ProjectID int
AS
BEGIN
SET NOCOUNT ON;
SELECT CharacterID, ProjectID, CharacterName, ShortName, Sex, BirthDate, AgeAtSeriesStart, AgeReferenceSceneID,
Height, EyeColour, DefaultDescription, CreatedDate, UpdatedDate, IsArchived
FROM dbo.Characters
WHERE ProjectID = @ProjectID AND IsArchived = 0
ORDER BY CharacterName;
END;
GO
CREATE OR ALTER PROCEDURE dbo.Character_Get
@CharacterID int
AS
BEGIN
SET NOCOUNT ON;
SELECT CharacterID, ProjectID, CharacterName, ShortName, Sex, BirthDate, AgeAtSeriesStart, AgeReferenceSceneID,
Height, EyeColour, DefaultDescription, CreatedDate, UpdatedDate, IsArchived
FROM dbo.Characters
WHERE CharacterID = @CharacterID AND IsArchived = 0;
END;
GO
CREATE OR ALTER PROCEDURE dbo.Character_Save
@CharacterID int = NULL,
@ProjectID int,
@CharacterName nvarchar(200),
@ShortName nvarchar(100) = NULL,
@Sex nvarchar(50) = NULL,
@BirthDate date = NULL,
@AgeAtSeriesStart int = NULL,
@AgeReferenceSceneID int = NULL,
@Height nvarchar(50) = NULL,
@EyeColour nvarchar(50) = NULL,
@DefaultDescription nvarchar(max) = NULL
AS
BEGIN
SET NOCOUNT ON;
IF @CharacterID IS NULL OR @CharacterID = 0
BEGIN
INSERT dbo.Characters (ProjectID, CharacterName, ShortName, Sex, BirthDate, AgeAtSeriesStart, AgeReferenceSceneID, Height, EyeColour, DefaultDescription)
VALUES (@ProjectID, @CharacterName, @ShortName, @Sex, @BirthDate, @AgeAtSeriesStart, @AgeReferenceSceneID, @Height, @EyeColour, @DefaultDescription);
SET @CharacterID = CAST(SCOPE_IDENTITY() AS int);
END
ELSE
BEGIN
UPDATE dbo.Characters
SET CharacterName = @CharacterName, ShortName = @ShortName, Sex = @Sex, BirthDate = @BirthDate,
AgeAtSeriesStart = @AgeAtSeriesStart, AgeReferenceSceneID = @AgeReferenceSceneID,
Height = @Height, EyeColour = @EyeColour, DefaultDescription = @DefaultDescription,
UpdatedDate = SYSUTCDATETIME()
WHERE CharacterID = @CharacterID;
END
SELECT @CharacterID;
END;
GO
CREATE OR ALTER PROCEDURE dbo.Character_Archive
@CharacterID int
AS
BEGIN
SET NOCOUNT ON;
UPDATE dbo.Characters SET IsArchived = 1, UpdatedDate = SYSUTCDATETIME() WHERE CharacterID = @CharacterID;
END;
GO
CREATE OR ALTER PROCEDURE dbo.SceneCharacter_ListByScene
@SceneID int
AS
BEGIN
SET NOCOUNT ON;
SELECT sc.SceneCharacterID, sc.SceneID, sc.CharacterID, c.ProjectID, c.CharacterName, c.ShortName, c.BirthDate, c.AgeAtSeriesStart,
sc.RoleInSceneTypeID, role.TypeName AS RoleInSceneTypeName, sc.PresenceTypeID, presence.TypeName AS PresenceTypeName,
sc.AppearanceNotes, sc.OutfitDescription, sc.PhysicalCondition, sc.EmotionalState, sc.KnowledgeNotes,
sc.CreatedDate, sc.UpdatedDate
FROM dbo.SceneCharacters sc
INNER JOIN dbo.Characters c ON c.CharacterID = sc.CharacterID
LEFT JOIN dbo.CharacterRoleInSceneTypes role ON role.CharacterRoleInSceneTypeID = sc.RoleInSceneTypeID
LEFT JOIN dbo.PresenceTypes presence ON presence.PresenceTypeID = sc.PresenceTypeID
WHERE sc.SceneID = @SceneID AND c.IsArchived = 0
ORDER BY role.SortOrder, c.CharacterName;
END;
GO
CREATE OR ALTER PROCEDURE dbo.SceneCharacter_Save
@SceneCharacterID int = NULL,
@SceneID int,
@CharacterID int,
@RoleInSceneTypeID int = NULL,
@PresenceTypeID int = NULL,
@AppearanceNotes nvarchar(max) = NULL,
@OutfitDescription nvarchar(max) = NULL,
@PhysicalCondition nvarchar(max) = NULL,
@EmotionalState nvarchar(max) = NULL,
@KnowledgeNotes nvarchar(max) = NULL
AS
BEGIN
SET NOCOUNT ON;
IF @SceneCharacterID IS NULL OR @SceneCharacterID = 0
BEGIN
SELECT @SceneCharacterID = SceneCharacterID FROM dbo.SceneCharacters WHERE SceneID = @SceneID AND CharacterID = @CharacterID;
END
IF @SceneCharacterID IS NULL OR @SceneCharacterID = 0
BEGIN
INSERT dbo.SceneCharacters (SceneID, CharacterID, RoleInSceneTypeID, PresenceTypeID, AppearanceNotes, OutfitDescription, PhysicalCondition, EmotionalState, KnowledgeNotes)
VALUES (@SceneID, @CharacterID, @RoleInSceneTypeID, @PresenceTypeID, @AppearanceNotes, @OutfitDescription, @PhysicalCondition, @EmotionalState, @KnowledgeNotes);
SET @SceneCharacterID = CAST(SCOPE_IDENTITY() AS int);
END
ELSE
BEGIN
UPDATE dbo.SceneCharacters
SET CharacterID = @CharacterID, RoleInSceneTypeID = @RoleInSceneTypeID, PresenceTypeID = @PresenceTypeID,
AppearanceNotes = @AppearanceNotes, OutfitDescription = @OutfitDescription, PhysicalCondition = @PhysicalCondition,
EmotionalState = @EmotionalState, KnowledgeNotes = @KnowledgeNotes, UpdatedDate = SYSUTCDATETIME()
WHERE SceneCharacterID = @SceneCharacterID;
END
SELECT @SceneCharacterID;
END;
GO
CREATE OR ALTER PROCEDURE dbo.SceneCharacter_Delete
@SceneCharacterID int
AS
BEGIN
SET NOCOUNT ON;
DELETE FROM dbo.SceneCharacters WHERE SceneCharacterID = @SceneCharacterID;
END;
GO
CREATE OR ALTER PROCEDURE dbo.CharacterAttributeEvent_ListByCharacter
@CharacterID int
AS
BEGIN
SET NOCOUNT ON;
SELECT cae.CharacterAttributeEventID, cae.CharacterID, c.CharacterName, cae.CharacterAttributeTypeID, cat.AttributeName,
cae.SceneID, s.SceneNumber, s.SceneTitle, ch.ChapterNumber, b.BookTitle, cae.AttributeValue, cae.Description, cae.CreatedDate, cae.UpdatedDate
FROM dbo.CharacterAttributeEvents cae
INNER JOIN dbo.Characters c ON c.CharacterID = cae.CharacterID
INNER JOIN dbo.CharacterAttributeTypes cat ON cat.CharacterAttributeTypeID = cae.CharacterAttributeTypeID
INNER JOIN dbo.Scenes s ON s.SceneID = cae.SceneID
INNER JOIN dbo.Chapters ch ON ch.ChapterID = s.ChapterID
INNER JOIN dbo.Books b ON b.BookID = ch.BookID
WHERE cae.CharacterID = @CharacterID
ORDER BY b.SortOrder, ch.SortOrder, s.SortOrder, cat.SortOrder;
END;
GO
CREATE OR ALTER PROCEDURE dbo.CharacterAttributeEvent_Save
@CharacterAttributeEventID int = NULL,
@CharacterID int,
@CharacterAttributeTypeID int,
@SceneID int,
@AttributeValue nvarchar(500),
@Description nvarchar(max) = NULL
AS
BEGIN
SET NOCOUNT ON;
IF @CharacterAttributeEventID IS NULL OR @CharacterAttributeEventID = 0
BEGIN
INSERT dbo.CharacterAttributeEvents (CharacterID, CharacterAttributeTypeID, SceneID, AttributeValue, Description)
VALUES (@CharacterID, @CharacterAttributeTypeID, @SceneID, @AttributeValue, @Description);
SET @CharacterAttributeEventID = CAST(SCOPE_IDENTITY() AS int);
END
ELSE
BEGIN
UPDATE dbo.CharacterAttributeEvents
SET CharacterID = @CharacterID, CharacterAttributeTypeID = @CharacterAttributeTypeID, SceneID = @SceneID,
AttributeValue = @AttributeValue, Description = @Description, UpdatedDate = SYSUTCDATETIME()
WHERE CharacterAttributeEventID = @CharacterAttributeEventID;
END
SELECT @CharacterAttributeEventID;
END;
GO
CREATE OR ALTER PROCEDURE dbo.CharacterKnowledge_ListByScene
@SceneID int
AS
BEGIN
SET NOCOUNT ON;
SELECT ck.CharacterKnowledgeID, ck.CharacterID, c.CharacterName, ck.StoryAssetID, sa.AssetName, ck.PlotThreadID, pt.ThreadTitle,
ck.SceneID, ck.KnowledgeStateID, ks.StateName AS KnowledgeStateName, ck.SourceEventID, ck.Description, ck.CreatedDate, ck.UpdatedDate
FROM dbo.CharacterKnowledge ck
INNER JOIN dbo.Characters c ON c.CharacterID = ck.CharacterID
INNER JOIN dbo.KnowledgeStates ks ON ks.KnowledgeStateID = ck.KnowledgeStateID
LEFT JOIN dbo.StoryAssets sa ON sa.StoryAssetID = ck.StoryAssetID
LEFT JOIN dbo.PlotThreads pt ON pt.PlotThreadID = ck.PlotThreadID
WHERE ck.SceneID = @SceneID
ORDER BY c.CharacterName, ks.SortOrder;
END;
GO
CREATE OR ALTER PROCEDURE dbo.CharacterKnowledge_ListByCharacter
@CharacterID int
AS
BEGIN
SET NOCOUNT ON;
SELECT ck.CharacterKnowledgeID, ck.CharacterID, c.CharacterName, ck.StoryAssetID, sa.AssetName, ck.PlotThreadID, pt.ThreadTitle,
ck.SceneID, s.SceneNumber, s.SceneTitle, ch.ChapterNumber, b.BookTitle,
ck.KnowledgeStateID, ks.StateName AS KnowledgeStateName, ck.SourceEventID, ck.Description, ck.CreatedDate, ck.UpdatedDate
FROM dbo.CharacterKnowledge ck
INNER JOIN dbo.Characters c ON c.CharacterID = ck.CharacterID
INNER JOIN dbo.KnowledgeStates ks ON ks.KnowledgeStateID = ck.KnowledgeStateID
INNER JOIN dbo.Scenes s ON s.SceneID = ck.SceneID
INNER JOIN dbo.Chapters ch ON ch.ChapterID = s.ChapterID
INNER JOIN dbo.Books b ON b.BookID = ch.BookID
LEFT JOIN dbo.StoryAssets sa ON sa.StoryAssetID = ck.StoryAssetID
LEFT JOIN dbo.PlotThreads pt ON pt.PlotThreadID = ck.PlotThreadID
WHERE ck.CharacterID = @CharacterID
ORDER BY b.SortOrder, ch.SortOrder, s.SortOrder;
END;
GO
CREATE OR ALTER PROCEDURE dbo.CharacterKnowledge_Save
@CharacterKnowledgeID int = NULL,
@CharacterID int,
@StoryAssetID int = NULL,
@PlotThreadID int = NULL,
@SceneID int,
@KnowledgeStateID int,
@SourceEventID int = NULL,
@Description nvarchar(max) = NULL
AS
BEGIN
SET NOCOUNT ON;
IF @CharacterKnowledgeID IS NULL OR @CharacterKnowledgeID = 0
BEGIN
INSERT dbo.CharacterKnowledge (CharacterID, StoryAssetID, PlotThreadID, SceneID, KnowledgeStateID, SourceEventID, Description)
VALUES (@CharacterID, @StoryAssetID, @PlotThreadID, @SceneID, @KnowledgeStateID, @SourceEventID, @Description);
SET @CharacterKnowledgeID = CAST(SCOPE_IDENTITY() AS int);
END
ELSE
BEGIN
UPDATE dbo.CharacterKnowledge
SET CharacterID = @CharacterID, StoryAssetID = @StoryAssetID, PlotThreadID = @PlotThreadID, SceneID = @SceneID,
KnowledgeStateID = @KnowledgeStateID, SourceEventID = @SourceEventID, Description = @Description, UpdatedDate = SYSUTCDATETIME()
WHERE CharacterKnowledgeID = @CharacterKnowledgeID;
END
SELECT @CharacterKnowledgeID;
END;
GO
CREATE OR ALTER PROCEDURE dbo.Relationship_ListByCharacter
@CharacterID int
AS
BEGIN
SET NOCOUNT ON;
SELECT cr.CharacterRelationshipID, cr.ProjectID, cr.CharacterAID, ca.CharacterName AS CharacterAName,
cr.CharacterBID, cb.CharacterName AS CharacterBName, cr.RelationshipTypeID, rt.TypeName AS RelationshipTypeName,
cr.IsPermanent, cr.StartSceneID, cr.EndSceneID, cr.IsKnownToReader, cr.Notes, cr.CreatedDate, cr.UpdatedDate, cr.IsArchived
FROM dbo.CharacterRelationships cr
INNER JOIN dbo.Characters ca ON ca.CharacterID = cr.CharacterAID
INNER JOIN dbo.Characters cb ON cb.CharacterID = cr.CharacterBID
INNER JOIN dbo.RelationshipTypes rt ON rt.RelationshipTypeID = cr.RelationshipTypeID
WHERE cr.IsArchived = 0 AND @CharacterID IN (cr.CharacterAID, cr.CharacterBID)
ORDER BY ca.CharacterName, cb.CharacterName, rt.SortOrder;
END;
GO
CREATE OR ALTER PROCEDURE dbo.Relationship_ListByProject
@ProjectID int
AS
BEGIN
SET NOCOUNT ON;
SELECT cr.CharacterRelationshipID, cr.ProjectID, cr.CharacterAID, ca.CharacterName AS CharacterAName,
cr.CharacterBID, cb.CharacterName AS CharacterBName, cr.RelationshipTypeID, rt.TypeName AS RelationshipTypeName,
cr.IsPermanent, cr.StartSceneID, cr.EndSceneID, cr.IsKnownToReader, cr.Notes, cr.CreatedDate, cr.UpdatedDate, cr.IsArchived
FROM dbo.CharacterRelationships cr
INNER JOIN dbo.Characters ca ON ca.CharacterID = cr.CharacterAID
INNER JOIN dbo.Characters cb ON cb.CharacterID = cr.CharacterBID
INNER JOIN dbo.RelationshipTypes rt ON rt.RelationshipTypeID = cr.RelationshipTypeID
WHERE cr.ProjectID = @ProjectID AND cr.IsArchived = 0
ORDER BY ca.CharacterName, cb.CharacterName, rt.SortOrder;
END;
GO
CREATE OR ALTER PROCEDURE dbo.Relationship_Save
@CharacterRelationshipID int = NULL,
@ProjectID int,
@CharacterAID int,
@CharacterBID int,
@RelationshipTypeID int,
@IsPermanent bit,
@StartSceneID int = NULL,
@EndSceneID int = NULL,
@IsKnownToReader bit,
@Notes nvarchar(max) = NULL
AS
BEGIN
SET NOCOUNT ON;
IF @CharacterRelationshipID IS NULL OR @CharacterRelationshipID = 0
BEGIN
INSERT dbo.CharacterRelationships (ProjectID, CharacterAID, CharacterBID, RelationshipTypeID, IsPermanent, StartSceneID, EndSceneID, IsKnownToReader, Notes)
VALUES (@ProjectID, @CharacterAID, @CharacterBID, @RelationshipTypeID, @IsPermanent, @StartSceneID, @EndSceneID, @IsKnownToReader, @Notes);
SET @CharacterRelationshipID = CAST(SCOPE_IDENTITY() AS int);
END
ELSE
BEGIN
UPDATE dbo.CharacterRelationships
SET CharacterAID = @CharacterAID, CharacterBID = @CharacterBID, RelationshipTypeID = @RelationshipTypeID,
IsPermanent = @IsPermanent, StartSceneID = @StartSceneID, EndSceneID = @EndSceneID,
IsKnownToReader = @IsKnownToReader, Notes = @Notes, UpdatedDate = SYSUTCDATETIME()
WHERE CharacterRelationshipID = @CharacterRelationshipID;
END
SELECT @CharacterRelationshipID;
END;
GO
CREATE OR ALTER PROCEDURE dbo.Relationship_Archive
@CharacterRelationshipID int
AS
BEGIN
SET NOCOUNT ON;
UPDATE dbo.CharacterRelationships SET IsArchived = 1, UpdatedDate = SYSUTCDATETIME() WHERE CharacterRelationshipID = @CharacterRelationshipID;
END;
GO
CREATE OR ALTER PROCEDURE dbo.RelationshipEvent_ListByCharacter
@CharacterID int
AS
BEGIN
SET NOCOUNT ON;
SELECT re.RelationshipEventID, re.CharacterRelationshipID, cr.ProjectID, cr.CharacterAID, ca.CharacterName AS CharacterAName,
cr.CharacterBID, cb.CharacterName AS CharacterBName, rt.TypeName AS RelationshipTypeName,
re.SceneID, s.SceneNumber, s.SceneTitle, ch.ChapterNumber, b.BookTitle,
re.RelationshipStateID, rs.StateName AS RelationshipStateName, re.Intensity, re.IsKnownToOtherCharacter,
re.Description, re.CreatedDate, re.UpdatedDate
FROM dbo.RelationshipEvents re
INNER JOIN dbo.CharacterRelationships cr ON cr.CharacterRelationshipID = re.CharacterRelationshipID
INNER JOIN dbo.Characters ca ON ca.CharacterID = cr.CharacterAID
INNER JOIN dbo.Characters cb ON cb.CharacterID = cr.CharacterBID
INNER JOIN dbo.RelationshipTypes rt ON rt.RelationshipTypeID = cr.RelationshipTypeID
INNER JOIN dbo.RelationshipStates rs ON rs.RelationshipStateID = re.RelationshipStateID
INNER JOIN dbo.Scenes s ON s.SceneID = re.SceneID
INNER JOIN dbo.Chapters ch ON ch.ChapterID = s.ChapterID
INNER JOIN dbo.Books b ON b.BookID = ch.BookID
WHERE @CharacterID IN (cr.CharacterAID, cr.CharacterBID)
ORDER BY b.SortOrder, ch.SortOrder, s.SortOrder;
END;
GO
CREATE OR ALTER PROCEDURE dbo.RelationshipEvent_ListByScene
@SceneID int
AS
BEGIN
SET NOCOUNT ON;
SELECT re.RelationshipEventID, re.CharacterRelationshipID, cr.ProjectID, cr.CharacterAID, ca.CharacterName AS CharacterAName,
cr.CharacterBID, cb.CharacterName AS CharacterBName, rt.TypeName AS RelationshipTypeName,
re.SceneID, re.RelationshipStateID, rs.StateName AS RelationshipStateName, re.Intensity, re.IsKnownToOtherCharacter,
re.Description, re.CreatedDate, re.UpdatedDate
FROM dbo.RelationshipEvents re
INNER JOIN dbo.CharacterRelationships cr ON cr.CharacterRelationshipID = re.CharacterRelationshipID
INNER JOIN dbo.Characters ca ON ca.CharacterID = cr.CharacterAID
INNER JOIN dbo.Characters cb ON cb.CharacterID = cr.CharacterBID
INNER JOIN dbo.RelationshipTypes rt ON rt.RelationshipTypeID = cr.RelationshipTypeID
INNER JOIN dbo.RelationshipStates rs ON rs.RelationshipStateID = re.RelationshipStateID
WHERE re.SceneID = @SceneID
ORDER BY ca.CharacterName, cb.CharacterName;
END;
GO
CREATE OR ALTER PROCEDURE dbo.RelationshipEvent_Save
@RelationshipEventID int = NULL,
@CharacterRelationshipID int,
@SceneID int,
@RelationshipStateID int,
@Intensity int = NULL,
@IsKnownToOtherCharacter bit,
@Description nvarchar(max) = NULL
AS
BEGIN
SET NOCOUNT ON;
IF @RelationshipEventID IS NULL OR @RelationshipEventID = 0
BEGIN
INSERT dbo.RelationshipEvents (CharacterRelationshipID, SceneID, RelationshipStateID, Intensity, IsKnownToOtherCharacter, Description)
VALUES (@CharacterRelationshipID, @SceneID, @RelationshipStateID, @Intensity, @IsKnownToOtherCharacter, @Description);
SET @RelationshipEventID = CAST(SCOPE_IDENTITY() AS int);
END
ELSE
BEGIN
UPDATE dbo.RelationshipEvents
SET CharacterRelationshipID = @CharacterRelationshipID, SceneID = @SceneID, RelationshipStateID = @RelationshipStateID,
Intensity = @Intensity, IsKnownToOtherCharacter = @IsKnownToOtherCharacter,
Description = @Description, UpdatedDate = SYSUTCDATETIME()
WHERE RelationshipEventID = @RelationshipEventID;
END
SELECT @RelationshipEventID;
END;
GO
CREATE OR ALTER PROCEDURE dbo.CharacterTimeline_GetByProject
@ProjectID int,
@BookID int = NULL
AS
BEGIN
SET NOCOUNT ON;
SELECT TOP (12) CharacterID, ProjectID, CharacterName, ShortName, Sex, BirthDate, AgeAtSeriesStart, AgeReferenceSceneID,
Height, EyeColour, DefaultDescription, CreatedDate, UpdatedDate, IsArchived
FROM dbo.Characters
WHERE ProjectID = @ProjectID AND IsArchived = 0
ORDER BY CharacterName;
SELECT sc.SceneCharacterID, sc.SceneID, sc.CharacterID, c.ProjectID, c.CharacterName, c.ShortName, c.BirthDate, c.AgeAtSeriesStart,
sc.RoleInSceneTypeID, role.TypeName AS RoleInSceneTypeName, sc.PresenceTypeID, presence.TypeName AS PresenceTypeName,
sc.AppearanceNotes, sc.OutfitDescription, sc.PhysicalCondition, sc.EmotionalState, sc.KnowledgeNotes,
sc.CreatedDate, sc.UpdatedDate
FROM dbo.SceneCharacters sc
INNER JOIN dbo.Characters c ON c.CharacterID = sc.CharacterID
INNER JOIN dbo.Scenes s ON s.SceneID = sc.SceneID
INNER JOIN dbo.Chapters ch ON ch.ChapterID = s.ChapterID
INNER JOIN dbo.Books b ON b.BookID = ch.BookID
LEFT JOIN dbo.CharacterRoleInSceneTypes role ON role.CharacterRoleInSceneTypeID = sc.RoleInSceneTypeID
LEFT JOIN dbo.PresenceTypes presence ON presence.PresenceTypeID = sc.PresenceTypeID
WHERE c.ProjectID = @ProjectID AND c.IsArchived = 0 AND (@BookID IS NULL OR b.BookID = @BookID)
ORDER BY b.SortOrder, ch.SortOrder, s.SortOrder, c.CharacterName;
END;
GO
CREATE OR ALTER PROCEDURE dbo.AssetCustodyEvent_Save
@AssetCustodyEventID int = NULL,
@StoryAssetID int,
@SceneID int,
@AssetCustodyEventTypeID int,
@Description nvarchar(max) = NULL,
@CustodianNames nvarchar(max) = NULL,
@CustodianCharacterIds nvarchar(max) = NULL,
@CustodyRoleID int
AS
BEGIN
SET NOCOUNT ON;
IF @AssetCustodyEventID IS NULL OR @AssetCustodyEventID = 0
BEGIN
INSERT dbo.AssetCustodyEvents (StoryAssetID, SceneID, AssetCustodyEventTypeID, Description)
VALUES (@StoryAssetID, @SceneID, @AssetCustodyEventTypeID, @Description);
SET @AssetCustodyEventID = CAST(SCOPE_IDENTITY() AS int);
END
ELSE
BEGIN
UPDATE dbo.AssetCustodyEvents
SET StoryAssetID = @StoryAssetID, SceneID = @SceneID, AssetCustodyEventTypeID = @AssetCustodyEventTypeID,
Description = @Description, UpdatedDate = SYSUTCDATETIME()
WHERE AssetCustodyEventID = @AssetCustodyEventID;
END
DELETE FROM dbo.AssetCustodyEventCharacters WHERE AssetCustodyEventID = @AssetCustodyEventID;
INSERT dbo.AssetCustodyEventCharacters (AssetCustodyEventID, CharacterID, CharacterNameText, CustodyRoleID)
SELECT @AssetCustodyEventID, TRY_CAST(value AS int), NULL, @CustodyRoleID
FROM STRING_SPLIT(ISNULL(@CustodianCharacterIds, N''), N',')
WHERE TRY_CAST(value AS int) IS NOT NULL;
INSERT dbo.AssetCustodyEventCharacters (AssetCustodyEventID, CharacterNameText, CustodyRoleID)
SELECT @AssetCustodyEventID, LTRIM(RTRIM(value)), @CustodyRoleID
FROM STRING_SPLIT(REPLACE(ISNULL(@CustodianNames, N''), CHAR(10), N','), N',')
WHERE LTRIM(RTRIM(value)) <> N'';
SELECT @AssetCustodyEventID;
END;
GO
CREATE OR ALTER PROCEDURE dbo.AssetCustodyEvent_ListByScene
@SceneID int
AS
BEGIN
SET NOCOUNT ON;
SELECT ace.AssetCustodyEventID, ace.StoryAssetID, sa.AssetName, ace.SceneID,
ace.AssetCustodyEventTypeID, acet.TypeName AS CustodyEventTypeName, ace.Description,
STRING_AGG(CONCAT(COALESCE(c.CharacterName, aec.CharacterNameText), N' (', cr.RoleName, N')'), N', ') AS CustodianSummary,
ace.CreatedDate, ace.UpdatedDate
FROM dbo.AssetCustodyEvents ace
INNER JOIN dbo.StoryAssets sa ON sa.StoryAssetID = ace.StoryAssetID
INNER JOIN dbo.AssetCustodyEventTypes acet ON acet.AssetCustodyEventTypeID = ace.AssetCustodyEventTypeID
LEFT JOIN dbo.AssetCustodyEventCharacters aec ON aec.AssetCustodyEventID = ace.AssetCustodyEventID
LEFT JOIN dbo.Characters c ON c.CharacterID = aec.CharacterID
LEFT JOIN dbo.CustodyRoles cr ON cr.CustodyRoleID = aec.CustodyRoleID
WHERE ace.SceneID = @SceneID
GROUP BY ace.AssetCustodyEventID, ace.StoryAssetID, sa.AssetName, ace.SceneID, ace.AssetCustodyEventTypeID, acet.TypeName, ace.Description, ace.CreatedDate, ace.UpdatedDate
ORDER BY sa.AssetName, ace.AssetCustodyEventID;
END;
GO
CREATE OR ALTER PROCEDURE dbo.AssetCustodyEvent_ListByAsset
@StoryAssetID int
AS
BEGIN
SET NOCOUNT ON;
SELECT ace.AssetCustodyEventID, ace.StoryAssetID, sa.AssetName, ace.SceneID,
ace.AssetCustodyEventTypeID, acet.TypeName AS CustodyEventTypeName, ace.Description,
STRING_AGG(CONCAT(COALESCE(c.CharacterName, aec.CharacterNameText), N' (', cr.RoleName, N')'), N', ') AS CustodianSummary,
ace.CreatedDate, ace.UpdatedDate, b.BookTitle, ch.ChapterNumber, s.SceneNumber, s.SceneTitle
FROM dbo.AssetCustodyEvents ace
INNER JOIN dbo.StoryAssets sa ON sa.StoryAssetID = ace.StoryAssetID
INNER JOIN dbo.AssetCustodyEventTypes acet ON acet.AssetCustodyEventTypeID = ace.AssetCustodyEventTypeID
INNER JOIN dbo.Scenes s ON s.SceneID = ace.SceneID
INNER JOIN dbo.Chapters ch ON ch.ChapterID = s.ChapterID
INNER JOIN dbo.Books b ON b.BookID = ch.BookID
LEFT JOIN dbo.AssetCustodyEventCharacters aec ON aec.AssetCustodyEventID = ace.AssetCustodyEventID
LEFT JOIN dbo.Characters c ON c.CharacterID = aec.CharacterID
LEFT JOIN dbo.CustodyRoles cr ON cr.CustodyRoleID = aec.CustodyRoleID
WHERE ace.StoryAssetID = @StoryAssetID
GROUP BY ace.AssetCustodyEventID, ace.StoryAssetID, sa.AssetName, ace.SceneID, ace.AssetCustodyEventTypeID, acet.TypeName, ace.Description, ace.CreatedDate, ace.UpdatedDate, b.BookTitle, ch.ChapterNumber, s.SceneNumber, s.SceneTitle, b.SortOrder, ch.SortOrder, s.SortOrder
ORDER BY b.SortOrder, ch.SortOrder, s.SortOrder, ace.AssetCustodyEventID;
END;
GO

View File

@ -0,0 +1,732 @@
SET ANSI_NULLS ON;
GO
SET QUOTED_IDENTIFIER ON;
GO
IF OBJECT_ID(N'dbo.LocationTypes', N'U') IS NULL
BEGIN
CREATE TABLE dbo.LocationTypes
(
LocationTypeID int IDENTITY(1,1) NOT NULL CONSTRAINT PK_LocationTypes PRIMARY KEY,
TypeName nvarchar(100) NOT NULL,
SortOrder int NOT NULL,
IsActive bit NOT NULL CONSTRAINT DF_LocationTypes_IsActive DEFAULT 1,
CONSTRAINT UQ_LocationTypes_TypeName UNIQUE (TypeName)
);
END;
GO
IF OBJECT_ID(N'dbo.Locations', N'U') IS NULL
BEGIN
CREATE TABLE dbo.Locations
(
LocationID int IDENTITY(1,1) NOT NULL CONSTRAINT PK_Locations PRIMARY KEY,
ProjectID int NOT NULL,
ParentLocationID int NULL,
LocationName nvarchar(200) NOT NULL,
LocationTypeID int NULL,
Description nvarchar(max) NULL,
CreatedDate datetime2 NOT NULL CONSTRAINT DF_Locations_CreatedDate DEFAULT SYSUTCDATETIME(),
UpdatedDate datetime2 NOT NULL CONSTRAINT DF_Locations_UpdatedDate DEFAULT SYSUTCDATETIME(),
IsArchived bit NOT NULL CONSTRAINT DF_Locations_IsArchived DEFAULT 0,
CONSTRAINT FK_Locations_Projects FOREIGN KEY (ProjectID) REFERENCES dbo.Projects(ProjectID),
CONSTRAINT FK_Locations_Parent FOREIGN KEY (ParentLocationID) REFERENCES dbo.Locations(LocationID),
CONSTRAINT FK_Locations_Types FOREIGN KEY (LocationTypeID) REFERENCES dbo.LocationTypes(LocationTypeID)
);
END;
GO
IF OBJECT_ID(N'dbo.LocationRelationshipTypes', N'U') IS NULL
BEGIN
CREATE TABLE dbo.LocationRelationshipTypes
(
LocationRelationshipTypeID int IDENTITY(1,1) NOT NULL CONSTRAINT PK_LocationRelationshipTypes PRIMARY KEY,
TypeName nvarchar(100) NOT NULL,
SortOrder int NOT NULL,
IsActive bit NOT NULL CONSTRAINT DF_LocationRelationshipTypes_IsActive DEFAULT 1,
CONSTRAINT UQ_LocationRelationshipTypes_TypeName UNIQUE (TypeName)
);
END;
GO
IF OBJECT_ID(N'dbo.LocationRelationships', N'U') IS NULL
BEGIN
CREATE TABLE dbo.LocationRelationships
(
LocationRelationshipID int IDENTITY(1,1) NOT NULL CONSTRAINT PK_LocationRelationships PRIMARY KEY,
FromLocationID int NOT NULL,
ToLocationID int NOT NULL,
LocationRelationshipTypeID int NOT NULL,
IsBidirectional bit NOT NULL CONSTRAINT DF_LocationRelationships_IsBidirectional DEFAULT 0,
Strength int NULL,
CanHearNormalSpeech bit NOT NULL CONSTRAINT DF_LocationRelationships_CanHearNormalSpeech DEFAULT 0,
CanHearLoudNoise bit NOT NULL CONSTRAINT DF_LocationRelationships_CanHearLoudNoise DEFAULT 0,
CanSeeMovement bit NOT NULL CONSTRAINT DF_LocationRelationships_CanSeeMovement DEFAULT 0,
CanSeeClearly bit NOT NULL CONSTRAINT DF_LocationRelationships_CanSeeClearly DEFAULT 0,
CanTravelDirectly bit NOT NULL CONSTRAINT DF_LocationRelationships_CanTravelDirectly DEFAULT 0,
Notes nvarchar(max) NULL,
CreatedDate datetime2 NOT NULL CONSTRAINT DF_LocationRelationships_CreatedDate DEFAULT SYSUTCDATETIME(),
UpdatedDate datetime2 NOT NULL CONSTRAINT DF_LocationRelationships_UpdatedDate DEFAULT SYSUTCDATETIME(),
IsArchived bit NOT NULL CONSTRAINT DF_LocationRelationships_IsArchived DEFAULT 0,
CONSTRAINT FK_LocationRelationships_From FOREIGN KEY (FromLocationID) REFERENCES dbo.Locations(LocationID),
CONSTRAINT FK_LocationRelationships_To FOREIGN KEY (ToLocationID) REFERENCES dbo.Locations(LocationID),
CONSTRAINT FK_LocationRelationships_Types FOREIGN KEY (LocationRelationshipTypeID) REFERENCES dbo.LocationRelationshipTypes(LocationRelationshipTypeID)
);
END;
GO
IF OBJECT_ID(N'dbo.SceneAssetLocations', N'U') IS NULL
BEGIN
CREATE TABLE dbo.SceneAssetLocations
(
SceneAssetLocationID int IDENTITY(1,1) NOT NULL CONSTRAINT PK_SceneAssetLocations PRIMARY KEY,
SceneID int NOT NULL,
StoryAssetID int NOT NULL,
LocationID int NOT NULL,
Description nvarchar(max) NULL,
CreatedDate datetime2 NOT NULL CONSTRAINT DF_SceneAssetLocations_CreatedDate DEFAULT SYSUTCDATETIME(),
UpdatedDate datetime2 NOT NULL CONSTRAINT DF_SceneAssetLocations_UpdatedDate DEFAULT SYSUTCDATETIME(),
CONSTRAINT FK_SceneAssetLocations_Scenes FOREIGN KEY (SceneID) REFERENCES dbo.Scenes(SceneID),
CONSTRAINT FK_SceneAssetLocations_StoryAssets FOREIGN KEY (StoryAssetID) REFERENCES dbo.StoryAssets(StoryAssetID),
CONSTRAINT FK_SceneAssetLocations_Locations FOREIGN KEY (LocationID) REFERENCES dbo.Locations(LocationID)
);
END;
GO
MERGE dbo.LocationTypes AS target
USING (VALUES
(N'Country', 10), (N'Region', 20), (N'Town', 30), (N'Village', 40), (N'Street', 50),
(N'Building', 60), (N'Floor', 70), (N'Room', 80), (N'Outdoor Area', 90), (N'Vehicle Interior', 100),
(N'Landmark', 110), (N'Other', 120)
) AS source (TypeName, SortOrder)
ON target.TypeName = source.TypeName
WHEN NOT MATCHED THEN INSERT (TypeName, SortOrder) VALUES (source.TypeName, source.SortOrder);
GO
MERGE dbo.LocationRelationshipTypes AS target
USING (VALUES
(N'Adjacent To', 10), (N'Above', 20), (N'Below', 30), (N'Connected To', 40), (N'Visible From', 50),
(N'Audible From', 60), (N'Near', 70), (N'Leads To', 80), (N'Overlooks', 90),
(N'Same Building As', 100), (N'Same Area As', 110), (N'Secret Passage', 120), (N'Road Connection', 130)
) AS source (TypeName, SortOrder)
ON target.TypeName = source.TypeName
WHEN NOT MATCHED THEN INSERT (TypeName, SortOrder) VALUES (source.TypeName, source.SortOrder);
GO
CREATE OR ALTER VIEW dbo.LocationPaths
AS
WITH LocationTree AS
(
SELECT LocationID, ProjectID, ParentLocationID, LocationName, LocationTypeID, Description,
CAST(LocationName AS nvarchar(max)) AS LocationPath,
CAST(RIGHT('000000' + CAST(LocationID AS varchar(6)), 6) AS nvarchar(max)) AS SortPath,
0 AS Depth
FROM dbo.Locations
WHERE ParentLocationID IS NULL AND IsArchived = 0
UNION ALL
SELECT child.LocationID, child.ProjectID, child.ParentLocationID, child.LocationName, child.LocationTypeID, child.Description,
CAST(parent.LocationPath + N' > ' + child.LocationName AS nvarchar(max)),
CAST(parent.SortPath + N'.' + RIGHT('000000' + CAST(child.LocationID AS varchar(6)), 6) AS nvarchar(max)),
parent.Depth + 1
FROM dbo.Locations child
INNER JOIN LocationTree parent ON parent.LocationID = child.ParentLocationID
WHERE child.IsArchived = 0
)
SELECT LocationID, ProjectID, ParentLocationID, LocationName, LocationTypeID, Description, LocationPath, SortPath, Depth
FROM LocationTree;
GO
CREATE OR ALTER PROCEDURE dbo.LocationLookup_All
AS
BEGIN
SET NOCOUNT ON;
SELECT LocationTypeID, TypeName, SortOrder, IsActive FROM dbo.LocationTypes WHERE IsActive = 1 ORDER BY SortOrder;
SELECT LocationRelationshipTypeID, TypeName, SortOrder, IsActive FROM dbo.LocationRelationshipTypes WHERE IsActive = 1 ORDER BY SortOrder;
END;
GO
CREATE OR ALTER PROCEDURE dbo.Location_ListByProject
@ProjectID int
AS
BEGIN
SET NOCOUNT ON;
SELECT lp.LocationID, lp.ProjectID, lp.ParentLocationID, lp.LocationName, lp.LocationTypeID, lt.TypeName AS LocationTypeName,
lp.Description, lp.LocationPath, lp.Depth, l.CreatedDate, l.UpdatedDate, l.IsArchived
FROM dbo.LocationPaths lp
INNER JOIN dbo.Locations l ON l.LocationID = lp.LocationID
LEFT JOIN dbo.LocationTypes lt ON lt.LocationTypeID = lp.LocationTypeID
WHERE lp.ProjectID = @ProjectID
ORDER BY lp.SortPath;
END;
GO
CREATE OR ALTER PROCEDURE dbo.LocationTree_GetByProject
@ProjectID int
AS
BEGIN
SET NOCOUNT ON;
EXEC dbo.Location_ListByProject @ProjectID;
END;
GO
CREATE OR ALTER PROCEDURE dbo.Location_Get
@LocationID int
AS
BEGIN
SET NOCOUNT ON;
SELECT lp.LocationID, lp.ProjectID, lp.ParentLocationID, parent.LocationName AS ParentLocationName,
lp.LocationName, lp.LocationTypeID, lt.TypeName AS LocationTypeName, lp.Description, lp.LocationPath, lp.Depth,
l.CreatedDate, l.UpdatedDate, l.IsArchived
FROM dbo.LocationPaths lp
INNER JOIN dbo.Locations l ON l.LocationID = lp.LocationID
LEFT JOIN dbo.Locations parent ON parent.LocationID = lp.ParentLocationID
LEFT JOIN dbo.LocationTypes lt ON lt.LocationTypeID = lp.LocationTypeID
WHERE lp.LocationID = @LocationID;
END;
GO
CREATE OR ALTER PROCEDURE dbo.Location_Save
@LocationID int = NULL,
@ProjectID int,
@ParentLocationID int = NULL,
@LocationName nvarchar(200),
@LocationTypeID int = NULL,
@Description nvarchar(max) = NULL
AS
BEGIN
SET NOCOUNT ON;
IF @LocationID IS NULL OR @LocationID = 0
BEGIN
INSERT dbo.Locations (ProjectID, ParentLocationID, LocationName, LocationTypeID, Description)
VALUES (@ProjectID, @ParentLocationID, @LocationName, @LocationTypeID, @Description);
SET @LocationID = CAST(SCOPE_IDENTITY() AS int);
END
ELSE
BEGIN
UPDATE dbo.Locations
SET ParentLocationID = @ParentLocationID, LocationName = @LocationName, LocationTypeID = @LocationTypeID,
Description = @Description, UpdatedDate = SYSUTCDATETIME()
WHERE LocationID = @LocationID;
END
SELECT @LocationID;
END;
GO
CREATE OR ALTER PROCEDURE dbo.Location_Archive
@LocationID int
AS
BEGIN
SET NOCOUNT ON;
UPDATE dbo.Locations SET IsArchived = 1, UpdatedDate = SYSUTCDATETIME() WHERE LocationID = @LocationID;
END;
GO
CREATE OR ALTER PROCEDURE dbo.LocationRelationship_ListByLocation
@LocationID int
AS
BEGIN
SET NOCOUNT ON;
SELECT lr.LocationRelationshipID, lr.FromLocationID, fromLocation.LocationName AS FromLocationName,
lr.ToLocationID, toLocation.LocationName AS ToLocationName, lr.LocationRelationshipTypeID,
lrt.TypeName AS RelationshipTypeName, lr.IsBidirectional, lr.Strength,
lr.CanHearNormalSpeech, lr.CanHearLoudNoise, lr.CanSeeMovement, lr.CanSeeClearly, lr.CanTravelDirectly,
lr.Notes, lr.CreatedDate, lr.UpdatedDate, lr.IsArchived
FROM dbo.LocationRelationships lr
INNER JOIN dbo.Locations fromLocation ON fromLocation.LocationID = lr.FromLocationID
INNER JOIN dbo.Locations toLocation ON toLocation.LocationID = lr.ToLocationID
INNER JOIN dbo.LocationRelationshipTypes lrt ON lrt.LocationRelationshipTypeID = lr.LocationRelationshipTypeID
WHERE lr.IsArchived = 0 AND @LocationID IN (lr.FromLocationID, lr.ToLocationID)
ORDER BY lrt.SortOrder, fromLocation.LocationName, toLocation.LocationName;
END;
GO
CREATE OR ALTER PROCEDURE dbo.LocationRelationship_Save
@LocationRelationshipID int = NULL,
@FromLocationID int,
@ToLocationID int,
@LocationRelationshipTypeID int,
@IsBidirectional bit,
@Strength int = NULL,
@CanHearNormalSpeech bit,
@CanHearLoudNoise bit,
@CanSeeMovement bit,
@CanSeeClearly bit,
@CanTravelDirectly bit,
@Notes nvarchar(max) = NULL
AS
BEGIN
SET NOCOUNT ON;
IF @LocationRelationshipID IS NULL OR @LocationRelationshipID = 0
BEGIN
INSERT dbo.LocationRelationships (FromLocationID, ToLocationID, LocationRelationshipTypeID, IsBidirectional, Strength,
CanHearNormalSpeech, CanHearLoudNoise, CanSeeMovement, CanSeeClearly, CanTravelDirectly, Notes)
VALUES (@FromLocationID, @ToLocationID, @LocationRelationshipTypeID, @IsBidirectional, @Strength,
@CanHearNormalSpeech, @CanHearLoudNoise, @CanSeeMovement, @CanSeeClearly, @CanTravelDirectly, @Notes);
SET @LocationRelationshipID = CAST(SCOPE_IDENTITY() AS int);
END
ELSE
BEGIN
UPDATE dbo.LocationRelationships
SET FromLocationID = @FromLocationID, ToLocationID = @ToLocationID,
LocationRelationshipTypeID = @LocationRelationshipTypeID, IsBidirectional = @IsBidirectional,
Strength = @Strength, CanHearNormalSpeech = @CanHearNormalSpeech, CanHearLoudNoise = @CanHearLoudNoise,
CanSeeMovement = @CanSeeMovement, CanSeeClearly = @CanSeeClearly, CanTravelDirectly = @CanTravelDirectly,
Notes = @Notes, UpdatedDate = SYSUTCDATETIME()
WHERE LocationRelationshipID = @LocationRelationshipID;
END
SELECT @LocationRelationshipID;
END;
GO
CREATE OR ALTER PROCEDURE dbo.Scene_Save
@SceneID int = NULL, @ChapterID int, @SceneNumber decimal(10,2), @SceneTitle nvarchar(200), @Summary nvarchar(max) = NULL,
@TimeModeID int, @StartDateTime datetime2 = NULL, @EndDateTime datetime2 = NULL, @DurationAmount decimal(10,2) = NULL,
@DurationUnitID int = NULL, @RelativeTimeText nvarchar(200) = NULL, @TimeConfidenceID int,
@ScenePurposeNotes nvarchar(max) = NULL, @SceneOutcomeNotes nvarchar(max) = NULL, @RevisionStatusID int,
@PrimaryLocationID int = NULL
AS
BEGIN
SET NOCOUNT ON;
DECLARE @NextSortOrder int;
IF @SceneID IS NULL OR @SceneID = 0
BEGIN
SELECT @NextSortOrder = ISNULL(MAX(SortOrder), 0) + 10 FROM dbo.Scenes WHERE ChapterID = @ChapterID;
INSERT dbo.Scenes (ChapterID, SceneNumber, SceneTitle, Summary, SortOrder, TimeModeID, StartDateTime, EndDateTime,
DurationAmount, DurationUnitID, RelativeTimeText, TimeConfidenceID, ScenePurposeNotes, SceneOutcomeNotes, RevisionStatusID, PrimaryLocationID)
VALUES (@ChapterID, @SceneNumber, @SceneTitle, @Summary, @NextSortOrder, @TimeModeID, @StartDateTime, @EndDateTime,
@DurationAmount, @DurationUnitID, @RelativeTimeText, @TimeConfidenceID, @ScenePurposeNotes, @SceneOutcomeNotes, @RevisionStatusID, @PrimaryLocationID);
SET @SceneID = CAST(SCOPE_IDENTITY() AS int);
END
ELSE
BEGIN
UPDATE dbo.Scenes
SET SceneNumber = @SceneNumber, SceneTitle = @SceneTitle, Summary = @Summary, TimeModeID = @TimeModeID,
StartDateTime = @StartDateTime, EndDateTime = @EndDateTime, DurationAmount = @DurationAmount, DurationUnitID = @DurationUnitID,
RelativeTimeText = @RelativeTimeText, TimeConfidenceID = @TimeConfidenceID, ScenePurposeNotes = @ScenePurposeNotes,
SceneOutcomeNotes = @SceneOutcomeNotes, RevisionStatusID = @RevisionStatusID, PrimaryLocationID = @PrimaryLocationID,
UpdatedDate = SYSUTCDATETIME()
WHERE SceneID = @SceneID;
END
SELECT @SceneID;
END;
GO
CREATE OR ALTER PROCEDURE dbo.SceneLocation_SavePrimary
@SceneID int,
@PrimaryLocationID int = NULL
AS
BEGIN
SET NOCOUNT ON;
UPDATE dbo.Scenes
SET PrimaryLocationID = @PrimaryLocationID, UpdatedDate = SYSUTCDATETIME()
WHERE SceneID = @SceneID;
END;
GO
CREATE OR ALTER PROCEDURE dbo.Scene_Get
@SceneID int
AS
BEGIN
SET NOCOUNT ON;
EXEC dbo.SceneMetric_EnsureDefaults @SceneID;
SELECT s.SceneID, s.ChapterID, s.SceneNumber, s.SceneTitle, s.Summary, s.POVCharacterID, s.PrimaryLocationID,
location.LocationName AS PrimaryLocationName, location.LocationPath AS PrimaryLocationPath,
s.SortOrder, s.TimeModeID, tm.TimeModeName, s.StartDateTime, s.EndDateTime, s.DurationAmount, s.DurationUnitID,
du.DurationUnitName, s.RelativeTimeText, s.TimeConfidenceID, tc.TimeConfidenceName, s.ScenePurposeNotes,
s.SceneOutcomeNotes, s.RevisionStatusID, rs.StatusName AS RevisionStatusName, s.CreatedDate, s.UpdatedDate, s.IsArchived
FROM dbo.Scenes s
INNER JOIN dbo.TimeModes tm ON tm.TimeModeID = s.TimeModeID
INNER JOIN dbo.TimeConfidences tc ON tc.TimeConfidenceID = s.TimeConfidenceID
INNER JOIN dbo.RevisionStatuses rs ON rs.RevisionStatusID = s.RevisionStatusID
LEFT JOIN dbo.DurationUnits du ON du.DurationUnitID = s.DurationUnitID
LEFT JOIN dbo.LocationPaths location ON location.LocationID = s.PrimaryLocationID
WHERE s.SceneID = @SceneID AND s.IsArchived = 0;
SELECT ScenePurposeTypeID FROM dbo.ScenePurposes WHERE SceneID = @SceneID;
EXEC dbo.SceneMetric_ListByScene @SceneID;
END;
GO
CREATE OR ALTER PROCEDURE dbo.SceneCharacter_Save
@SceneCharacterID int = NULL, @SceneID int, @CharacterID int, @RoleInSceneTypeID int = NULL, @PresenceTypeID int = NULL,
@LocationID int = NULL, @EntryLocationID int = NULL, @ExitLocationID int = NULL,
@AppearanceNotes nvarchar(max) = NULL, @OutfitDescription nvarchar(max) = NULL, @PhysicalCondition nvarchar(max) = NULL,
@EmotionalState nvarchar(max) = NULL, @KnowledgeNotes nvarchar(max) = NULL
AS
BEGIN
SET NOCOUNT ON;
IF @SceneCharacterID IS NULL OR @SceneCharacterID = 0
SELECT @SceneCharacterID = SceneCharacterID FROM dbo.SceneCharacters WHERE SceneID = @SceneID AND CharacterID = @CharacterID;
IF @SceneCharacterID IS NULL OR @SceneCharacterID = 0
BEGIN
INSERT dbo.SceneCharacters (SceneID, CharacterID, RoleInSceneTypeID, PresenceTypeID, LocationID, EntryLocationID, ExitLocationID,
AppearanceNotes, OutfitDescription, PhysicalCondition, EmotionalState, KnowledgeNotes)
VALUES (@SceneID, @CharacterID, @RoleInSceneTypeID, @PresenceTypeID, @LocationID, @EntryLocationID, @ExitLocationID,
@AppearanceNotes, @OutfitDescription, @PhysicalCondition, @EmotionalState, @KnowledgeNotes);
SET @SceneCharacterID = CAST(SCOPE_IDENTITY() AS int);
END
ELSE
BEGIN
UPDATE dbo.SceneCharacters
SET CharacterID = @CharacterID, RoleInSceneTypeID = @RoleInSceneTypeID, PresenceTypeID = @PresenceTypeID,
LocationID = @LocationID, EntryLocationID = @EntryLocationID, ExitLocationID = @ExitLocationID,
AppearanceNotes = @AppearanceNotes, OutfitDescription = @OutfitDescription, PhysicalCondition = @PhysicalCondition,
EmotionalState = @EmotionalState, KnowledgeNotes = @KnowledgeNotes, UpdatedDate = SYSUTCDATETIME()
WHERE SceneCharacterID = @SceneCharacterID;
END
SELECT @SceneCharacterID;
END;
GO
CREATE OR ALTER PROCEDURE dbo.SceneCharacter_ListByScene
@SceneID int
AS
BEGIN
SET NOCOUNT ON;
SELECT sc.SceneCharacterID, sc.SceneID, sc.CharacterID, c.ProjectID, c.CharacterName, c.ShortName, c.BirthDate, c.AgeAtSeriesStart,
sc.RoleInSceneTypeID, role.TypeName AS RoleInSceneTypeName, sc.PresenceTypeID, presence.TypeName AS PresenceTypeName,
sc.LocationID, location.LocationName, location.LocationPath, sc.EntryLocationID, entry.LocationName AS EntryLocationName,
sc.ExitLocationID, exitLocation.LocationName AS ExitLocationName, sc.AppearanceNotes, sc.OutfitDescription,
sc.PhysicalCondition, sc.EmotionalState, sc.KnowledgeNotes, sc.CreatedDate, sc.UpdatedDate
FROM dbo.SceneCharacters sc
INNER JOIN dbo.Characters c ON c.CharacterID = sc.CharacterID
LEFT JOIN dbo.CharacterRoleInSceneTypes role ON role.CharacterRoleInSceneTypeID = sc.RoleInSceneTypeID
LEFT JOIN dbo.PresenceTypes presence ON presence.PresenceTypeID = sc.PresenceTypeID
LEFT JOIN dbo.LocationPaths location ON location.LocationID = sc.LocationID
LEFT JOIN dbo.LocationPaths entry ON entry.LocationID = sc.EntryLocationID
LEFT JOIN dbo.LocationPaths exitLocation ON exitLocation.LocationID = sc.ExitLocationID
WHERE sc.SceneID = @SceneID AND c.IsArchived = 0
ORDER BY role.SortOrder, c.CharacterName;
END;
GO
CREATE OR ALTER PROCEDURE dbo.StoryAsset_Save
@StoryAssetID int = NULL, @ProjectID int, @AssetName nvarchar(200), @AssetKindID int, @Description nvarchar(max) = NULL,
@Importance int, @CurrentStateID int = NULL, @CurrentLocationID int = NULL, @IsResolved bit
AS
BEGIN
SET NOCOUNT ON;
IF @StoryAssetID IS NULL OR @StoryAssetID = 0
BEGIN
INSERT dbo.StoryAssets (ProjectID, AssetName, AssetKindID, Description, Importance, CurrentStateID, CurrentLocationID, IsResolved)
VALUES (@ProjectID, @AssetName, @AssetKindID, @Description, @Importance, @CurrentStateID, @CurrentLocationID, @IsResolved);
SET @StoryAssetID = CAST(SCOPE_IDENTITY() AS int);
END
ELSE
BEGIN
UPDATE dbo.StoryAssets
SET AssetName = @AssetName, AssetKindID = @AssetKindID, Description = @Description, Importance = @Importance,
CurrentStateID = @CurrentStateID, CurrentLocationID = @CurrentLocationID, IsResolved = @IsResolved, UpdatedDate = SYSUTCDATETIME()
WHERE StoryAssetID = @StoryAssetID;
END
SELECT @StoryAssetID;
END;
GO
CREATE OR ALTER PROCEDURE dbo.SceneAssetLocation_Save
@SceneAssetLocationID int = NULL,
@SceneID int,
@StoryAssetID int,
@LocationID int,
@Description nvarchar(max) = NULL,
@UpdateCurrentLocation bit = 1
AS
BEGIN
SET NOCOUNT ON;
IF @SceneAssetLocationID IS NULL OR @SceneAssetLocationID = 0
SELECT @SceneAssetLocationID = SceneAssetLocationID FROM dbo.SceneAssetLocations WHERE SceneID = @SceneID AND StoryAssetID = @StoryAssetID;
IF @SceneAssetLocationID IS NULL OR @SceneAssetLocationID = 0
BEGIN
INSERT dbo.SceneAssetLocations (SceneID, StoryAssetID, LocationID, Description)
VALUES (@SceneID, @StoryAssetID, @LocationID, @Description);
SET @SceneAssetLocationID = CAST(SCOPE_IDENTITY() AS int);
END
ELSE
BEGIN
UPDATE dbo.SceneAssetLocations SET LocationID = @LocationID, Description = @Description, UpdatedDate = SYSUTCDATETIME()
WHERE SceneAssetLocationID = @SceneAssetLocationID;
END
IF @UpdateCurrentLocation = 1
UPDATE dbo.StoryAssets SET CurrentLocationID = @LocationID, UpdatedDate = SYSUTCDATETIME() WHERE StoryAssetID = @StoryAssetID;
SELECT @SceneAssetLocationID;
END;
GO
CREATE OR ALTER PROCEDURE dbo.SceneAssetLocation_ListByScene
@SceneID int
AS
BEGIN
SET NOCOUNT ON;
SELECT sal.SceneAssetLocationID, sal.SceneID, sal.StoryAssetID, sa.AssetName, sal.LocationID,
location.LocationName, location.LocationPath, sal.Description, sal.CreatedDate, sal.UpdatedDate
FROM dbo.SceneAssetLocations sal
INNER JOIN dbo.StoryAssets sa ON sa.StoryAssetID = sal.StoryAssetID
INNER JOIN dbo.LocationPaths location ON location.LocationID = sal.LocationID
WHERE sal.SceneID = @SceneID
ORDER BY location.LocationPath, sa.AssetName;
END;
GO
CREATE OR ALTER PROCEDURE dbo.Timeline_GetByProject
@ProjectID int,
@BookID int = NULL
AS
BEGIN
SET NOCOUNT ON;
SELECT ProjectID, ProjectName, Description, CreatedDate, UpdatedDate, IsArchived FROM dbo.Projects WHERE ProjectID = @ProjectID AND IsArchived = 0;
SELECT BookID, ProjectID, BookTitle, BookNumber, Description, SortOrder, CreatedDate, UpdatedDate, IsArchived
FROM dbo.Books WHERE ProjectID = @ProjectID AND IsArchived = 0 AND (@BookID IS NULL OR BookID = @BookID) ORDER BY SortOrder, BookNumber;
SELECT c.ChapterID, c.BookID, c.ChapterNumber, c.ChapterTitle, c.POVCharacterID, c.Summary, c.SortOrder,
c.RevisionStatusID, rs.StatusName AS RevisionStatusName, c.CreatedDate, c.UpdatedDate, c.IsArchived
FROM dbo.Chapters c INNER JOIN dbo.Books b ON b.BookID = c.BookID INNER JOIN dbo.RevisionStatuses rs ON rs.RevisionStatusID = c.RevisionStatusID
WHERE b.ProjectID = @ProjectID AND c.IsArchived = 0 AND b.IsArchived = 0 AND (@BookID IS NULL OR b.BookID = @BookID)
ORDER BY b.SortOrder, c.SortOrder;
SELECT s.SceneID, s.ChapterID, s.SceneNumber, s.SceneTitle, s.Summary, s.POVCharacterID, s.PrimaryLocationID,
location.LocationName AS PrimaryLocationName, location.LocationPath AS PrimaryLocationPath,
s.SortOrder, s.TimeModeID, tm.TimeModeName, s.StartDateTime, s.EndDateTime, s.DurationAmount, s.DurationUnitID,
du.DurationUnitName, s.RelativeTimeText, s.TimeConfidenceID, tc.TimeConfidenceName, s.ScenePurposeNotes,
s.SceneOutcomeNotes, s.RevisionStatusID, rs.StatusName AS RevisionStatusName, s.CreatedDate, s.UpdatedDate, s.IsArchived
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.TimeModes tm ON tm.TimeModeID = s.TimeModeID
INNER JOIN dbo.TimeConfidences tc ON tc.TimeConfidenceID = s.TimeConfidenceID
INNER JOIN dbo.RevisionStatuses rs ON rs.RevisionStatusID = s.RevisionStatusID
LEFT JOIN dbo.DurationUnits du ON du.DurationUnitID = s.DurationUnitID
LEFT JOIN dbo.LocationPaths location ON location.LocationID = s.PrimaryLocationID
WHERE b.ProjectID = @ProjectID AND s.IsArchived = 0 AND c.IsArchived = 0 AND b.IsArchived = 0 AND (@BookID IS NULL OR b.BookID = @BookID)
ORDER BY b.SortOrder, c.SortOrder, s.SortOrder;
SELECT sp.SceneID, spt.ScenePurposeTypeID, spt.PurposeName, spt.SortOrder
FROM dbo.ScenePurposes sp INNER JOIN dbo.ScenePurposeTypes spt ON spt.ScenePurposeTypeID = sp.ScenePurposeTypeID
INNER JOIN dbo.Scenes s ON s.SceneID = sp.SceneID INNER JOIN dbo.Chapters c ON c.ChapterID = s.ChapterID INNER JOIN dbo.Books b ON b.BookID = c.BookID
WHERE b.ProjectID = @ProjectID AND (@BookID IS NULL OR b.BookID = @BookID)
ORDER BY spt.SortOrder;
SELECT MetricTypeID, ProjectID, MetricName, Description, MinValue, MaxValue, DefaultValue, SortOrder, IsActive FROM dbo.SceneMetricTypes WHERE IsActive = 1 AND (ProjectID IS NULL OR ProjectID = @ProjectID) ORDER BY SortOrder;
SELECT smv.SceneID, smv.MetricTypeID, smt.MetricName, smv.Value, smv.Notes
FROM dbo.SceneMetricValues smv INNER JOIN dbo.SceneMetricTypes smt ON smt.MetricTypeID = smv.MetricTypeID
INNER JOIN dbo.Scenes s ON s.SceneID = smv.SceneID INNER JOIN dbo.Chapters c ON c.ChapterID = s.ChapterID INNER JOIN dbo.Books b ON b.BookID = c.BookID
WHERE b.ProjectID = @ProjectID AND (@BookID IS NULL OR b.BookID = @BookID);
SELECT pl.PlotLineID, pl.ProjectID, pl.BookID, b.BookTitle, pl.PlotLineName, pl.PlotLineTypeID, plt.TypeName AS PlotLineTypeName,
pl.Description, pl.ParentPlotLineID, parent.PlotLineName AS ParentPlotLineName, pl.SortOrder, pl.Colour, pl.IsMainPlot, pl.IsVisibleOnTimeline,
pl.CreatedDate, pl.UpdatedDate, pl.IsArchived
FROM dbo.PlotLines pl INNER JOIN dbo.PlotLineTypes plt ON plt.PlotLineTypeID = pl.PlotLineTypeID LEFT JOIN dbo.Books b ON b.BookID = pl.BookID LEFT JOIN dbo.PlotLines parent ON parent.PlotLineID = pl.ParentPlotLineID
WHERE pl.ProjectID = @ProjectID AND pl.IsArchived = 0 AND pl.IsVisibleOnTimeline = 1 AND (@BookID IS NULL OR pl.BookID IS NULL OR pl.BookID = @BookID)
ORDER BY pl.SortOrder, pl.PlotLineName;
SELECT te.ThreadEventID, te.PlotThreadID, pt.ThreadTitle, pt.PlotLineID, pl.PlotLineName, pl.Colour, te.SceneID, te.EventTypeID,
tet.TypeName AS EventTypeName, tet.MarkerText, te.EventTitle, te.EventDescription, te.CreatedDate, te.UpdatedDate
FROM dbo.ThreadEvents te INNER JOIN dbo.PlotThreads pt ON pt.PlotThreadID = te.PlotThreadID INNER JOIN dbo.PlotLines pl ON pl.PlotLineID = pt.PlotLineID
INNER JOIN dbo.ThreadEventTypes tet ON tet.ThreadEventTypeID = te.EventTypeID INNER JOIN dbo.Scenes s ON s.SceneID = te.SceneID
INNER JOIN dbo.Chapters c ON c.ChapterID = s.ChapterID INNER JOIN dbo.Books b ON b.BookID = c.BookID
WHERE b.ProjectID = @ProjectID AND pl.IsArchived = 0 AND pt.IsArchived = 0 AND (@BookID IS NULL OR b.BookID = @BookID)
ORDER BY pl.SortOrder, pt.ThreadTitle, b.SortOrder, c.SortOrder, s.SortOrder;
END;
GO
CREATE OR ALTER PROCEDURE dbo.LocationRelationship_ListByProject
@ProjectID int
AS
BEGIN
SET NOCOUNT ON;
SELECT lr.LocationRelationshipID, lr.FromLocationID, fromLocation.LocationName AS FromLocationName,
lr.ToLocationID, toLocation.LocationName AS ToLocationName, lr.LocationRelationshipTypeID,
lrt.TypeName AS RelationshipTypeName, lr.IsBidirectional, lr.Strength,
lr.CanHearNormalSpeech, lr.CanHearLoudNoise, lr.CanSeeMovement, lr.CanSeeClearly, lr.CanTravelDirectly,
lr.Notes, lr.CreatedDate, lr.UpdatedDate, lr.IsArchived
FROM dbo.LocationRelationships lr
INNER JOIN dbo.Locations fromLocation ON fromLocation.LocationID = lr.FromLocationID
INNER JOIN dbo.Locations toLocation ON toLocation.LocationID = lr.ToLocationID
INNER JOIN dbo.LocationRelationshipTypes lrt ON lrt.LocationRelationshipTypeID = lr.LocationRelationshipTypeID
WHERE lr.IsArchived = 0 AND fromLocation.ProjectID = @ProjectID
ORDER BY lrt.SortOrder, fromLocation.LocationName, toLocation.LocationName;
END;
GO
CREATE OR ALTER PROCEDURE dbo.LocationRelationship_Archive
@LocationRelationshipID int
AS
BEGIN
SET NOCOUNT ON;
UPDATE dbo.LocationRelationships
SET IsArchived = 1, UpdatedDate = SYSUTCDATETIME()
WHERE LocationRelationshipID = @LocationRelationshipID;
END;
GO
CREATE OR ALTER PROCEDURE dbo.SceneAssetLocation_Delete
@SceneAssetLocationID int
AS
BEGIN
SET NOCOUNT ON;
DELETE dbo.SceneAssetLocations WHERE SceneAssetLocationID = @SceneAssetLocationID;
END;
GO
CREATE OR ALTER PROCEDURE dbo.SceneAssetLocation_ListByLocation
@LocationID int
AS
BEGIN
SET NOCOUNT ON;
SELECT sal.SceneAssetLocationID, sal.SceneID, sal.StoryAssetID, sa.AssetName, sal.LocationID,
location.LocationName, location.LocationPath, sal.Description, sal.CreatedDate, sal.UpdatedDate
FROM dbo.SceneAssetLocations sal
INNER JOIN dbo.StoryAssets sa ON sa.StoryAssetID = sal.StoryAssetID
INNER JOIN dbo.LocationPaths location ON location.LocationID = sal.LocationID
WHERE sal.LocationID = @LocationID
ORDER BY sa.AssetName;
END;
GO
CREATE OR ALTER PROCEDURE dbo.LocationScene_ListByLocation
@LocationID int
AS
BEGIN
SET NOCOUNT ON;
SELECT s.SceneID, s.ChapterID, s.SceneNumber, s.SceneTitle, s.Summary, s.POVCharacterID, s.PrimaryLocationID,
location.LocationName AS PrimaryLocationName, location.LocationPath AS PrimaryLocationPath,
s.SortOrder, s.TimeModeID, tm.TimeModeName, s.StartDateTime, s.EndDateTime, s.DurationAmount, s.DurationUnitID,
du.DurationUnitName, s.RelativeTimeText, s.TimeConfidenceID, tc.TimeConfidenceName, s.ScenePurposeNotes,
s.SceneOutcomeNotes, s.RevisionStatusID, rs.StatusName AS RevisionStatusName, s.CreatedDate, s.UpdatedDate, s.IsArchived
FROM dbo.Scenes s
INNER JOIN dbo.TimeModes tm ON tm.TimeModeID = s.TimeModeID
INNER JOIN dbo.TimeConfidences tc ON tc.TimeConfidenceID = s.TimeConfidenceID
INNER JOIN dbo.RevisionStatuses rs ON rs.RevisionStatusID = s.RevisionStatusID
LEFT JOIN dbo.DurationUnits du ON du.DurationUnitID = s.DurationUnitID
LEFT JOIN dbo.LocationPaths location ON location.LocationID = s.PrimaryLocationID
WHERE s.PrimaryLocationID = @LocationID AND s.IsArchived = 0
ORDER BY s.SortOrder;
END;
GO
CREATE OR ALTER PROCEDURE dbo.LocationCharacter_ListByLocation
@LocationID int
AS
BEGIN
SET NOCOUNT ON;
SELECT sc.SceneCharacterID, sc.SceneID, sc.CharacterID, c.ProjectID, c.CharacterName, c.ShortName, c.BirthDate, c.AgeAtSeriesStart,
sc.RoleInSceneTypeID, role.TypeName AS RoleInSceneTypeName, sc.PresenceTypeID, presence.TypeName AS PresenceTypeName,
sc.LocationID, location.LocationName, location.LocationPath, sc.EntryLocationID, entry.LocationName AS EntryLocationName,
sc.ExitLocationID, exitLocation.LocationName AS ExitLocationName, sc.AppearanceNotes, sc.OutfitDescription,
sc.PhysicalCondition, sc.EmotionalState, sc.KnowledgeNotes, s.SceneNumber, s.SceneTitle, ch.ChapterNumber, b.BookTitle,
sc.CreatedDate, sc.UpdatedDate
FROM dbo.SceneCharacters sc
INNER JOIN dbo.Characters c ON c.CharacterID = sc.CharacterID
INNER JOIN dbo.Scenes s ON s.SceneID = sc.SceneID
INNER JOIN dbo.Chapters ch ON ch.ChapterID = s.ChapterID
INNER JOIN dbo.Books b ON b.BookID = ch.BookID
LEFT JOIN dbo.CharacterRoleInSceneTypes role ON role.CharacterRoleInSceneTypeID = sc.RoleInSceneTypeID
LEFT JOIN dbo.PresenceTypes presence ON presence.PresenceTypeID = sc.PresenceTypeID
LEFT JOIN dbo.LocationPaths location ON location.LocationID = sc.LocationID
LEFT JOIN dbo.LocationPaths entry ON entry.LocationID = sc.EntryLocationID
LEFT JOIN dbo.LocationPaths exitLocation ON exitLocation.LocationID = sc.ExitLocationID
WHERE sc.LocationID = @LocationID AND c.IsArchived = 0
ORDER BY b.SortOrder, ch.SortOrder, s.SortOrder, c.CharacterName;
END;
GO
CREATE OR ALTER PROCEDURE dbo.LocationCharacter_ListByScene
@SceneID int,
@LocationID int = NULL
AS
BEGIN
SET NOCOUNT ON;
SELECT sc.SceneCharacterID, sc.SceneID, sc.CharacterID, c.ProjectID, c.CharacterName, c.ShortName, c.BirthDate, c.AgeAtSeriesStart,
sc.RoleInSceneTypeID, role.TypeName AS RoleInSceneTypeName, sc.PresenceTypeID, presence.TypeName AS PresenceTypeName,
sc.LocationID, location.LocationName, location.LocationPath, sc.EntryLocationID, entry.LocationName AS EntryLocationName,
sc.ExitLocationID, exitLocation.LocationName AS ExitLocationName, sc.AppearanceNotes, sc.OutfitDescription,
sc.PhysicalCondition, sc.EmotionalState, sc.KnowledgeNotes, sc.CreatedDate, sc.UpdatedDate
FROM dbo.SceneCharacters sc
INNER JOIN dbo.Characters c ON c.CharacterID = sc.CharacterID
LEFT JOIN dbo.CharacterRoleInSceneTypes role ON role.CharacterRoleInSceneTypeID = sc.RoleInSceneTypeID
LEFT JOIN dbo.PresenceTypes presence ON presence.PresenceTypeID = sc.PresenceTypeID
LEFT JOIN dbo.LocationPaths location ON location.LocationID = sc.LocationID
LEFT JOIN dbo.LocationPaths entry ON entry.LocationID = sc.EntryLocationID
LEFT JOIN dbo.LocationPaths exitLocation ON exitLocation.LocationID = sc.ExitLocationID
WHERE sc.SceneID = @SceneID AND (@LocationID IS NULL OR sc.LocationID = @LocationID) AND c.IsArchived = 0
ORDER BY role.SortOrder, c.CharacterName;
END;
GO
CREATE OR ALTER PROCEDURE dbo.SceneAssetLocation_ListBySceneLocation
@SceneID int,
@LocationID int = NULL
AS
BEGIN
SET NOCOUNT ON;
SELECT sal.SceneAssetLocationID, sal.SceneID, sal.StoryAssetID, sa.AssetName, sal.LocationID,
location.LocationName, location.LocationPath, sal.Description, sal.CreatedDate, sal.UpdatedDate
FROM dbo.SceneAssetLocations sal
INNER JOIN dbo.StoryAssets sa ON sa.StoryAssetID = sal.StoryAssetID
INNER JOIN dbo.LocationPaths location ON location.LocationID = sal.LocationID
WHERE sal.SceneID = @SceneID AND (@LocationID IS NULL OR sal.LocationID = @LocationID)
ORDER BY location.LocationPath, sa.AssetName;
END;
GO
CREATE OR ALTER PROCEDURE dbo.StoryAsset_ListByProject
@ProjectID int
AS
BEGIN
SET NOCOUNT ON;
SELECT sa.StoryAssetID, sa.ProjectID, sa.AssetName, sa.AssetKindID, ak.KindName, sa.Description, sa.Importance,
sa.CurrentStateID, ast.StateName AS CurrentStateName, sa.CurrentLocationID,
location.LocationName AS CurrentLocationName, location.LocationPath AS CurrentLocationPath,
sa.IsResolved, sa.CreatedDate, sa.UpdatedDate, sa.IsArchived
FROM dbo.StoryAssets sa
INNER JOIN dbo.AssetKinds ak ON ak.AssetKindID = sa.AssetKindID
LEFT JOIN dbo.AssetStates ast ON ast.AssetStateID = sa.CurrentStateID
LEFT JOIN dbo.LocationPaths location ON location.LocationID = sa.CurrentLocationID
WHERE sa.ProjectID = @ProjectID AND sa.IsArchived = 0
ORDER BY sa.Importance DESC, sa.AssetName;
END;
GO
CREATE OR ALTER PROCEDURE dbo.StoryAsset_Get
@StoryAssetID int
AS
BEGIN
SET NOCOUNT ON;
SELECT sa.StoryAssetID, sa.ProjectID, sa.AssetName, sa.AssetKindID, ak.KindName, sa.Description, sa.Importance,
sa.CurrentStateID, ast.StateName AS CurrentStateName, sa.CurrentLocationID,
location.LocationName AS CurrentLocationName, location.LocationPath AS CurrentLocationPath,
sa.IsResolved, sa.CreatedDate, sa.UpdatedDate, sa.IsArchived
FROM dbo.StoryAssets sa
INNER JOIN dbo.AssetKinds ak ON ak.AssetKindID = sa.AssetKindID
LEFT JOIN dbo.AssetStates ast ON ast.AssetStateID = sa.CurrentStateID
LEFT JOIN dbo.LocationPaths location ON location.LocationID = sa.CurrentLocationID
WHERE sa.StoryAssetID = @StoryAssetID AND sa.IsArchived = 0;
END;
GO
CREATE OR ALTER PROCEDURE dbo.AssetTimeline_GetByProject
@ProjectID int,
@BookID int = NULL
AS
BEGIN
SET NOCOUNT ON;
SELECT sa.StoryAssetID, sa.ProjectID, sa.AssetName, sa.AssetKindID, ak.KindName, sa.Description, sa.Importance,
sa.CurrentStateID, ast.StateName AS CurrentStateName, sa.CurrentLocationID,
location.LocationName AS CurrentLocationName, location.LocationPath AS CurrentLocationPath,
sa.IsResolved, sa.CreatedDate, sa.UpdatedDate, sa.IsArchived
FROM dbo.StoryAssets sa
INNER JOIN dbo.AssetKinds ak ON ak.AssetKindID = sa.AssetKindID
LEFT JOIN dbo.AssetStates ast ON ast.AssetStateID = sa.CurrentStateID
LEFT JOIN dbo.LocationPaths location ON location.LocationID = sa.CurrentLocationID
WHERE sa.ProjectID = @ProjectID AND sa.IsArchived = 0 AND (sa.Importance >= 5 OR sa.IsResolved = 0)
ORDER BY sa.Importance DESC, sa.AssetName;
SELECT ae.AssetEventID, ae.StoryAssetID, sa.ProjectID, sa.AssetName, sa.AssetKindID, ak.KindName,
ae.SceneID, ae.AssetEventTypeID, aet.TypeName AS AssetEventTypeName, aet.MarkerText,
ae.FromStateID, fs.StateName AS FromStateName, ae.ToStateID, ts.StateName AS ToStateName,
ae.EventTitle, ae.EventDescription, ae.CreatedDate, ae.UpdatedDate
FROM dbo.AssetEvents ae
INNER JOIN dbo.StoryAssets sa ON sa.StoryAssetID = ae.StoryAssetID
INNER JOIN dbo.AssetKinds ak ON ak.AssetKindID = sa.AssetKindID
INNER JOIN dbo.AssetEventTypes aet ON aet.AssetEventTypeID = ae.AssetEventTypeID
INNER JOIN dbo.Scenes s ON s.SceneID = ae.SceneID
INNER JOIN dbo.Chapters c ON c.ChapterID = s.ChapterID
INNER JOIN dbo.Books b ON b.BookID = c.BookID
LEFT JOIN dbo.AssetStates fs ON fs.AssetStateID = ae.FromStateID
LEFT JOIN dbo.AssetStates ts ON ts.AssetStateID = ae.ToStateID
WHERE sa.ProjectID = @ProjectID AND sa.IsArchived = 0 AND (@BookID IS NULL OR b.BookID = @BookID)
ORDER BY b.SortOrder, c.SortOrder, s.SortOrder, sa.AssetName;
END;
GO

View File

@ -0,0 +1,845 @@
SET ANSI_NULLS ON;
GO
SET QUOTED_IDENTIFIER ON;
GO
IF OBJECT_ID(N'dbo.WarningTypes', N'U') IS NULL
BEGIN
CREATE TABLE dbo.WarningTypes
(
WarningTypeID int IDENTITY(1,1) NOT NULL CONSTRAINT PK_WarningTypes PRIMARY KEY,
TypeName nvarchar(150) NOT NULL,
Description nvarchar(max) NULL,
SortOrder int NOT NULL,
IsActive bit NOT NULL CONSTRAINT DF_WarningTypes_IsActive DEFAULT 1,
CONSTRAINT UQ_WarningTypes_TypeName UNIQUE (TypeName)
);
END;
GO
IF OBJECT_ID(N'dbo.WarningSeverities', N'U') IS NULL
BEGIN
CREATE TABLE dbo.WarningSeverities
(
WarningSeverityID int IDENTITY(1,1) NOT NULL CONSTRAINT PK_WarningSeverities PRIMARY KEY,
SeverityName nvarchar(50) NOT NULL,
SortOrder int NOT NULL,
IsActive bit NOT NULL CONSTRAINT DF_WarningSeverities_IsActive DEFAULT 1,
CONSTRAINT UQ_WarningSeverities_SeverityName UNIQUE (SeverityName)
);
END;
GO
IF OBJECT_ID(N'dbo.ValidationRuns', N'U') IS NULL
BEGIN
CREATE TABLE dbo.ValidationRuns
(
ValidationRunID int IDENTITY(1,1) NOT NULL CONSTRAINT PK_ValidationRuns PRIMARY KEY,
ProjectID int NOT NULL,
BookID int NULL,
StartedDate datetime2 NOT NULL CONSTRAINT DF_ValidationRuns_StartedDate DEFAULT SYSUTCDATETIME(),
CompletedDate datetime2 NULL,
TriggeredBy nvarchar(100) NULL,
ScopeDescription nvarchar(200) NULL,
CONSTRAINT FK_ValidationRuns_Projects FOREIGN KEY (ProjectID) REFERENCES dbo.Projects(ProjectID),
CONSTRAINT FK_ValidationRuns_Books FOREIGN KEY (BookID) REFERENCES dbo.Books(BookID)
);
END;
GO
IF OBJECT_ID(N'dbo.ContinuityWarnings', N'U') IS NULL
BEGIN
CREATE TABLE dbo.ContinuityWarnings
(
ContinuityWarningID int IDENTITY(1,1) NOT NULL CONSTRAINT PK_ContinuityWarnings PRIMARY KEY,
ValidationRunID int NULL,
ProjectID int NOT NULL,
BookID int NULL,
ChapterID int NULL,
SceneID int NULL,
EntityType nvarchar(80) NOT NULL,
EntityID int NULL,
WarningTypeID int NOT NULL,
WarningSeverityID int NOT NULL,
Message nvarchar(500) NOT NULL,
Details nvarchar(max) NULL,
IsDismissed bit NOT NULL CONSTRAINT DF_ContinuityWarnings_IsDismissed DEFAULT 0,
IsIntentional bit NOT NULL CONSTRAINT DF_ContinuityWarnings_IsIntentional DEFAULT 0,
CreatedDate datetime2 NOT NULL CONSTRAINT DF_ContinuityWarnings_CreatedDate DEFAULT SYSUTCDATETIME(),
LastDetectedDate datetime2 NOT NULL CONSTRAINT DF_ContinuityWarnings_LastDetectedDate DEFAULT SYSUTCDATETIME(),
CONSTRAINT FK_ContinuityWarnings_ValidationRuns FOREIGN KEY (ValidationRunID) REFERENCES dbo.ValidationRuns(ValidationRunID),
CONSTRAINT FK_ContinuityWarnings_Projects FOREIGN KEY (ProjectID) REFERENCES dbo.Projects(ProjectID),
CONSTRAINT FK_ContinuityWarnings_Books FOREIGN KEY (BookID) REFERENCES dbo.Books(BookID),
CONSTRAINT FK_ContinuityWarnings_Chapters FOREIGN KEY (ChapterID) REFERENCES dbo.Chapters(ChapterID),
CONSTRAINT FK_ContinuityWarnings_Scenes FOREIGN KEY (SceneID) REFERENCES dbo.Scenes(SceneID),
CONSTRAINT FK_ContinuityWarnings_Types FOREIGN KEY (WarningTypeID) REFERENCES dbo.WarningTypes(WarningTypeID),
CONSTRAINT FK_ContinuityWarnings_Severities FOREIGN KEY (WarningSeverityID) REFERENCES dbo.WarningSeverities(WarningSeverityID)
);
END;
GO
IF OBJECT_ID(N'dbo.SceneDependencyTypes', N'U') IS NULL
BEGIN
CREATE TABLE dbo.SceneDependencyTypes
(
SceneDependencyTypeID int IDENTITY(1,1) NOT NULL CONSTRAINT PK_SceneDependencyTypes PRIMARY KEY,
TypeName nvarchar(100) NOT NULL,
SortOrder int NOT NULL,
IsActive bit NOT NULL CONSTRAINT DF_SceneDependencyTypes_IsActive DEFAULT 1,
CONSTRAINT UQ_SceneDependencyTypes_TypeName UNIQUE (TypeName)
);
END;
GO
IF OBJECT_ID(N'dbo.SceneDependencies', N'U') IS NULL
BEGIN
CREATE TABLE dbo.SceneDependencies
(
SceneDependencyID int IDENTITY(1,1) NOT NULL CONSTRAINT PK_SceneDependencies PRIMARY KEY,
SourceSceneID int NOT NULL,
TargetSceneID int NOT NULL,
SceneDependencyTypeID int NOT NULL,
Description nvarchar(max) NULL,
CreatedDate datetime2 NOT NULL CONSTRAINT DF_SceneDependencies_CreatedDate DEFAULT SYSUTCDATETIME(),
UpdatedDate datetime2 NOT NULL CONSTRAINT DF_SceneDependencies_UpdatedDate DEFAULT SYSUTCDATETIME(),
IsArchived bit NOT NULL CONSTRAINT DF_SceneDependencies_IsArchived DEFAULT 0,
CONSTRAINT FK_SceneDependencies_SourceScene FOREIGN KEY (SourceSceneID) REFERENCES dbo.Scenes(SceneID),
CONSTRAINT FK_SceneDependencies_TargetScene FOREIGN KEY (TargetSceneID) REFERENCES dbo.Scenes(SceneID),
CONSTRAINT FK_SceneDependencies_Types FOREIGN KEY (SceneDependencyTypeID) REFERENCES dbo.SceneDependencyTypes(SceneDependencyTypeID)
);
END;
GO
IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = N'IX_ContinuityWarnings_Project' AND object_id = OBJECT_ID(N'dbo.ContinuityWarnings'))
CREATE INDEX IX_ContinuityWarnings_Project ON dbo.ContinuityWarnings(ProjectID, BookID, SceneID, IsDismissed, IsIntentional, WarningSeverityID);
GO
MERGE dbo.WarningSeverities AS target
USING (VALUES (N'Info', 10), (N'Warning', 20), (N'Error', 30)) AS source (SeverityName, SortOrder)
ON target.SeverityName = source.SeverityName
WHEN NOT MATCHED THEN INSERT (SeverityName, SortOrder) VALUES (source.SeverityName, source.SortOrder);
GO
MERGE dbo.WarningTypes AS target
USING (VALUES
(N'Thread Resolved Before Introduced', N'Plot thread appears resolved before introduction.', 10),
(N'Question Answered Before Raised', N'Question event appears answered before raised.', 20),
(N'Clue Without Payoff', N'Clue has no payoff or resolution event.', 30),
(N'Thread Dormant', N'Open plot thread has gone quiet for many scenes.', 40),
(N'Asset Used Before Introduced', N'Asset appears used before it is introduced.', 50),
(N'Asset State Dependency Missing', N'Asset dependency may not have been satisfied.', 60),
(N'Asset Custody Conflict', N'Asset custody may not match use.', 70),
(N'Asset Location Conflict', N'Asset location changes without a movement event.', 80),
(N'Character Knowledge Conflict', N'Character knowledge may need a record.', 90),
(N'Scene Dependency Broken', N'Scene dependency order may be broken.', 100),
(N'Character Location Conflict', N'Character appears in conflicting scene locations.', 110),
(N'Scene Date Order Conflict', N'Narrative order and exact dates may conflict.', 120),
(N'Long Unexplained Time Gap', N'Long gap without time passage explanation.', 130),
(N'Sustained High Intensity', N'Metric remains high for many scenes.', 140),
(N'Sustained Low Intensity', N'Metric remains low for many scenes.', 150),
(N'Missing Scene Location', N'Scene has no primary location.', 160),
(N'Missing Scene Purpose', N'Scene has no purpose.', 170),
(N'Missing Scene Outcome', N'Scene has no outcome.', 180),
(N'Too Many Open Major Threads', N'Many major plot threads appear open at the same time.', 190)
) AS source (TypeName, Description, SortOrder)
ON target.TypeName = source.TypeName
WHEN NOT MATCHED THEN INSERT (TypeName, Description, SortOrder) VALUES (source.TypeName, source.Description, source.SortOrder);
GO
MERGE dbo.SceneDependencyTypes AS target
USING (VALUES
(N'Must Occur Before', 10), (N'Emotional Setup', 20), (N'Reveal Setup', 30), (N'Action Consequence', 40),
(N'Travel Dependency', 50), (N'Relationship Dependency', 60), (N'Knowledge Dependency', 70), (N'Object Dependency', 80)
) AS source (TypeName, SortOrder)
ON target.TypeName = source.TypeName
WHEN NOT MATCHED THEN INSERT (TypeName, SortOrder) VALUES (source.TypeName, source.SortOrder);
GO
CREATE OR ALTER PROCEDURE dbo.WarningLookup_All
AS
BEGIN
SET NOCOUNT ON;
SELECT WarningTypeID, TypeName, Description, SortOrder, IsActive FROM dbo.WarningTypes WHERE IsActive = 1 ORDER BY SortOrder;
SELECT WarningSeverityID, SeverityName, SortOrder, IsActive FROM dbo.WarningSeverities WHERE IsActive = 1 ORDER BY SortOrder;
END;
GO
CREATE OR ALTER PROCEDURE dbo.ValidationRun_Create
@ProjectID int,
@BookID int = NULL,
@TriggeredBy nvarchar(100) = NULL,
@ScopeDescription nvarchar(200) = NULL
AS
BEGIN
SET NOCOUNT ON;
INSERT dbo.ValidationRuns (ProjectID, BookID, TriggeredBy, ScopeDescription)
VALUES (@ProjectID, @BookID, @TriggeredBy, @ScopeDescription);
DECLARE @ValidationRunID int = CAST(SCOPE_IDENTITY() AS int);
RETURN @ValidationRunID;
END;
GO
CREATE OR ALTER PROCEDURE dbo.ValidationRun_Complete
@ValidationRunID int
AS
BEGIN
SET NOCOUNT ON;
UPDATE dbo.ValidationRuns SET CompletedDate = SYSUTCDATETIME() WHERE ValidationRunID = @ValidationRunID;
END;
GO
CREATE OR ALTER PROCEDURE dbo.ContinuityWarning_ClearActive
@ProjectID int,
@BookID int = NULL,
@SceneID int = NULL
AS
BEGIN
SET NOCOUNT ON;
DELETE cw
FROM dbo.ContinuityWarnings cw
WHERE cw.ProjectID = @ProjectID
AND (@BookID IS NULL OR cw.BookID = @BookID)
AND (@SceneID IS NULL OR cw.SceneID = @SceneID)
AND cw.IsDismissed = 0
AND cw.IsIntentional = 0;
END;
GO
CREATE OR ALTER PROCEDURE dbo.ContinuityWarning_Upsert
@ValidationRunID int = NULL,
@ProjectID int,
@BookID int = NULL,
@ChapterID int = NULL,
@SceneID int = NULL,
@EntityType nvarchar(80),
@EntityID int = NULL,
@WarningTypeName nvarchar(150),
@SeverityName nvarchar(50),
@Message nvarchar(500),
@Details nvarchar(max) = NULL
AS
BEGIN
SET NOCOUNT ON;
DECLARE @WarningTypeID int = (SELECT WarningTypeID FROM dbo.WarningTypes WHERE TypeName = @WarningTypeName);
DECLARE @WarningSeverityID int = (SELECT WarningSeverityID FROM dbo.WarningSeverities WHERE SeverityName = @SeverityName);
IF @WarningTypeID IS NULL OR @WarningSeverityID IS NULL
RETURN;
DECLARE @ExistingID int;
SELECT TOP (1) @ExistingID = ContinuityWarningID
FROM dbo.ContinuityWarnings
WHERE ProjectID = @ProjectID
AND ISNULL(BookID, 0) = ISNULL(@BookID, 0)
AND ISNULL(ChapterID, 0) = ISNULL(@ChapterID, 0)
AND ISNULL(SceneID, 0) = ISNULL(@SceneID, 0)
AND EntityType = @EntityType
AND ISNULL(EntityID, 0) = ISNULL(@EntityID, 0)
AND WarningTypeID = @WarningTypeID
AND IsDismissed = 0
AND IsIntentional = 0;
IF @ExistingID IS NULL
BEGIN
INSERT dbo.ContinuityWarnings (ValidationRunID, ProjectID, BookID, ChapterID, SceneID, EntityType, EntityID, WarningTypeID, WarningSeverityID, Message, Details)
VALUES (@ValidationRunID, @ProjectID, @BookID, @ChapterID, @SceneID, @EntityType, @EntityID, @WarningTypeID, @WarningSeverityID, @Message, @Details);
END
ELSE
BEGIN
UPDATE dbo.ContinuityWarnings
SET ValidationRunID = @ValidationRunID,
WarningSeverityID = @WarningSeverityID,
Message = @Message,
Details = @Details,
LastDetectedDate = SYSUTCDATETIME()
WHERE ContinuityWarningID = @ExistingID;
END
END;
GO
CREATE OR ALTER VIEW dbo.NarrativeSceneOrder
AS
SELECT p.ProjectID, b.BookID, c.ChapterID, s.SceneID,
b.SortOrder AS BookSortOrder, c.SortOrder AS ChapterSortOrder, s.SortOrder AS SceneSortOrder,
ROW_NUMBER() OVER (PARTITION BY p.ProjectID ORDER BY b.SortOrder, b.BookNumber, c.SortOrder, c.ChapterNumber, s.SortOrder, s.SceneNumber, s.SceneID) AS GlobalSceneIndex,
ROW_NUMBER() OVER (PARTITION BY b.BookID ORDER BY c.SortOrder, c.ChapterNumber, s.SortOrder, s.SceneNumber, s.SceneID) AS BookSceneIndex,
b.BookTitle, b.BookNumber, c.ChapterNumber, c.ChapterTitle, s.SceneNumber, s.SceneTitle,
s.PrimaryLocationID, s.StartDateTime, s.RelativeTimeText, s.ScenePurposeNotes, s.SceneOutcomeNotes
FROM dbo.Projects p
INNER JOIN dbo.Books b ON b.ProjectID = p.ProjectID AND b.IsArchived = 0
INNER JOIN dbo.Chapters c ON c.BookID = b.BookID AND c.IsArchived = 0
INNER JOIN dbo.Scenes s ON s.ChapterID = c.ChapterID AND s.IsArchived = 0
WHERE p.IsArchived = 0;
GO
CREATE OR ALTER PROCEDURE dbo.NarrativeSceneOrder_GetByProject
@ProjectID int,
@BookID int = NULL
AS
BEGIN
SET NOCOUNT ON;
SELECT * FROM dbo.NarrativeSceneOrder
WHERE ProjectID = @ProjectID AND (@BookID IS NULL OR BookID = @BookID)
ORDER BY GlobalSceneIndex;
END;
GO
CREATE OR ALTER PROCEDURE dbo.ContinuityValidation_ValidateProject
@ProjectID int,
@BookID int = NULL,
@SceneID int = NULL,
@TriggeredBy nvarchar(100) = NULL
AS
BEGIN
SET NOCOUNT ON;
DECLARE @ValidationRunID int;
DECLARE @Scope nvarchar(200) = CASE WHEN @SceneID IS NOT NULL THEN N'Scene validation' WHEN @BookID IS NOT NULL THEN N'Book validation' ELSE N'Project validation' END;
EXEC @ValidationRunID = dbo.ValidationRun_Create @ProjectID = @ProjectID, @BookID = @BookID, @TriggeredBy = @TriggeredBy, @ScopeDescription = @Scope;
EXEC dbo.ContinuityWarning_ClearActive @ProjectID = @ProjectID, @BookID = @BookID, @SceneID = @SceneID;
DECLARE @Warnings TABLE
(
BookID int NULL, ChapterID int NULL, SceneID int NULL, EntityType nvarchar(80), EntityID int NULL,
WarningTypeName nvarchar(150), SeverityName nvarchar(50), Message nvarchar(500), Details nvarchar(max) NULL
);
;WITH ScopeScenes AS
(
SELECT * FROM dbo.NarrativeSceneOrder
WHERE ProjectID = @ProjectID AND (@BookID IS NULL OR BookID = @BookID) AND (@SceneID IS NULL OR SceneID = @SceneID)
)
INSERT @Warnings
SELECT BookID, ChapterID, SceneID, N'Scene', SceneID, N'Missing Scene Location', N'Info',
N'Possible issue: this scene has no primary location yet.',
N'Add a primary location if the scene depends on where characters or assets are.'
FROM ScopeScenes WHERE PrimaryLocationID IS NULL;
;WITH ScopeScenes AS
(
SELECT nso.*
FROM dbo.NarrativeSceneOrder nso
WHERE nso.ProjectID = @ProjectID AND (@BookID IS NULL OR nso.BookID = @BookID) AND (@SceneID IS NULL OR nso.SceneID = @SceneID)
)
INSERT @Warnings
SELECT ss.BookID, ss.ChapterID, ss.SceneID, N'Scene', ss.SceneID, N'Missing Scene Purpose', N'Info',
N'Possible issue: this scene has no purpose label or purpose notes.',
N'Add a scene purpose if this is not intentional.'
FROM ScopeScenes ss
WHERE ISNULL(NULLIF(LTRIM(RTRIM(ss.ScenePurposeNotes)), N''), N'') = N''
AND NOT EXISTS (SELECT 1 FROM dbo.ScenePurposes sp WHERE sp.SceneID = ss.SceneID);
;WITH ScopeScenes AS
(
SELECT * FROM dbo.NarrativeSceneOrder
WHERE ProjectID = @ProjectID AND (@BookID IS NULL OR BookID = @BookID) AND (@SceneID IS NULL OR SceneID = @SceneID)
)
INSERT @Warnings
SELECT BookID, ChapterID, SceneID, N'Scene', SceneID, N'Missing Scene Outcome', N'Info',
N'Possible issue: this scene has no outcome notes.',
N'Add an outcome if the result of the scene needs tracking.'
FROM ScopeScenes WHERE ISNULL(NULLIF(LTRIM(RTRIM(SceneOutcomeNotes)), N''), N'') = N'';
;WITH Ordered AS
(
SELECT *, LAG(SceneID) OVER (ORDER BY GlobalSceneIndex) AS PreviousSceneID,
LAG(StartDateTime) OVER (ORDER BY GlobalSceneIndex) AS PreviousStartDateTime
FROM dbo.NarrativeSceneOrder
WHERE ProjectID = @ProjectID AND (@BookID IS NULL OR BookID = @BookID)
)
INSERT @Warnings
SELECT BookID, ChapterID, SceneID, N'Scene', SceneID, N'Scene Date Order Conflict', N'Warning',
N'Possible issue: this narrative scene has an earlier story date than the scene before it.',
CONCAT(N'Previous date: ', CONVERT(nvarchar(30), PreviousStartDateTime, 120), N'. Current date: ', CONVERT(nvarchar(30), StartDateTime, 120), N'.')
FROM Ordered
WHERE (@SceneID IS NULL OR SceneID = @SceneID)
AND PreviousStartDateTime IS NOT NULL AND StartDateTime IS NOT NULL AND StartDateTime < PreviousStartDateTime;
;WITH Ordered AS
(
SELECT *, LAG(StartDateTime) OVER (ORDER BY GlobalSceneIndex) AS PreviousStartDateTime
FROM dbo.NarrativeSceneOrder
WHERE ProjectID = @ProjectID AND (@BookID IS NULL OR BookID = @BookID)
)
INSERT @Warnings
SELECT BookID, ChapterID, SceneID, N'Scene', SceneID, N'Long Unexplained Time Gap', N'Info',
N'Possible issue: there is a long story-time gap before this scene without a clear time-passage note.',
CONCAT(N'Gap: ', DATEDIFF(day, PreviousStartDateTime, StartDateTime), N' days.')
FROM Ordered o
WHERE (@SceneID IS NULL OR SceneID = @SceneID)
AND PreviousStartDateTime IS NOT NULL AND StartDateTime IS NOT NULL
AND DATEDIFF(day, PreviousStartDateTime, StartDateTime) > 14
AND ISNULL(NULLIF(LTRIM(RTRIM(RelativeTimeText)), N''), N'') = N''
AND NOT EXISTS (
SELECT 1 FROM dbo.ScenePurposes sp
INNER JOIN dbo.ScenePurposeTypes spt ON spt.ScenePurposeTypeID = sp.ScenePurposeTypeID
WHERE sp.SceneID = o.SceneID AND spt.PurposeName = N'Time Passage'
);
;WITH Events AS
(
SELECT pt.PlotThreadID, pt.ThreadTitle, pt.Importance, ts.IsOpenStatus, te.SceneID, tet.TypeName, nso.BookID, nso.ChapterID, nso.GlobalSceneIndex
FROM dbo.ThreadEvents te
INNER JOIN dbo.ThreadEventTypes tet ON tet.ThreadEventTypeID = te.EventTypeID
INNER JOIN dbo.PlotThreads pt ON pt.PlotThreadID = te.PlotThreadID AND pt.IsArchived = 0
INNER JOIN dbo.ThreadStatuses ts ON ts.ThreadStatusID = pt.ThreadStatusID
INNER JOIN dbo.PlotLines pl ON pl.PlotLineID = pt.PlotLineID AND pl.ProjectID = @ProjectID AND pl.IsArchived = 0
INNER JOIN dbo.NarrativeSceneOrder nso ON nso.SceneID = te.SceneID
WHERE (@BookID IS NULL OR nso.BookID = @BookID)
),
ThreadRanges AS
(
SELECT PlotThreadID, ThreadTitle, Importance, IsOpenStatus,
MIN(CASE WHEN TypeName = N'Introduced' THEN GlobalSceneIndex END) AS IntroducedIndex,
MIN(CASE WHEN TypeName IN (N'Resolved', N'Payoff') THEN GlobalSceneIndex END) AS ResolvedIndex,
MIN(CASE WHEN TypeName = N'Question Raised' THEN GlobalSceneIndex END) AS QuestionRaisedIndex,
MIN(CASE WHEN TypeName = N'Question Answered' THEN GlobalSceneIndex END) AS QuestionAnsweredIndex,
MAX(GlobalSceneIndex) AS LastEventIndex,
MAX(BookID) AS BookID, MAX(ChapterID) AS ChapterID, MIN(SceneID) AS AnySceneID
FROM Events GROUP BY PlotThreadID, ThreadTitle, Importance, IsOpenStatus
)
INSERT @Warnings
SELECT BookID, ChapterID, AnySceneID, N'PlotThread', PlotThreadID, N'Thread Resolved Before Introduced', N'Warning',
CONCAT(N'Possible issue: "', ThreadTitle, N'" appears resolved before it is introduced.'),
NULL
FROM ThreadRanges
WHERE ResolvedIndex IS NOT NULL AND IntroducedIndex IS NOT NULL AND ResolvedIndex < IntroducedIndex;
;WITH Events AS
(
SELECT pt.PlotThreadID, pt.ThreadTitle, te.SceneID, tet.TypeName, nso.BookID, nso.ChapterID, nso.GlobalSceneIndex
FROM dbo.ThreadEvents te
INNER JOIN dbo.ThreadEventTypes tet ON tet.ThreadEventTypeID = te.EventTypeID
INNER JOIN dbo.PlotThreads pt ON pt.PlotThreadID = te.PlotThreadID AND pt.IsArchived = 0
INNER JOIN dbo.PlotLines pl ON pl.PlotLineID = pt.PlotLineID AND pl.ProjectID = @ProjectID AND pl.IsArchived = 0
INNER JOIN dbo.NarrativeSceneOrder nso ON nso.SceneID = te.SceneID
WHERE (@BookID IS NULL OR nso.BookID = @BookID)
),
ThreadRanges AS
(
SELECT PlotThreadID, ThreadTitle,
MIN(CASE WHEN TypeName = N'Question Raised' THEN GlobalSceneIndex END) AS QuestionRaisedIndex,
MIN(CASE WHEN TypeName = N'Question Answered' THEN GlobalSceneIndex END) AS QuestionAnsweredIndex,
MAX(BookID) AS BookID, MAX(ChapterID) AS ChapterID, MIN(SceneID) AS AnySceneID
FROM Events GROUP BY PlotThreadID, ThreadTitle
)
INSERT @Warnings
SELECT BookID, ChapterID, AnySceneID, N'PlotThread', PlotThreadID, N'Question Answered Before Raised', N'Warning',
CONCAT(N'Possible issue: "', ThreadTitle, N'" is answered before it is raised.'),
NULL
FROM ThreadRanges
WHERE QuestionAnsweredIndex IS NOT NULL AND QuestionRaisedIndex IS NOT NULL AND QuestionAnsweredIndex < QuestionRaisedIndex;
;WITH Clues AS
(
SELECT pt.PlotThreadID, pt.ThreadTitle, MIN(nso.SceneID) AS SceneID, MIN(nso.BookID) AS BookID, MIN(nso.ChapterID) AS ChapterID
FROM dbo.ThreadEvents te
INNER JOIN dbo.ThreadEventTypes tet ON tet.ThreadEventTypeID = te.EventTypeID
INNER JOIN dbo.PlotThreads pt ON pt.PlotThreadID = te.PlotThreadID AND pt.IsArchived = 0
INNER JOIN dbo.PlotLines pl ON pl.PlotLineID = pt.PlotLineID AND pl.ProjectID = @ProjectID AND pl.IsArchived = 0
INNER JOIN dbo.NarrativeSceneOrder nso ON nso.SceneID = te.SceneID
WHERE tet.TypeName = N'Clue Planted' AND (@BookID IS NULL OR nso.BookID = @BookID)
GROUP BY pt.PlotThreadID, pt.ThreadTitle
)
INSERT @Warnings
SELECT BookID, ChapterID, SceneID, N'PlotThread', PlotThreadID, N'Clue Without Payoff', N'Info',
CONCAT(N'Possible issue: clue thread "', ThreadTitle, N'" has no payoff, answer, reveal, or resolution yet.'),
NULL
FROM Clues c
WHERE NOT EXISTS (
SELECT 1 FROM dbo.ThreadEvents te
INNER JOIN dbo.ThreadEventTypes tet ON tet.ThreadEventTypeID = te.EventTypeID
WHERE te.PlotThreadID = c.PlotThreadID AND tet.TypeName IN (N'Payoff', N'Question Answered', N'Revealed', N'Resolved')
);
;WITH LastThreadEvent AS
(
SELECT pt.PlotThreadID, pt.ThreadTitle, MAX(nso.GlobalSceneIndex) AS LastEventIndex, MAX(nso.BookID) AS BookID, MAX(nso.ChapterID) AS ChapterID, MAX(nso.SceneID) AS SceneID
FROM dbo.PlotThreads pt
INNER JOIN dbo.ThreadStatuses ts ON ts.ThreadStatusID = pt.ThreadStatusID AND ts.IsOpenStatus = 1
INNER JOIN dbo.PlotLines pl ON pl.PlotLineID = pt.PlotLineID AND pl.ProjectID = @ProjectID AND pl.IsArchived = 0
INNER JOIN dbo.ThreadEvents te ON te.PlotThreadID = pt.PlotThreadID
INNER JOIN dbo.NarrativeSceneOrder nso ON nso.SceneID = te.SceneID
WHERE pt.IsArchived = 0 AND (@BookID IS NULL OR nso.BookID = @BookID)
GROUP BY pt.PlotThreadID, pt.ThreadTitle
),
MaxScope AS
(
SELECT MAX(GlobalSceneIndex) AS MaxSceneIndex FROM dbo.NarrativeSceneOrder WHERE ProjectID = @ProjectID AND (@BookID IS NULL OR BookID = @BookID)
)
INSERT @Warnings
SELECT BookID, ChapterID, SceneID, N'PlotThread', PlotThreadID, N'Thread Dormant', N'Info',
CONCAT(N'Possible issue: "', ThreadTitle, N'" has no event for more than 8 scenes.'),
CONCAT(N'Scenes since last event: ', MaxSceneIndex - LastEventIndex, N'.')
FROM LastThreadEvent CROSS JOIN MaxScope
WHERE MaxSceneIndex - LastEventIndex > 8;
;WITH OpenMajor AS
(
SELECT COUNT(*) AS OpenCount
FROM dbo.PlotThreads pt
INNER JOIN dbo.ThreadStatuses ts ON ts.ThreadStatusID = pt.ThreadStatusID AND ts.IsOpenStatus = 1
INNER JOIN dbo.PlotLines pl ON pl.PlotLineID = pt.PlotLineID AND pl.ProjectID = @ProjectID AND pl.IsArchived = 0
WHERE pt.IsArchived = 0 AND pt.Importance >= 8 AND (@BookID IS NULL OR pl.BookID IS NULL OR pl.BookID = @BookID)
)
INSERT @Warnings
SELECT @BookID, NULL, NULL, N'Project', @ProjectID, N'Too Many Open Major Threads', N'Info',
CONCAT(N'Possible issue: there are ', OpenCount, N' high-importance open threads.'),
N'This may be intentional, but it can make the active story load feel heavy.'
FROM OpenMajor WHERE OpenCount > 10;
;WITH AssetEventsOrdered AS
(
SELECT ae.StoryAssetID, sa.AssetName, ae.SceneID, aet.TypeName, nso.BookID, nso.ChapterID, nso.GlobalSceneIndex
FROM dbo.AssetEvents ae
INNER JOIN dbo.StoryAssets sa ON sa.StoryAssetID = ae.StoryAssetID AND sa.ProjectID = @ProjectID AND sa.IsArchived = 0
INNER JOIN dbo.AssetEventTypes aet ON aet.AssetEventTypeID = ae.AssetEventTypeID
INNER JOIN dbo.NarrativeSceneOrder nso ON nso.SceneID = ae.SceneID
WHERE (@BookID IS NULL OR nso.BookID = @BookID)
),
AssetFirsts AS
(
SELECT StoryAssetID, AssetName,
MIN(CASE WHEN TypeName IN (N'First Mentioned', N'Mentioned', N'Found') THEN GlobalSceneIndex END) AS IntroducedIndex,
MIN(CASE WHEN TypeName IN (N'Used', N'Opened', N'Revealed', N'Destroyed', N'Given', N'Taken', N'Hidden', N'Lost', N'Stolen') THEN GlobalSceneIndex END) AS UsedIndex,
MIN(CASE WHEN TypeName IN (N'Used', N'Opened', N'Revealed', N'Destroyed', N'Given', N'Taken', N'Hidden', N'Lost', N'Stolen') THEN SceneID END) AS UsedSceneID,
MIN(BookID) AS BookID, MIN(ChapterID) AS ChapterID
FROM AssetEventsOrdered GROUP BY StoryAssetID, AssetName
)
INSERT @Warnings
SELECT BookID, ChapterID, UsedSceneID, N'StoryAsset', StoryAssetID, N'Asset Used Before Introduced', N'Warning',
CONCAT(N'Possible issue: "', AssetName, N'" appears used before it is introduced.'),
NULL
FROM AssetFirsts
WHERE UsedIndex IS NOT NULL AND (IntroducedIndex IS NULL OR UsedIndex < IntroducedIndex);
;WITH TargetEvents AS
(
SELECT ad.AssetDependencyID, ad.SourceAssetID, source.AssetName AS SourceAssetName, ad.TargetAssetID, target.AssetName AS TargetAssetName,
ad.SourceRequiredStateID, ad.TargetBlockedStateID, ae.SceneID, nso.BookID, nso.ChapterID, nso.GlobalSceneIndex
FROM dbo.AssetDependencies ad
INNER JOIN dbo.AssetDependencyTypes adt ON adt.AssetDependencyTypeID = ad.AssetDependencyTypeID AND adt.IsValidatingDependency = 1
INNER JOIN dbo.StoryAssets source ON source.StoryAssetID = ad.SourceAssetID
INNER JOIN dbo.StoryAssets target ON target.StoryAssetID = ad.TargetAssetID AND target.ProjectID = @ProjectID
INNER JOIN dbo.AssetEvents ae ON ae.StoryAssetID = ad.TargetAssetID AND ae.ToStateID = ad.TargetBlockedStateID
INNER JOIN dbo.NarrativeSceneOrder nso ON nso.SceneID = ae.SceneID
WHERE ad.IsArchived = 0 AND ad.SourceRequiredStateID IS NOT NULL AND ad.TargetBlockedStateID IS NOT NULL AND (@BookID IS NULL OR nso.BookID = @BookID)
)
INSERT @Warnings
SELECT BookID, ChapterID, SceneID, N'AssetDependency', AssetDependencyID, N'Asset State Dependency Missing', N'Warning',
CONCAT(N'Possible issue: "', TargetAssetName, N'" reaches a blocked state before "', SourceAssetName, N'" reaches its required state.'),
NULL
FROM TargetEvents te
WHERE NOT EXISTS (
SELECT 1 FROM dbo.AssetEvents sourceEvent
INNER JOIN dbo.NarrativeSceneOrder sourceScene ON sourceScene.SceneID = sourceEvent.SceneID
WHERE sourceEvent.StoryAssetID = te.SourceAssetID
AND sourceEvent.ToStateID = te.SourceRequiredStateID
AND sourceScene.GlobalSceneIndex <= te.GlobalSceneIndex
);
;WITH AssetPlaces AS
(
SELECT sal.StoryAssetID, sa.AssetName, sal.LocationID, sal.SceneID, nso.BookID, nso.ChapterID, nso.GlobalSceneIndex,
LAG(sal.LocationID) OVER (PARTITION BY sal.StoryAssetID ORDER BY nso.GlobalSceneIndex) AS PreviousLocationID
FROM dbo.SceneAssetLocations sal
INNER JOIN dbo.StoryAssets sa ON sa.StoryAssetID = sal.StoryAssetID AND sa.ProjectID = @ProjectID AND sa.IsArchived = 0
INNER JOIN dbo.NarrativeSceneOrder nso ON nso.SceneID = sal.SceneID
WHERE (@BookID IS NULL OR nso.BookID = @BookID)
)
INSERT @Warnings
SELECT BookID, ChapterID, SceneID, N'StoryAsset', StoryAssetID, N'Asset Location Conflict', N'Warning',
CONCAT(N'Possible issue: "', AssetName, N'" appears in a new location without a movement-style event.'),
N'Add a Move, Given, Taken, Found, or Location Changed event if the location change needs explaining.'
FROM AssetPlaces ap
WHERE PreviousLocationID IS NOT NULL AND PreviousLocationID <> LocationID
AND NOT EXISTS (
SELECT 1 FROM dbo.AssetEvents ae
INNER JOIN dbo.AssetEventTypes aet ON aet.AssetEventTypeID = ae.AssetEventTypeID
WHERE ae.StoryAssetID = ap.StoryAssetID AND ae.SceneID = ap.SceneID AND aet.TypeName IN (N'Moved', N'Given', N'Taken', N'Found', N'Location Changed')
);
INSERT @Warnings
SELECT DISTINCT nso.BookID, nso.ChapterID, sc.SceneID, N'Character', sc.CharacterID, N'Character Knowledge Conflict', N'Info',
CONCAT(N'Possible issue: ', ch.CharacterName, N' has knowledge notes in this scene but no structured knowledge record.'),
N'This is only a low-severity prompt to add Character Knowledge if useful.'
FROM dbo.SceneCharacters sc
INNER JOIN dbo.Characters ch ON ch.CharacterID = sc.CharacterID
INNER JOIN dbo.NarrativeSceneOrder nso ON nso.SceneID = sc.SceneID
WHERE ch.ProjectID = @ProjectID AND (@BookID IS NULL OR nso.BookID = @BookID) AND (@SceneID IS NULL OR sc.SceneID = @SceneID)
AND ISNULL(NULLIF(LTRIM(RTRIM(sc.KnowledgeNotes)), N''), N'') <> N''
AND NOT EXISTS (SELECT 1 FROM dbo.CharacterKnowledge ck WHERE ck.SceneID = sc.SceneID AND ck.CharacterID = sc.CharacterID);
INSERT @Warnings
SELECT targetOrder.BookID, targetOrder.ChapterID, sd.TargetSceneID, N'SceneDependency', sd.SceneDependencyID, N'Scene Dependency Broken', N'Warning',
N'Possible issue: a scene dependency says an earlier setup should occur before this scene, but the order is reversed.',
sd.Description
FROM dbo.SceneDependencies sd
INNER JOIN dbo.NarrativeSceneOrder sourceOrder ON sourceOrder.SceneID = sd.SourceSceneID
INNER JOIN dbo.NarrativeSceneOrder targetOrder ON targetOrder.SceneID = sd.TargetSceneID
WHERE sd.IsArchived = 0 AND sourceOrder.ProjectID = @ProjectID
AND (@BookID IS NULL OR targetOrder.BookID = @BookID) AND (@SceneID IS NULL OR targetOrder.SceneID = @SceneID)
AND sourceOrder.GlobalSceneIndex > targetOrder.GlobalSceneIndex;
;WITH CharacterSceneLocations AS
(
SELECT sc.SceneID, sc.CharacterID, COUNT(DISTINCT sc.LocationID) AS LocationCount, MAX(ch.CharacterName) AS CharacterName,
MAX(nso.BookID) AS BookID, MAX(nso.ChapterID) AS ChapterID
FROM dbo.SceneCharacters sc
INNER JOIN dbo.Characters ch ON ch.CharacterID = sc.CharacterID AND ch.ProjectID = @ProjectID
INNER JOIN dbo.NarrativeSceneOrder nso ON nso.SceneID = sc.SceneID
LEFT JOIN dbo.PresenceTypes pt ON pt.PresenceTypeID = sc.PresenceTypeID
WHERE sc.LocationID IS NOT NULL AND (@BookID IS NULL OR nso.BookID = @BookID) AND (@SceneID IS NULL OR sc.SceneID = @SceneID)
AND ISNULL(pt.TypeName, N'') NOT IN (N'Nearby', N'Hidden', N'Overhearing', N'Off-screen')
GROUP BY sc.SceneID, sc.CharacterID
)
INSERT @Warnings
SELECT BookID, ChapterID, SceneID, N'Character', CharacterID, N'Character Location Conflict', N'Warning',
CONCAT(N'Possible issue: ', CharacterName, N' appears in more than one location in the same scene.'),
NULL
FROM CharacterSceneLocations WHERE LocationCount > 1;
;WITH MetricRows AS
(
SELECT smv.MetricTypeID, smt.MetricName, smv.SceneID, smv.Value, nso.BookID, nso.ChapterID, nso.GlobalSceneIndex,
ROW_NUMBER() OVER (PARTITION BY smv.MetricTypeID ORDER BY nso.GlobalSceneIndex) AS rn,
ROW_NUMBER() OVER (PARTITION BY smv.MetricTypeID, CASE WHEN smv.Value >= 8 THEN 1 ELSE 0 END ORDER BY nso.GlobalSceneIndex) AS grpRn
FROM dbo.SceneMetricValues smv
INNER JOIN dbo.SceneMetricTypes smt ON smt.MetricTypeID = smv.MetricTypeID
INNER JOIN dbo.NarrativeSceneOrder nso ON nso.SceneID = smv.SceneID
WHERE nso.ProjectID = @ProjectID AND (@BookID IS NULL OR nso.BookID = @BookID)
),
HighGroups AS
(
SELECT MetricTypeID, MetricName, rn - grpRn AS grp, COUNT(*) AS SceneCount, MIN(SceneID) AS FirstSceneID, MAX(SceneID) AS LastSceneID,
MAX(BookID) AS BookID, MAX(ChapterID) AS ChapterID
FROM MetricRows WHERE Value >= 8 GROUP BY MetricTypeID, MetricName, rn - grpRn HAVING COUNT(*) > 6
)
INSERT @Warnings
SELECT BookID, ChapterID, LastSceneID, N'Metric', MetricTypeID, N'Sustained High Intensity', N'Info',
CONCAT(N'Possible reader fatigue: ', MetricName, N' remains high for ', SceneCount, N' consecutive scenes.'),
CONCAT(N'Scene range: ', FirstSceneID, N' to ', LastSceneID, N'.')
FROM HighGroups;
;WITH MetricRows AS
(
SELECT smv.MetricTypeID, smt.MetricName, smv.SceneID, smv.Value, nso.BookID, nso.ChapterID, nso.GlobalSceneIndex,
ROW_NUMBER() OVER (PARTITION BY smv.MetricTypeID ORDER BY nso.GlobalSceneIndex) AS rn,
ROW_NUMBER() OVER (PARTITION BY smv.MetricTypeID, CASE WHEN smv.Value <= 2 THEN 1 ELSE 0 END ORDER BY nso.GlobalSceneIndex) AS grpRn
FROM dbo.SceneMetricValues smv
INNER JOIN dbo.SceneMetricTypes smt ON smt.MetricTypeID = smv.MetricTypeID
INNER JOIN dbo.NarrativeSceneOrder nso ON nso.SceneID = smv.SceneID
WHERE nso.ProjectID = @ProjectID AND (@BookID IS NULL OR nso.BookID = @BookID)
AND smt.MetricName IN (N'Overall Intensity', N'Tension', N'Emotional Weight', N'Action')
),
LowGroups AS
(
SELECT MetricTypeID, MetricName, rn - grpRn AS grp, COUNT(*) AS SceneCount, MIN(SceneID) AS FirstSceneID, MAX(SceneID) AS LastSceneID,
MAX(BookID) AS BookID, MAX(ChapterID) AS ChapterID
FROM MetricRows WHERE Value <= 2 GROUP BY MetricTypeID, MetricName, rn - grpRn HAVING COUNT(*) > 8
)
INSERT @Warnings
SELECT BookID, ChapterID, LastSceneID, N'Metric', MetricTypeID, N'Sustained Low Intensity', N'Info',
CONCAT(N'Possible issue: ', MetricName, N' remains low for ', SceneCount, N' consecutive scenes.'),
CONCAT(N'Scene range: ', FirstSceneID, N' to ', LastSceneID, N'.')
FROM LowGroups;
INSERT dbo.ContinuityWarnings (ValidationRunID, ProjectID, BookID, ChapterID, SceneID, EntityType, EntityID, WarningTypeID, WarningSeverityID, Message, Details)
SELECT @ValidationRunID, @ProjectID, w.BookID, w.ChapterID, w.SceneID, w.EntityType, w.EntityID, wt.WarningTypeID, ws.WarningSeverityID, w.Message, w.Details
FROM @Warnings w
INNER JOIN dbo.WarningTypes wt ON wt.TypeName = w.WarningTypeName
INNER JOIN dbo.WarningSeverities ws ON ws.SeverityName = w.SeverityName
WHERE NOT EXISTS
(
SELECT 1 FROM dbo.ContinuityWarnings existing
WHERE existing.ProjectID = @ProjectID
AND ISNULL(existing.BookID, 0) = ISNULL(w.BookID, 0)
AND ISNULL(existing.ChapterID, 0) = ISNULL(w.ChapterID, 0)
AND ISNULL(existing.SceneID, 0) = ISNULL(w.SceneID, 0)
AND existing.EntityType = w.EntityType
AND ISNULL(existing.EntityID, 0) = ISNULL(w.EntityID, 0)
AND existing.WarningTypeID = wt.WarningTypeID
AND (existing.IsDismissed = 1 OR existing.IsIntentional = 1)
);
EXEC dbo.ValidationRun_Complete @ValidationRunID;
SELECT @ValidationRunID AS ValidationRunID,
SUM(CASE WHEN ws.SeverityName = N'Error' THEN 1 ELSE 0 END) AS ErrorCount,
SUM(CASE WHEN ws.SeverityName = N'Warning' THEN 1 ELSE 0 END) AS WarningCount,
SUM(CASE WHEN ws.SeverityName = N'Info' THEN 1 ELSE 0 END) AS InfoCount,
COUNT(*) AS TotalCount
FROM dbo.ContinuityWarnings cw
INNER JOIN dbo.WarningSeverities ws ON ws.WarningSeverityID = cw.WarningSeverityID
WHERE cw.ValidationRunID = @ValidationRunID;
END;
GO
CREATE OR ALTER PROCEDURE dbo.ContinuityValidation_ValidateBook
@BookID int,
@TriggeredBy nvarchar(100) = NULL
AS
BEGIN
SET NOCOUNT ON;
DECLARE @ProjectID int = (SELECT ProjectID FROM dbo.Books WHERE BookID = @BookID);
EXEC dbo.ContinuityValidation_ValidateProject @ProjectID = @ProjectID, @BookID = @BookID, @TriggeredBy = @TriggeredBy;
END;
GO
CREATE OR ALTER PROCEDURE dbo.ContinuityValidation_ValidateScene
@SceneID int,
@TriggeredBy nvarchar(100) = NULL
AS
BEGIN
SET NOCOUNT ON;
DECLARE @ProjectID int, @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
WHERE s.SceneID = @SceneID;
EXEC dbo.ContinuityValidation_ValidateProject @ProjectID = @ProjectID, @BookID = @BookID, @SceneID = @SceneID, @TriggeredBy = @TriggeredBy;
END;
GO
CREATE OR ALTER PROCEDURE dbo.ContinuityWarning_List
@ProjectID int,
@BookID int = NULL,
@SceneID int = NULL,
@EntityType nvarchar(80) = NULL,
@EntityID int = NULL,
@WarningTypeID int = NULL,
@WarningSeverityID int = NULL,
@IncludeDismissed bit = 0,
@IncludeIntentional bit = 0
AS
BEGIN
SET NOCOUNT ON;
SELECT cw.ContinuityWarningID, cw.ValidationRunID, cw.ProjectID, cw.BookID, b.BookTitle, cw.ChapterID, c.ChapterNumber, c.ChapterTitle,
cw.SceneID, s.SceneNumber, s.SceneTitle, cw.EntityType, cw.EntityID, cw.WarningTypeID, wt.TypeName AS WarningTypeName,
cw.WarningSeverityID, ws.SeverityName, cw.Message, cw.Details, cw.IsDismissed, cw.IsIntentional, cw.CreatedDate, cw.LastDetectedDate
FROM dbo.ContinuityWarnings cw
INNER JOIN dbo.WarningTypes wt ON wt.WarningTypeID = cw.WarningTypeID
INNER JOIN dbo.WarningSeverities ws ON ws.WarningSeverityID = cw.WarningSeverityID
LEFT JOIN dbo.Books b ON b.BookID = cw.BookID
LEFT JOIN dbo.Chapters c ON c.ChapterID = cw.ChapterID
LEFT JOIN dbo.Scenes s ON s.SceneID = cw.SceneID
WHERE cw.ProjectID = @ProjectID
AND (@BookID IS NULL OR cw.BookID = @BookID)
AND (@SceneID IS NULL OR cw.SceneID = @SceneID)
AND (@EntityType IS NULL OR cw.EntityType = @EntityType)
AND (@EntityID IS NULL OR cw.EntityID = @EntityID)
AND (@WarningTypeID IS NULL OR cw.WarningTypeID = @WarningTypeID)
AND (@WarningSeverityID IS NULL OR cw.WarningSeverityID = @WarningSeverityID)
AND (@IncludeDismissed = 1 OR cw.IsDismissed = 0)
AND (@IncludeIntentional = 1 OR cw.IsIntentional = 0)
ORDER BY ws.SortOrder DESC, cw.LastDetectedDate DESC, cw.ContinuityWarningID DESC;
END;
GO
CREATE OR ALTER PROCEDURE dbo.ContinuityWarning_ListByProject
@ProjectID int,
@IncludeDismissed bit = 0,
@IncludeIntentional bit = 0
AS
BEGIN
SET NOCOUNT ON;
EXEC dbo.ContinuityWarning_List @ProjectID = @ProjectID, @IncludeDismissed = @IncludeDismissed, @IncludeIntentional = @IncludeIntentional;
END;
GO
CREATE OR ALTER PROCEDURE dbo.ContinuityWarning_ListByBook
@ProjectID int,
@BookID int,
@IncludeDismissed bit = 0,
@IncludeIntentional bit = 0
AS
BEGIN
SET NOCOUNT ON;
EXEC dbo.ContinuityWarning_List @ProjectID = @ProjectID, @BookID = @BookID, @IncludeDismissed = @IncludeDismissed, @IncludeIntentional = @IncludeIntentional;
END;
GO
CREATE OR ALTER PROCEDURE dbo.ContinuityWarning_ListByScene
@SceneID int,
@IncludeDismissed bit = 0,
@IncludeIntentional bit = 0
AS
BEGIN
SET NOCOUNT ON;
DECLARE @ProjectID int = (SELECT 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 WHERE s.SceneID = @SceneID);
EXEC dbo.ContinuityWarning_List @ProjectID = @ProjectID, @SceneID = @SceneID, @IncludeDismissed = @IncludeDismissed, @IncludeIntentional = @IncludeIntentional;
END;
GO
CREATE OR ALTER PROCEDURE dbo.ContinuityWarning_ListByEntity
@ProjectID int,
@EntityType nvarchar(80),
@EntityID int,
@IncludeDismissed bit = 0,
@IncludeIntentional bit = 0
AS
BEGIN
SET NOCOUNT ON;
EXEC dbo.ContinuityWarning_List @ProjectID = @ProjectID, @EntityType = @EntityType, @EntityID = @EntityID, @IncludeDismissed = @IncludeDismissed, @IncludeIntentional = @IncludeIntentional;
END;
GO
CREATE OR ALTER PROCEDURE dbo.ContinuityWarning_SceneCounts
@ProjectID int,
@BookID int = NULL
AS
BEGIN
SET NOCOUNT ON;
SELECT cw.SceneID, COUNT(*) AS WarningCount
FROM dbo.ContinuityWarnings cw
WHERE cw.ProjectID = @ProjectID
AND cw.SceneID IS NOT NULL
AND cw.IsDismissed = 0
AND cw.IsIntentional = 0
AND (@BookID IS NULL OR cw.BookID = @BookID)
GROUP BY cw.SceneID;
END;
GO
CREATE OR ALTER PROCEDURE dbo.ContinuityWarning_SetState
@ContinuityWarningID int,
@IsDismissed bit = NULL,
@IsIntentional bit = NULL
AS
BEGIN
SET NOCOUNT ON;
UPDATE dbo.ContinuityWarnings
SET IsDismissed = COALESCE(@IsDismissed, IsDismissed),
IsIntentional = COALESCE(@IsIntentional, IsIntentional)
WHERE ContinuityWarningID = @ContinuityWarningID;
END;
GO
CREATE OR ALTER PROCEDURE dbo.ContinuityWarning_Dismiss
@ContinuityWarningID int
AS
BEGIN
SET NOCOUNT ON;
EXEC dbo.ContinuityWarning_SetState @ContinuityWarningID = @ContinuityWarningID, @IsDismissed = 1;
END;
GO
CREATE OR ALTER PROCEDURE dbo.ContinuityWarning_MarkIntentional
@ContinuityWarningID int
AS
BEGIN
SET NOCOUNT ON;
EXEC dbo.ContinuityWarning_SetState @ContinuityWarningID = @ContinuityWarningID, @IsIntentional = 1;
END;
GO
CREATE OR ALTER PROCEDURE dbo.ContinuityWarning_Restore
@ContinuityWarningID int
AS
BEGIN
SET NOCOUNT ON;
UPDATE dbo.ContinuityWarnings
SET IsDismissed = 0, IsIntentional = 0
WHERE ContinuityWarningID = @ContinuityWarningID;
END;
GO

View File

@ -0,0 +1,305 @@
SET ANSI_NULLS ON;
GO
SET QUOTED_IDENTIFIER ON;
GO
IF COL_LENGTH('dbo.SceneDependencies', 'ProjectID') IS NULL
BEGIN
ALTER TABLE dbo.SceneDependencies ADD ProjectID int NULL;
END;
GO
UPDATE sd
SET ProjectID = b.ProjectID
FROM dbo.SceneDependencies sd
INNER JOIN dbo.Scenes s ON s.SceneID = sd.SourceSceneID
INNER JOIN dbo.Chapters c ON c.ChapterID = s.ChapterID
INNER JOIN dbo.Books b ON b.BookID = c.BookID
WHERE sd.ProjectID IS NULL;
GO
IF COL_LENGTH('dbo.SceneDependencies', 'IsHardDependency') IS NULL
BEGIN
ALTER TABLE dbo.SceneDependencies ADD IsHardDependency bit NOT NULL CONSTRAINT DF_SceneDependencies_IsHardDependency DEFAULT 0;
END;
GO
MERGE dbo.SceneDependencyTypes AS target
USING (VALUES
(N'Must Occur Before', 10), (N'Emotional Setup', 20), (N'Reveal Setup', 30), (N'Action Consequence', 40),
(N'Travel Dependency', 50), (N'Relationship Dependency', 60), (N'Knowledge Dependency', 70), (N'Object Dependency', 80),
(N'Location Dependency', 90), (N'Time Dependency', 100)
) AS source (TypeName, SortOrder)
ON target.TypeName = source.TypeName
WHEN NOT MATCHED THEN INSERT (TypeName, SortOrder) VALUES (source.TypeName, source.SortOrder);
GO
CREATE OR ALTER PROCEDURE dbo.SceneDependencyLookup_All
AS
BEGIN
SET NOCOUNT ON;
SELECT SceneDependencyTypeID, TypeName, SortOrder, IsActive
FROM dbo.SceneDependencyTypes
WHERE IsActive = 1
ORDER BY SortOrder;
END;
GO
CREATE OR ALTER PROCEDURE dbo.SceneDependency_Save
@SceneDependencyID int = NULL,
@ProjectID int = NULL,
@SourceSceneID int,
@TargetSceneID int,
@SceneDependencyTypeID int,
@Description nvarchar(max) = NULL,
@IsHardDependency bit = 0
AS
BEGIN
SET NOCOUNT ON;
IF @ProjectID IS NULL
BEGIN
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
WHERE s.SceneID = @SourceSceneID;
END;
IF @SourceSceneID = @TargetSceneID
RETURN;
IF @SceneDependencyID IS NULL OR @SceneDependencyID = 0
BEGIN
INSERT dbo.SceneDependencies (ProjectID, SourceSceneID, TargetSceneID, SceneDependencyTypeID, Description, IsHardDependency)
VALUES (@ProjectID, @SourceSceneID, @TargetSceneID, @SceneDependencyTypeID, @Description, @IsHardDependency);
SELECT CAST(SCOPE_IDENTITY() AS int);
RETURN;
END;
UPDATE dbo.SceneDependencies
SET SourceSceneID = @SourceSceneID,
TargetSceneID = @TargetSceneID,
SceneDependencyTypeID = @SceneDependencyTypeID,
Description = @Description,
IsHardDependency = @IsHardDependency,
UpdatedDate = SYSUTCDATETIME()
WHERE SceneDependencyID = @SceneDependencyID;
SELECT @SceneDependencyID;
END;
GO
CREATE OR ALTER PROCEDURE dbo.SceneDependency_Archive
@SceneDependencyID int
AS
BEGIN
SET NOCOUNT ON;
UPDATE dbo.SceneDependencies
SET IsArchived = 1, UpdatedDate = SYSUTCDATETIME()
WHERE SceneDependencyID = @SceneDependencyID;
END;
GO
CREATE OR ALTER PROCEDURE dbo.SceneDependency_ListByScene
@SceneID int
AS
BEGIN
SET NOCOUNT ON;
SELECT sd.SceneDependencyID, sd.ProjectID, sd.SourceSceneID, sd.TargetSceneID, sd.SceneDependencyTypeID,
sdt.TypeName AS SceneDependencyTypeName, sd.Description, sd.IsHardDependency, sd.CreatedDate, sd.UpdatedDate, sd.IsArchived,
sourceOrder.BookID AS SourceBookID, sourceOrder.BookTitle AS SourceBookTitle, sourceOrder.ChapterID AS SourceChapterID,
sourceOrder.ChapterNumber AS SourceChapterNumber, sourceOrder.SceneNumber AS SourceSceneNumber, sourceOrder.SceneTitle AS SourceSceneTitle,
targetOrder.BookID AS TargetBookID, targetOrder.BookTitle AS TargetBookTitle, targetOrder.ChapterID AS TargetChapterID,
targetOrder.ChapterNumber AS TargetChapterNumber, targetOrder.SceneNumber AS TargetSceneNumber, targetOrder.SceneTitle AS TargetSceneTitle,
CASE WHEN sd.TargetSceneID = @SceneID THEN N'DependsOn' ELSE N'Dependent' END AS DependencyDirection
FROM dbo.SceneDependencies sd
INNER JOIN dbo.SceneDependencyTypes sdt ON sdt.SceneDependencyTypeID = sd.SceneDependencyTypeID
INNER JOIN dbo.NarrativeSceneOrder sourceOrder ON sourceOrder.SceneID = sd.SourceSceneID
INNER JOIN dbo.NarrativeSceneOrder targetOrder ON targetOrder.SceneID = sd.TargetSceneID
WHERE sd.IsArchived = 0 AND @SceneID IN (sd.SourceSceneID, sd.TargetSceneID)
ORDER BY DependencyDirection, sourceOrder.GlobalSceneIndex, targetOrder.GlobalSceneIndex;
END;
GO
CREATE OR ALTER PROCEDURE dbo.SceneDependency_ListByProject
@ProjectID int,
@BookID int = NULL
AS
BEGIN
SET NOCOUNT ON;
SELECT sd.SceneDependencyID, sd.ProjectID, sd.SourceSceneID, sd.TargetSceneID, sd.SceneDependencyTypeID,
sdt.TypeName AS SceneDependencyTypeName, sd.Description, sd.IsHardDependency, sd.CreatedDate, sd.UpdatedDate, sd.IsArchived,
sourceOrder.BookID AS SourceBookID, sourceOrder.BookTitle AS SourceBookTitle, sourceOrder.ChapterID AS SourceChapterID,
sourceOrder.ChapterNumber AS SourceChapterNumber, sourceOrder.SceneNumber AS SourceSceneNumber, sourceOrder.SceneTitle AS SourceSceneTitle,
targetOrder.BookID AS TargetBookID, targetOrder.BookTitle AS TargetBookTitle, targetOrder.ChapterID AS TargetChapterID,
targetOrder.ChapterNumber AS TargetChapterNumber, targetOrder.SceneNumber AS TargetSceneNumber, targetOrder.SceneTitle AS TargetSceneTitle
FROM dbo.SceneDependencies sd
INNER JOIN dbo.SceneDependencyTypes sdt ON sdt.SceneDependencyTypeID = sd.SceneDependencyTypeID
INNER JOIN dbo.NarrativeSceneOrder sourceOrder ON sourceOrder.SceneID = sd.SourceSceneID
INNER JOIN dbo.NarrativeSceneOrder targetOrder ON targetOrder.SceneID = sd.TargetSceneID
WHERE sd.IsArchived = 0 AND sd.ProjectID = @ProjectID AND (@BookID IS NULL OR targetOrder.BookID = @BookID OR sourceOrder.BookID = @BookID)
ORDER BY sourceOrder.GlobalSceneIndex, targetOrder.GlobalSceneIndex;
END;
GO
CREATE OR ALTER PROCEDURE dbo.SceneDependency_Counts
@ProjectID int,
@BookID int = NULL
AS
BEGIN
SET NOCOUNT ON;
SELECT scene.SceneID, COUNT(*) AS DependencyCount
FROM dbo.NarrativeSceneOrder scene
INNER JOIN dbo.SceneDependencies sd ON scene.SceneID IN (sd.SourceSceneID, sd.TargetSceneID) AND sd.IsArchived = 0
WHERE scene.ProjectID = @ProjectID AND (@BookID IS NULL OR scene.BookID = @BookID)
GROUP BY scene.SceneID;
END;
GO
CREATE OR ALTER PROCEDURE dbo.ChapterOption_ListByProject
@ProjectID int
AS
BEGIN
SET NOCOUNT ON;
SELECT c.ChapterID, b.BookID, b.BookTitle, b.BookNumber, c.ChapterNumber, c.ChapterTitle, c.SortOrder
FROM dbo.Chapters c
INNER JOIN dbo.Books b ON b.BookID = c.BookID
WHERE b.ProjectID = @ProjectID AND b.IsArchived = 0 AND c.IsArchived = 0
ORDER BY b.SortOrder, b.BookNumber, c.SortOrder, c.ChapterNumber;
END;
GO
CREATE OR ALTER PROCEDURE dbo.Scene_NormalizeSortOrder
@ChapterID int
AS
BEGIN
SET NOCOUNT ON;
;WITH ordered AS
(
SELECT SceneID, ROW_NUMBER() OVER (ORDER BY SortOrder, SceneNumber, SceneID) AS rn
FROM dbo.Scenes
WHERE ChapterID = @ChapterID AND IsArchived = 0
)
UPDATE s
SET SortOrder = ordered.rn * 10, UpdatedDate = SYSUTCDATETIME()
FROM dbo.Scenes s
INNER JOIN ordered ON ordered.SceneID = s.SceneID;
END;
GO
CREATE OR ALTER PROCEDURE dbo.Scene_RenumberChapter
@ChapterID int
AS
BEGIN
SET NOCOUNT ON;
;WITH ordered AS
(
SELECT SceneID, ROW_NUMBER() OVER (ORDER BY SortOrder, SceneNumber, SceneID) AS rn
FROM dbo.Scenes
WHERE ChapterID = @ChapterID AND IsArchived = 0
)
UPDATE s
SET SceneNumber = ordered.rn, SortOrder = ordered.rn * 10, UpdatedDate = SYSUTCDATETIME()
FROM dbo.Scenes s
INNER JOIN ordered ON ordered.SceneID = s.SceneID;
END;
GO
CREATE OR ALTER PROCEDURE dbo.Chapter_RenumberBook
@BookID int
AS
BEGIN
SET NOCOUNT ON;
;WITH ordered AS
(
SELECT ChapterID, ROW_NUMBER() OVER (ORDER BY SortOrder, ChapterNumber, ChapterID) AS rn
FROM dbo.Chapters
WHERE BookID = @BookID AND IsArchived = 0
)
UPDATE c
SET ChapterNumber = ordered.rn, SortOrder = ordered.rn * 10, UpdatedDate = SYSUTCDATETIME()
FROM dbo.Chapters c
INNER JOIN ordered ON ordered.ChapterID = c.ChapterID;
END;
GO
CREATE OR ALTER PROCEDURE dbo.SceneMovePreview_Get
@SceneID int,
@TargetChapterID int,
@Position nvarchar(20) = N'End'
AS
BEGIN
SET NOCOUNT ON;
SELECT s.SceneID, s.SceneNumber, s.SceneTitle, s.ChapterID AS SourceChapterID, sourceChapter.ChapterNumber AS SourceChapterNumber,
sourceChapter.ChapterTitle AS SourceChapterTitle, sourceBook.BookID AS SourceBookID, sourceBook.BookTitle AS SourceBookTitle,
@TargetChapterID AS TargetChapterID, targetChapter.ChapterNumber AS TargetChapterNumber, targetChapter.ChapterTitle AS TargetChapterTitle,
targetBook.BookID AS TargetBookID, targetBook.BookTitle AS TargetBookTitle, @Position AS Position,
(SELECT COUNT(*) FROM dbo.SceneDependencies WHERE IsArchived = 0 AND @SceneID IN (SourceSceneID, TargetSceneID)) AS DependencyCount,
(SELECT COUNT(*) FROM dbo.ContinuityWarnings WHERE IsDismissed = 0 AND IsIntentional = 0 AND SceneID = @SceneID) AS ActiveWarningCount
FROM dbo.Scenes s
INNER JOIN dbo.Chapters sourceChapter ON sourceChapter.ChapterID = s.ChapterID
INNER JOIN dbo.Books sourceBook ON sourceBook.BookID = sourceChapter.BookID
INNER JOIN dbo.Chapters targetChapter ON targetChapter.ChapterID = @TargetChapterID
INNER JOIN dbo.Books targetBook ON targetBook.BookID = targetChapter.BookID
WHERE s.SceneID = @SceneID;
END;
GO
CREATE OR ALTER PROCEDURE dbo.Scene_MoveToChapter
@SceneID int,
@TargetChapterID int,
@Position nvarchar(20) = N'End',
@Renumber bit = 0
AS
BEGIN
SET NOCOUNT ON;
DECLARE @SourceChapterID int = (SELECT ChapterID FROM dbo.Scenes WHERE SceneID = @SceneID);
DECLARE @NewSortOrder int;
IF @Position = N'Start'
SELECT @NewSortOrder = ISNULL(MIN(SortOrder), 10) - 5 FROM dbo.Scenes WHERE ChapterID = @TargetChapterID AND IsArchived = 0;
ELSE
SELECT @NewSortOrder = ISNULL(MAX(SortOrder), 0) + 10 FROM dbo.Scenes WHERE ChapterID = @TargetChapterID AND IsArchived = 0 AND SceneID <> @SceneID;
UPDATE dbo.Scenes
SET ChapterID = @TargetChapterID, SortOrder = @NewSortOrder, UpdatedDate = SYSUTCDATETIME()
WHERE SceneID = @SceneID;
EXEC dbo.Scene_NormalizeSortOrder @SourceChapterID;
EXEC dbo.Scene_NormalizeSortOrder @TargetChapterID;
IF @Renumber = 1
BEGIN
EXEC dbo.Scene_RenumberChapter @SourceChapterID;
EXEC dbo.Scene_RenumberChapter @TargetChapterID;
END;
SELECT @SceneID;
END;
GO
CREATE OR ALTER PROCEDURE dbo.SceneDependency_FinaliseWarnings
@ProjectID int,
@BookID int = NULL
AS
BEGIN
SET NOCOUNT ON;
DECLARE @ErrorSeverityID int = (SELECT WarningSeverityID FROM dbo.WarningSeverities WHERE SeverityName = N'Error');
DECLARE @WarningSeverityID int = (SELECT WarningSeverityID FROM dbo.WarningSeverities WHERE SeverityName = N'Warning');
DECLARE @WarningTypeID int = (SELECT WarningTypeID FROM dbo.WarningTypes WHERE TypeName = N'Scene Dependency Broken');
UPDATE cw
SET WarningSeverityID = CASE WHEN sd.IsHardDependency = 1 THEN @ErrorSeverityID ELSE @WarningSeverityID END,
Message = CONCAT(N'Possible scene order issue: ''', sourceOrder.SceneTitle, N''' is now after ''', targetOrder.SceneTitle, N'''.'),
Details = COALESCE(sd.Description, cw.Details),
LastDetectedDate = SYSUTCDATETIME()
FROM dbo.ContinuityWarnings cw
INNER JOIN dbo.SceneDependencies sd ON sd.SceneDependencyID = cw.EntityID
INNER JOIN dbo.NarrativeSceneOrder sourceOrder ON sourceOrder.SceneID = sd.SourceSceneID
INNER JOIN dbo.NarrativeSceneOrder targetOrder ON targetOrder.SceneID = sd.TargetSceneID
WHERE cw.ProjectID = @ProjectID
AND cw.WarningTypeID = @WarningTypeID
AND cw.EntityType = N'SceneDependency'
AND (@BookID IS NULL OR cw.BookID = @BookID);
END;
GO

View File

@ -0,0 +1,280 @@
SET ANSI_NULLS ON;
GO
SET QUOTED_IDENTIFIER ON;
GO
CREATE OR ALTER PROCEDURE dbo.Analytics_StoryHealth
@ProjectID int,
@BookID int = NULL,
@StartChapterNumber decimal(10,2) = NULL,
@EndChapterNumber decimal(10,2) = NULL,
@StartSceneNumber decimal(10,2) = NULL,
@EndSceneNumber decimal(10,2) = NULL
AS
BEGIN
SET NOCOUNT ON;
;WITH ScopeScenes AS
(
SELECT nso.*
FROM dbo.NarrativeSceneOrder nso
WHERE nso.ProjectID = @ProjectID
AND (@BookID IS NULL OR nso.BookID = @BookID)
AND (@StartChapterNumber IS NULL OR nso.ChapterNumber >= @StartChapterNumber)
AND (@EndChapterNumber IS NULL OR nso.ChapterNumber <= @EndChapterNumber)
AND (@StartSceneNumber IS NULL OR nso.SceneNumber >= @StartSceneNumber)
AND (@EndSceneNumber IS NULL OR nso.SceneNumber <= @EndSceneNumber)
)
SELECT ss.SceneID, ss.BookID, ss.BookTitle, ss.ChapterID, ss.ChapterNumber, ss.ChapterTitle,
ss.SceneNumber, ss.SceneTitle, ss.GlobalSceneIndex, ss.BookSceneIndex, smt.MetricTypeID,
smt.MetricName, smv.Value, smt.MinValue, smt.MaxValue,
CASE WHEN smt.MaxValue = smt.MinValue THEN 0 ELSE CAST(((smv.Value - smt.MinValue) * 100.0) / (smt.MaxValue - smt.MinValue) AS int) END AS [Percent]
FROM ScopeScenes ss
INNER JOIN dbo.SceneMetricValues smv ON smv.SceneID = ss.SceneID
INNER JOIN dbo.SceneMetricTypes smt ON smt.MetricTypeID = smv.MetricTypeID
WHERE smt.IsActive = 1
AND smt.MetricName IN (N'Overall Intensity', N'Tension', N'Emotional Weight', N'Action', N'Romance', N'Violence', N'Sexual Charge', N'Mystery', N'Hope / Lightness', N'Comedy', N'Darkness')
ORDER BY smt.SortOrder, ss.GlobalSceneIndex;
;WITH ScopeScenes AS
(
SELECT nso.SceneID, nso.ChapterID
FROM dbo.NarrativeSceneOrder nso
WHERE nso.ProjectID = @ProjectID
AND (@BookID IS NULL OR nso.BookID = @BookID)
AND (@StartChapterNumber IS NULL OR nso.ChapterNumber >= @StartChapterNumber)
AND (@EndChapterNumber IS NULL OR nso.ChapterNumber <= @EndChapterNumber)
AND (@StartSceneNumber IS NULL OR nso.SceneNumber >= @StartSceneNumber)
AND (@EndSceneNumber IS NULL OR nso.SceneNumber <= @EndSceneNumber)
)
SELECT rs.RevisionStatusID, rs.StatusName, COUNT(s.SceneID) AS SceneCount,
SUM(CASE WHEN c.ChapterID IS NOT NULL THEN 1 ELSE 0 END) AS ChapterCount
FROM dbo.RevisionStatuses rs
LEFT JOIN dbo.Scenes s ON s.RevisionStatusID = rs.RevisionStatusID AND s.SceneID IN (SELECT SceneID FROM ScopeScenes)
LEFT JOIN dbo.Chapters c ON c.RevisionStatusID = rs.RevisionStatusID AND c.ChapterID IN (SELECT DISTINCT ChapterID FROM ScopeScenes)
GROUP BY rs.RevisionStatusID, rs.StatusName, rs.SortOrder
ORDER BY rs.SortOrder;
;WITH ScopeScenes AS
(
SELECT nso.SceneID, nso.BookID, nso.BookTitle, nso.ChapterID, nso.ChapterNumber, nso.SceneNumber, nso.SceneTitle
FROM dbo.NarrativeSceneOrder nso
WHERE nso.ProjectID = @ProjectID
AND (@BookID IS NULL OR nso.BookID = @BookID)
AND (@StartChapterNumber IS NULL OR nso.ChapterNumber >= @StartChapterNumber)
AND (@EndChapterNumber IS NULL OR nso.ChapterNumber <= @EndChapterNumber)
AND (@StartSceneNumber IS NULL OR nso.SceneNumber >= @StartSceneNumber)
AND (@EndSceneNumber IS NULL OR nso.SceneNumber <= @EndSceneNumber)
)
SELECT ss.SceneID, ss.BookID, ss.BookTitle, ss.ChapterID, ss.ChapterNumber, ss.SceneNumber, ss.SceneTitle,
rs.StatusName AS RevisionStatusName
FROM ScopeScenes ss
INNER JOIN dbo.Scenes s ON s.SceneID = ss.SceneID
INNER JOIN dbo.RevisionStatuses rs ON rs.RevisionStatusID = s.RevisionStatusID
WHERE rs.StatusName IN (N'Needs Work', N'Cut Candidate', N'Drafted')
ORDER BY rs.SortOrder, ss.BookID, ss.ChapterNumber, ss.SceneNumber;
;WITH ScopeScenes AS
(
SELECT nso.SceneID
FROM dbo.NarrativeSceneOrder nso
WHERE nso.ProjectID = @ProjectID
AND (@BookID IS NULL OR nso.BookID = @BookID)
AND (@StartChapterNumber IS NULL OR nso.ChapterNumber >= @StartChapterNumber)
AND (@EndChapterNumber IS NULL OR nso.ChapterNumber <= @EndChapterNumber)
AND (@StartSceneNumber IS NULL OR nso.SceneNumber >= @StartSceneNumber)
AND (@EndSceneNumber IS NULL OR nso.SceneNumber <= @EndSceneNumber)
),
Total AS (SELECT COUNT(*) AS TotalScenes FROM ScopeScenes)
SELECT spt.ScenePurposeTypeID, spt.PurposeName, COUNT(sp.SceneID) AS SceneCount,
CASE WHEN Total.TotalScenes = 0 THEN 0 ELSE CAST(COUNT(sp.SceneID) * 100.0 / Total.TotalScenes AS decimal(5,1)) END AS PercentOfScenes
FROM dbo.ScenePurposeTypes spt
CROSS JOIN Total
LEFT JOIN dbo.ScenePurposes sp ON sp.ScenePurposeTypeID = spt.ScenePurposeTypeID AND sp.SceneID IN (SELECT SceneID FROM ScopeScenes)
WHERE spt.IsArchived = 0
GROUP BY spt.ScenePurposeTypeID, spt.PurposeName, spt.SortOrder, Total.TotalScenes
ORDER BY spt.SortOrder;
;WITH ScopeScenes AS
(
SELECT nso.SceneID, nso.BookID, nso.BookTitle, nso.ChapterID, nso.ChapterNumber, nso.SceneNumber, nso.SceneTitle
FROM dbo.NarrativeSceneOrder nso
INNER JOIN dbo.Scenes s ON s.SceneID = nso.SceneID
WHERE nso.ProjectID = @ProjectID
AND (@BookID IS NULL OR nso.BookID = @BookID)
AND (@StartChapterNumber IS NULL OR nso.ChapterNumber >= @StartChapterNumber)
AND (@EndChapterNumber IS NULL OR nso.ChapterNumber <= @EndChapterNumber)
AND (@StartSceneNumber IS NULL OR nso.SceneNumber >= @StartSceneNumber)
AND (@EndSceneNumber IS NULL OR nso.SceneNumber <= @EndSceneNumber)
AND (
NOT EXISTS (SELECT 1 FROM dbo.ScenePurposes sp WHERE sp.SceneID = nso.SceneID)
OR ISNULL(NULLIF(LTRIM(RTRIM(s.SceneOutcomeNotes)), N''), N'') = N''
)
)
SELECT ss.SceneID, ss.BookID, ss.BookTitle, ss.ChapterID, ss.ChapterNumber, ss.SceneNumber, ss.SceneTitle,
CASE WHEN NOT EXISTS (SELECT 1 FROM dbo.ScenePurposes sp WHERE sp.SceneID = ss.SceneID) THEN 1 ELSE 0 END AS MissingPurpose,
CASE WHEN ISNULL(NULLIF(LTRIM(RTRIM(s.SceneOutcomeNotes)), N''), N'') = N'' THEN 1 ELSE 0 END AS MissingOutcome
FROM ScopeScenes ss
INNER JOIN dbo.Scenes s ON s.SceneID = ss.SceneID
ORDER BY ss.BookID, ss.ChapterNumber, ss.SceneNumber;
;WITH EventScope AS
(
SELECT pt.PlotThreadID, MAX(nso.GlobalSceneIndex) AS LastTouchedIndex, MAX(nso.SceneID) AS LastTouchedSceneID,
MAX(CONCAT(N'Ch ', nso.ChapterNumber, N' / Scene ', nso.SceneNumber, N': ', nso.SceneTitle)) AS LastTouchedSceneLabel
FROM dbo.PlotThreads pt
INNER JOIN dbo.ThreadEvents te ON te.PlotThreadID = pt.PlotThreadID
INNER JOIN dbo.NarrativeSceneOrder nso ON nso.SceneID = te.SceneID
WHERE nso.ProjectID = @ProjectID AND (@BookID IS NULL OR nso.BookID = @BookID)
GROUP BY pt.PlotThreadID
)
SELECT pt.PlotThreadID, pt.ThreadTitle, pl.PlotLineID, pl.PlotLineName, pt.Importance,
ts.StatusName AS ThreadStatusName, ts.IsOpenStatus, ts.IsResolvedStatus,
pt.IntroducedSceneID, pt.PlannedResolutionSceneID, pt.ActualResolutionSceneID,
EventScope.LastTouchedSceneID, EventScope.LastTouchedSceneLabel,
CASE WHEN ts.IsOpenStatus = 1 AND pt.Importance >= 8 THEN 1 ELSE 0 END AS IsHighImportanceOpen,
CASE WHEN ts.IsOpenStatus = 1 AND EventScope.LastTouchedIndex IS NOT NULL
AND (SELECT MAX(GlobalSceneIndex) FROM dbo.NarrativeSceneOrder WHERE ProjectID = @ProjectID AND (@BookID IS NULL OR BookID = @BookID)) - EventScope.LastTouchedIndex > 8 THEN 1 ELSE 0 END AS IsDormant,
CASE WHEN EXISTS (
SELECT 1 FROM dbo.ThreadEvents te INNER JOIN dbo.ThreadEventTypes tet ON tet.ThreadEventTypeID = te.EventTypeID
WHERE te.PlotThreadID = pt.PlotThreadID AND tet.TypeName = N'Clue Planted'
) AND NOT EXISTS (
SELECT 1 FROM dbo.ThreadEvents te INNER JOIN dbo.ThreadEventTypes tet ON tet.ThreadEventTypeID = te.EventTypeID
WHERE te.PlotThreadID = pt.PlotThreadID AND tet.TypeName IN (N'Payoff', N'Question Answered', N'Revealed', N'Resolved')
) THEN 1 ELSE 0 END AS HasClueWithoutPayoff,
CASE WHEN EXISTS (
SELECT 1 FROM dbo.ThreadEvents te INNER JOIN dbo.ThreadEventTypes tet ON tet.ThreadEventTypeID = te.EventTypeID
WHERE te.PlotThreadID = pt.PlotThreadID AND tet.TypeName = N'Question Raised'
) AND NOT EXISTS (
SELECT 1 FROM dbo.ThreadEvents te INNER JOIN dbo.ThreadEventTypes tet ON tet.ThreadEventTypeID = te.EventTypeID
WHERE te.PlotThreadID = pt.PlotThreadID AND tet.TypeName = N'Question Answered'
) THEN 1 ELSE 0 END AS HasQuestionWithoutAnswer
FROM dbo.PlotThreads pt
INNER JOIN dbo.PlotLines pl ON pl.PlotLineID = pt.PlotLineID
INNER JOIN dbo.ThreadStatuses ts ON ts.ThreadStatusID = pt.ThreadStatusID
LEFT JOIN EventScope ON EventScope.PlotThreadID = pt.PlotThreadID
WHERE pl.ProjectID = @ProjectID AND pt.IsArchived = 0 AND pl.IsArchived = 0
AND (@BookID IS NULL OR pl.BookID IS NULL OR pl.BookID = @BookID)
ORDER BY ts.IsOpenStatus DESC, pt.Importance DESC, pl.PlotLineName, pt.ThreadTitle;
;WITH LastAssetEvent AS
(
SELECT ae.StoryAssetID, MAX(nso.GlobalSceneIndex) AS LastEventIndex, MAX(ae.SceneID) AS LastEventSceneID,
MAX(CONCAT(N'Ch ', nso.ChapterNumber, N' / Scene ', nso.SceneNumber, N': ', nso.SceneTitle)) AS LastEventSceneLabel
FROM dbo.AssetEvents ae
INNER JOIN dbo.NarrativeSceneOrder nso ON nso.SceneID = ae.SceneID
WHERE nso.ProjectID = @ProjectID AND (@BookID IS NULL OR nso.BookID = @BookID)
GROUP BY ae.StoryAssetID
)
SELECT sa.StoryAssetID, sa.AssetName, ak.KindName, sa.Importance, sa.CurrentStateID,
ast.StateName AS CurrentStateName, sa.CurrentLocationID, location.LocationPath AS CurrentLocationPath,
sa.IsResolved, LastAssetEvent.LastEventSceneID, LastAssetEvent.LastEventSceneLabel,
CASE WHEN EXISTS (SELECT 1 FROM dbo.AssetDependencies ad WHERE ad.IsArchived = 0 AND (ad.SourceAssetID = sa.StoryAssetID OR ad.TargetAssetID = sa.StoryAssetID)) THEN 1 ELSE 0 END AS HasDependencies,
CASE WHEN EXISTS (
SELECT 1 FROM dbo.ContinuityWarnings cw
INNER JOIN dbo.WarningTypes wt ON wt.WarningTypeID = cw.WarningTypeID
WHERE cw.ProjectID = @ProjectID AND cw.EntityType IN (N'StoryAsset', N'AssetDependency') AND cw.EntityID IN (sa.StoryAssetID)
AND wt.TypeName LIKE N'Asset%' AND cw.IsDismissed = 0 AND cw.IsIntentional = 0
) THEN 1 ELSE 0 END AS HasDependencyWarnings
FROM dbo.StoryAssets sa
INNER JOIN dbo.AssetKinds ak ON ak.AssetKindID = sa.AssetKindID
LEFT JOIN dbo.AssetStates ast ON ast.AssetStateID = sa.CurrentStateID
LEFT JOIN dbo.LocationPaths location ON location.LocationID = sa.CurrentLocationID
LEFT JOIN LastAssetEvent ON LastAssetEvent.StoryAssetID = sa.StoryAssetID
WHERE sa.ProjectID = @ProjectID AND sa.IsArchived = 0
ORDER BY sa.IsResolved, sa.Importance DESC, sa.AssetName;
SELECT ws.SeverityName, COUNT(*) AS WarningCount
FROM dbo.ContinuityWarnings cw
INNER JOIN dbo.WarningSeverities ws ON ws.WarningSeverityID = cw.WarningSeverityID
WHERE cw.ProjectID = @ProjectID AND (@BookID IS NULL OR cw.BookID = @BookID)
AND cw.IsDismissed = 0 AND cw.IsIntentional = 0
GROUP BY ws.SeverityName, ws.SortOrder
ORDER BY ws.SortOrder DESC;
SELECT TOP (12) wt.TypeName AS WarningTypeName, COUNT(*) AS WarningCount
FROM dbo.ContinuityWarnings cw
INNER JOIN dbo.WarningTypes wt ON wt.WarningTypeID = cw.WarningTypeID
WHERE cw.ProjectID = @ProjectID AND (@BookID IS NULL OR cw.BookID = @BookID)
AND cw.IsDismissed = 0 AND cw.IsIntentional = 0
GROUP BY wt.TypeName
ORDER BY COUNT(*) DESC, wt.TypeName;
SELECT TOP (12) cw.SceneID, s.SceneNumber, s.SceneTitle, c.ChapterNumber, b.BookTitle,
COUNT(*) AS WarningCount, MAX(ws.SortOrder) AS HighestSeveritySort
FROM dbo.ContinuityWarnings cw
INNER JOIN dbo.WarningSeverities ws ON ws.WarningSeverityID = cw.WarningSeverityID
INNER JOIN dbo.Scenes s ON s.SceneID = cw.SceneID
INNER JOIN dbo.Chapters c ON c.ChapterID = s.ChapterID
INNER JOIN dbo.Books b ON b.BookID = c.BookID
WHERE cw.ProjectID = @ProjectID AND cw.SceneID IS NOT NULL AND (@BookID IS NULL OR cw.BookID = @BookID)
AND cw.IsDismissed = 0 AND cw.IsIntentional = 0
GROUP BY cw.SceneID, s.SceneNumber, s.SceneTitle, c.ChapterNumber, b.BookTitle
ORDER BY COUNT(*) DESC, MAX(ws.SortOrder) DESC;
;WITH ScopeScenes AS
(
SELECT nso.SceneID, nso.BookID, nso.BookTitle, nso.ChapterID, nso.ChapterNumber, nso.SceneNumber, nso.SceneTitle,
COALESCE(s.POVCharacterID, c.POVCharacterID) AS POVCharacterID
FROM dbo.NarrativeSceneOrder nso
INNER JOIN dbo.Scenes s ON s.SceneID = nso.SceneID
INNER JOIN dbo.Chapters c ON c.ChapterID = nso.ChapterID
WHERE nso.ProjectID = @ProjectID AND (@BookID IS NULL OR nso.BookID = @BookID)
AND (@StartChapterNumber IS NULL OR nso.ChapterNumber >= @StartChapterNumber)
AND (@EndChapterNumber IS NULL OR nso.ChapterNumber <= @EndChapterNumber)
AND (@StartSceneNumber IS NULL OR nso.SceneNumber >= @StartSceneNumber)
AND (@EndSceneNumber IS NULL OR nso.SceneNumber <= @EndSceneNumber)
)
SELECT COALESCE(ch.CharacterName, N'No POV') AS POVName, ss.POVCharacterID, COUNT(*) AS SceneCount
FROM ScopeScenes ss
LEFT JOIN dbo.Characters ch ON ch.CharacterID = ss.POVCharacterID
GROUP BY ss.POVCharacterID, ch.CharacterName
ORDER BY COUNT(*) DESC, POVName;
;WITH ScopeScenes AS
(
SELECT nso.SceneID, nso.ChapterID
FROM dbo.NarrativeSceneOrder nso
WHERE nso.ProjectID = @ProjectID AND (@BookID IS NULL OR nso.BookID = @BookID)
)
SELECT c.ChapterID, c.ChapterNumber, c.ChapterTitle, COUNT(DISTINCT COALESCE(s.POVCharacterID, c.POVCharacterID)) AS POVCount
FROM dbo.Chapters c
INNER JOIN dbo.Scenes s ON s.ChapterID = c.ChapterID
WHERE c.ChapterID IN (SELECT ChapterID FROM ScopeScenes)
GROUP BY c.ChapterID, c.ChapterNumber, c.ChapterTitle
HAVING COUNT(DISTINCT COALESCE(s.POVCharacterID, c.POVCharacterID)) > 1
ORDER BY c.ChapterNumber;
SELECT ch.CharacterID, ch.CharacterName, COUNT(DISTINCT sc.SceneID) AS SceneCount
FROM dbo.Characters ch
LEFT JOIN dbo.SceneCharacters sc ON sc.CharacterID = ch.CharacterID
LEFT JOIN dbo.NarrativeSceneOrder nso ON nso.SceneID = sc.SceneID
WHERE ch.ProjectID = @ProjectID AND ch.IsArchived = 0 AND (@BookID IS NULL OR nso.BookID = @BookID OR sc.SceneID IS NULL)
GROUP BY ch.CharacterID, ch.CharacterName
ORDER BY COUNT(DISTINCT sc.SceneID) DESC, ch.CharacterName;
;WITH SceneCharacterCounts AS
(
SELECT nso.SceneID, nso.SceneNumber, nso.SceneTitle, nso.ChapterNumber, COUNT(sc.SceneCharacterID) AS CharacterCount
FROM dbo.NarrativeSceneOrder nso
LEFT JOIN dbo.SceneCharacters sc ON sc.SceneID = nso.SceneID
WHERE nso.ProjectID = @ProjectID AND (@BookID IS NULL OR nso.BookID = @BookID)
GROUP BY nso.SceneID, nso.SceneNumber, nso.SceneTitle, nso.ChapterNumber
)
SELECT TOP (12) * FROM SceneCharacterCounts WHERE CharacterCount >= 5 ORDER BY CharacterCount DESC, ChapterNumber, SceneNumber;
;WITH ScopeScenes AS
(
SELECT nso.SceneID, s.PrimaryLocationID
FROM dbo.NarrativeSceneOrder nso
INNER JOIN dbo.Scenes s ON s.SceneID = nso.SceneID
WHERE nso.ProjectID = @ProjectID AND (@BookID IS NULL OR nso.BookID = @BookID)
)
SELECT COALESCE(lp.LocationName, N'No location') AS LocationName, lp.LocationID, COALESCE(lp.LocationPath, N'No location') AS LocationPath,
COUNT(*) AS SceneCount
FROM ScopeScenes ss
LEFT JOIN dbo.LocationPaths lp ON lp.LocationID = ss.PrimaryLocationID
GROUP BY lp.LocationID, lp.LocationName, lp.LocationPath
ORDER BY COUNT(*) DESC, LocationName;
END;
GO

View File

@ -0,0 +1,737 @@
using System.ComponentModel.DataAnnotations;
using Microsoft.AspNetCore.Mvc.Rendering;
using PlotLine.Models;
namespace PlotLine.ViewModels;
public sealed class ProjectEditViewModel
{
public int ProjectID { get; set; }
[Required, StringLength(200)]
[Display(Name = "Project / Series name")]
public string ProjectName { get; set; } = string.Empty;
public string? Description { get; set; }
}
public sealed class ProjectDetailViewModel
{
public Project Project { get; set; } = new();
public IReadOnlyList<Book> Books { get; set; } = [];
}
public sealed class BookEditViewModel
{
public int BookID { get; set; }
public int ProjectID { get; set; }
[Required, StringLength(200)]
[Display(Name = "Book title")]
public string BookTitle { get; set; } = string.Empty;
[Display(Name = "Book number")]
public int BookNumber { get; set; } = 1;
public string? Description { get; set; }
public Project? Project { get; set; }
}
public sealed class BookDetailViewModel
{
public Project Project { get; set; } = new();
public Book Book { get; set; } = new();
public IReadOnlyList<Chapter> Chapters { get; set; } = [];
}
public sealed class ChapterEditViewModel
{
public int ChapterID { get; set; }
public int BookID { get; set; }
[Display(Name = "Chapter number")]
public decimal ChapterNumber { get; set; } = 1;
[Required, StringLength(200)]
[Display(Name = "Chapter title")]
public string ChapterTitle { get; set; } = string.Empty;
public string? Summary { get; set; }
[Display(Name = "Revision status")]
public int RevisionStatusID { get; set; }
public Book? Book { get; set; }
public Project? Project { get; set; }
public IReadOnlyList<SelectListItem> RevisionStatuses { get; set; } = [];
}
public sealed class ChapterDetailViewModel
{
public Project Project { get; set; } = new();
public Book Book { get; set; } = new();
public Chapter Chapter { get; set; } = new();
public IReadOnlyList<Scene> Scenes { get; set; } = [];
}
public sealed class SceneEditViewModel
{
public int SceneID { get; set; }
public int ChapterID { get; set; }
[Display(Name = "Scene number")]
public decimal SceneNumber { get; set; } = 1;
[Required, StringLength(200)]
[Display(Name = "Scene title")]
public string SceneTitle { get; set; } = string.Empty;
public string? Summary { get; set; }
[Display(Name = "Time mode")]
public int TimeModeID { get; set; }
[Display(Name = "Start date/time")]
public DateTime? StartDateTime { get; set; }
[Display(Name = "End date/time")]
public DateTime? EndDateTime { get; set; }
[Display(Name = "Duration")]
public decimal? DurationAmount { get; set; }
[Display(Name = "Duration unit")]
public int? DurationUnitID { get; set; }
[Display(Name = "Relative time")]
public string? RelativeTimeText { get; set; }
[Display(Name = "Time confidence")]
public int TimeConfidenceID { get; set; }
[Display(Name = "Purpose notes")]
public string? ScenePurposeNotes { get; set; }
[Display(Name = "Outcome notes")]
public string? SceneOutcomeNotes { get; set; }
[Display(Name = "Revision status")]
public int RevisionStatusID { get; set; }
[Display(Name = "Primary location")]
public int? PrimaryLocationID { get; set; }
[Display(Name = "Scene purposes")]
public List<int> SelectedPurposeTypeIds { get; set; } = [];
public List<SceneMetricInputViewModel> Metrics { get; set; } = [];
public IReadOnlyList<ThreadEvent> ThreadEvents { get; set; } = [];
public ThreadEventCreateViewModel NewThreadEvent { get; set; } = new();
public IReadOnlyList<AssetEvent> AssetEvents { get; set; } = [];
public IReadOnlyList<AssetCustodyEvent> AssetCustodyEvents { get; set; } = [];
public IReadOnlyList<SceneAssetLocation> SceneAssetLocations { get; set; } = [];
public AssetEventCreateViewModel NewAssetEvent { get; set; } = new();
public AssetCustodyCreateViewModel NewAssetCustodyEvent { get; set; } = new();
public SceneAssetLocationCreateViewModel NewSceneAssetLocation { get; set; } = new();
public IReadOnlyList<SceneCharacter> SceneCharacters { get; set; } = [];
public IReadOnlyList<CharacterAttributeEvent> CharacterAttributeEvents { get; set; } = [];
public IReadOnlyList<CharacterKnowledgeItem> CharacterKnowledge { get; set; } = [];
public IReadOnlyList<RelationshipEvent> RelationshipEvents { get; set; } = [];
public SceneCharacterCreateViewModel NewSceneCharacter { get; set; } = new();
public CharacterAttributeEventCreateViewModel NewCharacterAttributeEvent { get; set; } = new();
public CharacterKnowledgeCreateViewModel NewCharacterKnowledge { get; set; } = new();
public RelationshipEventCreateViewModel NewRelationshipEvent { get; set; } = new();
public IReadOnlyList<SelectListItem> PlotLineOptions { get; set; } = [];
public IReadOnlyList<SelectListItem> PlotThreadOptions { get; set; } = [];
public IReadOnlyList<SelectListItem> ThreadTypeOptions { get; set; } = [];
public IReadOnlyList<SelectListItem> ThreadEventTypeOptions { get; set; } = [];
public IReadOnlyList<SelectListItem> AssetOptions { get; set; } = [];
public IReadOnlyList<SelectListItem> AssetKindOptions { get; set; } = [];
public IReadOnlyList<SelectListItem> AssetStateOptions { get; set; } = [];
public IReadOnlyList<SelectListItem> AssetEventTypeOptions { get; set; } = [];
public IReadOnlyList<SelectListItem> AssetCustodyEventTypeOptions { get; set; } = [];
public IReadOnlyList<SelectListItem> CustodyRoleOptions { get; set; } = [];
public IReadOnlyList<SelectListItem> CharacterOptions { get; set; } = [];
public IReadOnlyList<SelectListItem> CharacterRoleOptions { get; set; } = [];
public IReadOnlyList<SelectListItem> PresenceTypeOptions { get; set; } = [];
public IReadOnlyList<SelectListItem> CharacterAttributeTypeOptions { get; set; } = [];
public IReadOnlyList<SelectListItem> KnowledgeStateOptions { get; set; } = [];
public IReadOnlyList<SelectListItem> RelationshipOptions { get; set; } = [];
public IReadOnlyList<SelectListItem> RelationshipTypeOptions { get; set; } = [];
public IReadOnlyList<SelectListItem> RelationshipStateOptions { get; set; } = [];
public IReadOnlyList<SelectListItem> LocationOptions { get; set; } = [];
public IReadOnlyList<SelectListItem> SceneOptions { get; set; } = [];
public IReadOnlyList<SelectListItem> ChapterOptions { get; set; } = [];
public IReadOnlyList<SelectListItem> SceneDependencyTypeOptions { get; set; } = [];
public IReadOnlyList<string> AssetDependencyMessages { get; set; } = [];
public IReadOnlyList<string> LocationConsistencyMessages { get; set; } = [];
public IReadOnlyList<ContinuityWarning> Warnings { get; set; } = [];
public IReadOnlyList<SceneDependency> DependenciesThisSceneNeeds { get; set; } = [];
public IReadOnlyList<SceneDependency> DependenciesNeedingThisScene { get; set; } = [];
public SceneDependencyCreateViewModel NewDependency { get; set; } = new();
public SceneMoveRequestViewModel MoveRequest { get; set; } = new();
public int? ReturnProjectID { get; set; }
public int? ReturnBookID { get; set; }
public bool ReturnToTimeline { get; set; }
public Chapter? Chapter { get; set; }
public Book? Book { get; set; }
public Project? Project { get; set; }
public IReadOnlyList<SelectListItem> RevisionStatuses { get; set; } = [];
public IReadOnlyList<SelectListItem> TimeModes { get; set; } = [];
public IReadOnlyList<SelectListItem> DurationUnits { get; set; } = [];
public IReadOnlyList<SelectListItem> TimeConfidences { get; set; } = [];
public IReadOnlyList<ScenePurposeType> ScenePurposeTypes { get; set; } = [];
}
public sealed class TimelineViewModel
{
public Project Project { get; set; } = new();
public int? SelectedBookID { get; set; }
public int? SelectedSceneID { get; set; }
public SceneEditViewModel? SelectedScene { get; set; }
public IReadOnlyList<BookTimelineViewModel> Books { get; set; } = [];
public IReadOnlyList<SelectListItem> BookOptions { get; set; } = [];
public IReadOnlyList<Scene> OrderedScenes { get; set; } = [];
public IReadOnlyList<MetricGraphViewModel> MetricGraphs { get; set; } = [];
public IReadOnlyList<PlotLineLaneViewModel> PlotLanes { get; set; } = [];
public IReadOnlyList<AssetLaneViewModel> AssetLanes { get; set; } = [];
public IReadOnlyList<CharacterLaneViewModel> CharacterLanes { get; set; } = [];
public ContinuityValidationSummary? LastValidationSummary { get; set; }
}
public sealed class BookTimelineViewModel
{
public Book Book { get; set; } = new();
public IReadOnlyList<ChapterTimelineViewModel> Chapters { get; set; } = [];
}
public sealed class ChapterTimelineViewModel
{
public Chapter Chapter { get; set; } = new();
public IReadOnlyList<Scene> Scenes { get; set; } = [];
}
public sealed class SceneMetricInputViewModel
{
public int MetricTypeID { get; set; }
public string MetricName { get; set; } = string.Empty;
public string? Description { get; set; }
public int MinValue { get; set; }
public int MaxValue { get; set; }
public int Value { get; set; }
public string? Notes { get; set; }
}
public sealed class MetricGraphViewModel
{
public int MetricTypeID { get; set; }
public string MetricName { get; set; } = string.Empty;
public IReadOnlyList<MetricGraphPointViewModel> Points { get; set; } = [];
}
public sealed class MetricGraphPointViewModel
{
public int SceneID { get; set; }
public decimal SceneNumber { get; set; }
public string SceneTitle { get; set; } = string.Empty;
public int Value { get; set; }
public int Percent { get; set; }
}
public sealed class PlotLineEditViewModel
{
public int PlotLineID { get; set; }
public int ProjectID { get; set; }
[Required, StringLength(200)]
[Display(Name = "Plot line name")]
public string PlotLineName { get; set; } = string.Empty;
[Display(Name = "Type")]
public int PlotLineTypeID { get; set; }
public int? BookID { get; set; }
public string? Description { get; set; }
[Display(Name = "Parent plot line")]
public int? ParentPlotLineID { 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> BookOptions { get; set; } = [];
public IReadOnlyList<SelectListItem> ParentPlotLineOptions { get; set; } = [];
}
public sealed class PlotLineListViewModel
{
public Project Project { get; set; } = new();
public IReadOnlyList<PlotLineItem> PlotLines { get; set; } = [];
}
public sealed class PlotThreadEditViewModel
{
public int PlotThreadID { get; set; }
public int ProjectID { get; set; }
[Display(Name = "Plot line")]
public int PlotLineID { get; set; }
[Required, StringLength(200)]
[Display(Name = "Thread title")]
public string ThreadTitle { get; set; } = string.Empty;
[Display(Name = "Type")]
public int ThreadTypeID { get; set; }
[Display(Name = "Status")]
public int ThreadStatusID { get; set; }
[Range(1, 10)]
public int Importance { get; set; } = 5;
public string? Summary { get; set; }
[Display(Name = "Introduced scene")]
public int? IntroducedSceneID { get; set; }
[Display(Name = "Planned resolution scene")]
public int? PlannedResolutionSceneID { get; set; }
[Display(Name = "Actual resolution scene")]
public int? ActualResolutionSceneID { get; set; }
public Project? Project { get; set; }
public IReadOnlyList<SelectListItem> PlotLineOptions { get; set; } = [];
public IReadOnlyList<SelectListItem> ThreadTypeOptions { get; set; } = [];
public IReadOnlyList<SelectListItem> ThreadStatusOptions { get; set; } = [];
public IReadOnlyList<SelectListItem> SceneOptions { get; set; } = [];
}
public sealed class PlotThreadListViewModel
{
public Project Project { get; set; } = new();
public IReadOnlyList<PlotThread> PlotThreads { get; set; } = [];
}
public sealed class ThreadEventCreateViewModel
{
public int SceneID { get; set; }
public int? PlotLineID { get; set; }
public int? PlotThreadID { get; set; }
public string? NewThreadTitle { get; set; }
public int? NewThreadTypeID { get; set; }
public int EventTypeID { get; set; }
public string EventTitle { get; set; } = string.Empty;
public string? EventDescription { get; set; }
public int? ReturnProjectID { get; set; }
public int? ReturnBookID { get; set; }
}
public sealed class PlotLineLaneViewModel
{
public PlotLineItem PlotLine { get; set; } = new();
public IReadOnlyList<PlotLaneSceneViewModel> SceneSlots { get; set; } = [];
}
public sealed class PlotLaneSceneViewModel
{
public Scene Scene { get; set; } = new();
public IReadOnlyList<ThreadEvent> Events { get; set; } = [];
}
public sealed class StoryAssetListViewModel
{
public Project Project { get; set; } = new();
public IReadOnlyList<StoryAsset> Assets { get; set; } = [];
}
public sealed class StoryAssetEditViewModel
{
public int StoryAssetID { get; set; }
public int ProjectID { get; set; }
[Required, StringLength(200)]
[Display(Name = "Asset name")]
public string AssetName { get; set; } = string.Empty;
[Display(Name = "Kind")]
public int AssetKindID { get; set; }
public string? Description { get; set; }
[Range(1, 10)]
public int Importance { get; set; } = 5;
[Display(Name = "Current state")]
public int? CurrentStateID { get; set; }
[Display(Name = "Current location")]
public int? CurrentLocationID { get; set; }
[Display(Name = "Resolved")]
public bool IsResolved { get; set; }
public Project? Project { get; set; }
public IReadOnlyList<SelectListItem> AssetKindOptions { get; set; } = [];
public IReadOnlyList<SelectListItem> AssetStateOptions { get; set; } = [];
public IReadOnlyList<SelectListItem> LocationOptions { get; set; } = [];
}
public sealed class StoryAssetDetailViewModel
{
public Project Project { get; set; } = new();
public StoryAsset Asset { get; set; } = new();
public IReadOnlyList<StoryAsset> ProjectAssets { get; set; } = [];
public IReadOnlyList<AssetEvent> Events { get; set; } = [];
public IReadOnlyList<AssetDependency> Dependencies { get; set; } = [];
public IReadOnlyList<AssetCustodyEvent> CustodyEvents { get; set; } = [];
public AssetDependencyEditViewModel NewDependency { get; set; } = new();
public IReadOnlyList<SelectListItem> AssetOptions { get; set; } = [];
public IReadOnlyList<SelectListItem> AssetDependencyTypeOptions { get; set; } = [];
public IReadOnlyList<SelectListItem> AssetStateOptions { get; set; } = [];
}
public sealed class AssetEventCreateViewModel
{
public int SceneID { get; set; }
public int? StoryAssetID { get; set; }
public string? NewAssetName { get; set; }
public int? NewAssetKindID { get; set; }
public int AssetEventTypeID { get; set; }
public int? FromStateID { get; set; }
public int? ToStateID { get; set; }
public string EventTitle { get; set; } = string.Empty;
public string? EventDescription { get; set; }
public int? ReturnProjectID { get; set; }
public int? ReturnBookID { get; set; }
}
public sealed class AssetCustodyCreateViewModel
{
public int SceneID { get; set; }
public int StoryAssetID { get; set; }
public int AssetCustodyEventTypeID { get; set; }
public int CustodyRoleID { get; set; }
public List<int> CustodianCharacterIds { get; set; } = [];
public string CustodianNames { get; set; } = string.Empty;
public string? Description { get; set; }
public int? ReturnProjectID { get; set; }
public int? ReturnBookID { get; set; }
}
public sealed class SceneAssetLocationCreateViewModel
{
public int SceneAssetLocationID { get; set; }
public int SceneID { get; set; }
public int StoryAssetID { get; set; }
public int LocationID { get; set; }
public string? Description { get; set; }
public bool UpdateCurrentLocation { get; set; } = true;
public int? ReturnProjectID { get; set; }
public int? ReturnBookID { get; set; }
}
public sealed class AssetDependencyEditViewModel
{
public int AssetDependencyID { get; set; }
public int ProjectID { get; set; }
public int SourceAssetID { get; set; }
public int TargetAssetID { get; set; }
public int AssetDependencyTypeID { get; set; }
public int? SourceRequiredStateID { get; set; }
public int? TargetBlockedStateID { get; set; }
public string? Description { get; set; }
}
public sealed class AssetLaneViewModel
{
public StoryAsset Asset { get; set; } = new();
public IReadOnlyList<AssetLaneSceneViewModel> SceneSlots { get; set; } = [];
}
public sealed class AssetLaneSceneViewModel
{
public Scene Scene { get; set; } = new();
public IReadOnlyList<AssetEvent> Events { get; set; } = [];
}
public sealed class CharacterListViewModel
{
public Project Project { get; set; } = new();
public IReadOnlyList<Character> Characters { get; set; } = [];
}
public sealed class CharacterEditViewModel
{
public int CharacterID { get; set; }
public int ProjectID { get; set; }
[Required, StringLength(200)]
[Display(Name = "Character name")]
public string CharacterName { get; set; } = string.Empty;
[Display(Name = "Short name")]
public string? ShortName { get; set; }
public string? Sex { get; set; }
[Display(Name = "Birth date")]
public DateTime? BirthDate { get; set; }
[Display(Name = "Age at series start")]
public int? AgeAtSeriesStart { get; set; }
public string? Height { get; set; }
[Display(Name = "Eye colour")]
public string? EyeColour { get; set; }
[Display(Name = "Default description")]
public string? DefaultDescription { get; set; }
public Project? Project { get; set; }
}
public sealed class CharacterDetailViewModel
{
public Project Project { get; set; } = new();
public Character Character { get; set; } = new();
public string DisplayAge { get; set; } = "Unknown";
public IReadOnlyList<SceneCharacter> Appearances { get; set; } = [];
public IReadOnlyList<CharacterAttributeEvent> AttributeEvents { get; set; } = [];
public IReadOnlyList<CharacterKnowledgeItem> KnowledgeItems { get; set; } = [];
public IReadOnlyList<CharacterRelationship> Relationships { get; set; } = [];
public IReadOnlyList<RelationshipEvent> RelationshipEvents { get; set; } = [];
}
public sealed class SceneCharacterCreateViewModel
{
public int SceneID { get; set; }
public int? CharacterID { get; set; }
public string? NewCharacterName { get; set; }
public int? RoleInSceneTypeID { get; set; }
public int? PresenceTypeID { get; set; }
public int? LocationID { get; set; }
public int? EntryLocationID { get; set; }
public int? ExitLocationID { get; set; }
public string? AppearanceNotes { get; set; }
public string? OutfitDescription { get; set; }
public string? PhysicalCondition { get; set; }
public string? EmotionalState { get; set; }
public string? KnowledgeNotes { get; set; }
public int? ReturnProjectID { get; set; }
public int? ReturnBookID { get; set; }
}
public sealed class CharacterAttributeEventCreateViewModel
{
public int SceneID { get; set; }
public int CharacterID { get; set; }
public int CharacterAttributeTypeID { get; set; }
public string AttributeValue { get; set; } = string.Empty;
public string? Description { get; set; }
public int? ReturnProjectID { get; set; }
public int? ReturnBookID { get; set; }
}
public sealed class CharacterKnowledgeCreateViewModel
{
public int SceneID { get; set; }
public int CharacterID { get; set; }
public int? StoryAssetID { get; set; }
public int? PlotThreadID { get; set; }
public int KnowledgeStateID { get; set; }
public string? Description { get; set; }
public int? ReturnProjectID { get; set; }
public int? ReturnBookID { get; set; }
}
public sealed class RelationshipEventCreateViewModel
{
public int SceneID { get; set; }
public int? CharacterRelationshipID { get; set; }
public int? CharacterAID { get; set; }
public int? CharacterBID { get; set; }
public int? RelationshipTypeID { get; set; }
public int RelationshipStateID { get; set; }
public int? Intensity { get; set; }
public bool IsKnownToOtherCharacter { get; set; }
public string? Description { get; set; }
public int? ReturnProjectID { get; set; }
public int? ReturnBookID { get; set; }
}
public sealed class CharacterLaneViewModel
{
public Character Character { get; set; } = new();
public IReadOnlyList<CharacterLaneSceneViewModel> SceneSlots { get; set; } = [];
}
public sealed class CharacterLaneSceneViewModel
{
public Scene Scene { get; set; } = new();
public IReadOnlyList<SceneCharacter> Appearances { get; set; } = [];
}
public sealed class LocationListViewModel
{
public Project Project { get; set; } = new();
public IReadOnlyList<LocationItem> Locations { get; set; } = [];
}
public sealed class LocationEditViewModel
{
public int LocationID { get; set; }
public int ProjectID { get; set; }
public int? ParentLocationID { get; set; }
[Required, StringLength(200)]
[Display(Name = "Location name")]
public string LocationName { get; set; } = string.Empty;
[Display(Name = "Location type")]
public int? LocationTypeID { get; set; }
public string? Description { get; set; }
public Project? Project { get; set; }
public IReadOnlyList<SelectListItem> ParentLocationOptions { get; set; } = [];
public IReadOnlyList<SelectListItem> LocationTypeOptions { get; set; } = [];
}
public sealed class LocationDetailViewModel
{
public Project Project { get; set; } = new();
public LocationItem Location { get; set; } = new();
public IReadOnlyList<LocationItem> ChildLocations { get; set; } = [];
public IReadOnlyList<LocationRelationship> Relationships { get; set; } = [];
public IReadOnlyList<Scene> Scenes { get; set; } = [];
public IReadOnlyList<SceneCharacter> Characters { get; set; } = [];
public IReadOnlyList<SceneAssetLocation> Assets { get; set; } = [];
public LocationRelationshipEditViewModel NewRelationship { get; set; } = new();
public IReadOnlyList<SelectListItem> LocationOptions { get; set; } = [];
public IReadOnlyList<SelectListItem> RelationshipTypeOptions { get; set; } = [];
}
public sealed class LocationRelationshipEditViewModel
{
public int LocationRelationshipID { get; set; }
public int ProjectID { get; set; }
public int FromLocationID { get; set; }
public int ToLocationID { get; set; }
public int LocationRelationshipTypeID { get; set; }
public bool IsBidirectional { get; set; }
public int? Strength { get; set; }
public bool CanHearNormalSpeech { get; set; }
public bool CanHearLoudNoise { get; set; }
public bool CanSeeMovement { get; set; }
public bool CanSeeClearly { get; set; }
public bool CanTravelDirectly { get; set; }
public string? Notes { get; set; }
}
public sealed class WarningDashboardViewModel
{
public Project Project { get; set; } = new();
public int? BookID { get; set; }
public int? SceneID { get; set; }
public int? WarningTypeID { get; set; }
public int? WarningSeverityID { get; set; }
public string? EntityType { get; set; }
public bool IncludeDismissed { get; set; }
public bool IncludeIntentional { get; set; }
public IReadOnlyList<ContinuityWarning> Warnings { get; set; } = [];
public IReadOnlyList<SelectListItem> BookOptions { get; set; } = [];
public IReadOnlyList<SelectListItem> WarningTypeOptions { get; set; } = [];
public IReadOnlyList<SelectListItem> SeverityOptions { get; set; } = [];
public IReadOnlyList<SelectListItem> EntityTypeOptions { get; set; } = [];
public IReadOnlyDictionary<string, int> CountsBySeverity { get; set; } = new Dictionary<string, int>();
public IReadOnlyDictionary<string, int> CountsByType { get; set; } = new Dictionary<string, int>();
public ContinuityValidationSummary? LastValidationSummary { get; set; }
}
public sealed class SceneDependencyCreateViewModel
{
public int SceneDependencyID { get; set; }
public int ProjectID { get; set; }
public int CurrentSceneID { get; set; }
public int RelatedSceneID { get; set; }
public string Direction { get; set; } = "CurrentDependsOnRelated";
public int SceneDependencyTypeID { get; set; }
public bool IsHardDependency { get; set; }
public string? Description { get; set; }
public int? ReturnProjectID { get; set; }
public int? ReturnBookID { get; set; }
}
public sealed class SceneMoveRequestViewModel
{
public int SceneID { get; set; }
public int TargetChapterID { get; set; }
public string Position { get; set; } = "End";
public bool RenumberScenes { get; set; }
public int? ReturnProjectID { get; set; }
public int? ReturnBookID { get; set; }
}
public sealed class SceneMovePreviewViewModel
{
public SceneMovePreview Preview { get; set; } = new();
public SceneMoveRequestViewModel Request { get; set; } = new();
public IReadOnlyList<ContinuityWarning> CurrentWarnings { get; set; } = [];
}
public sealed class AnalyticsViewModel
{
public Project Project { get; set; } = new();
public int? BookID { get; set; }
public decimal? StartChapterNumber { get; set; }
public decimal? EndChapterNumber { get; set; }
public decimal? StartSceneNumber { get; set; }
public decimal? EndSceneNumber { get; set; }
public IReadOnlyList<SelectListItem> BookOptions { get; set; } = [];
public IReadOnlyList<AnalyticsMetricGraphViewModel> MetricGraphs { get; set; } = [];
public IReadOnlyList<AnalyticsRevisionSummary> RevisionSummary { get; set; } = [];
public IReadOnlyList<AnalyticsRevisionScene> RevisionAttentionScenes { get; set; } = [];
public IReadOnlyList<AnalyticsPurposeSummary> PurposeSummary { get; set; } = [];
public IReadOnlyList<AnalyticsSceneGap> SceneGaps { get; set; } = [];
public IReadOnlyList<AnalyticsThreadHealth> ThreadHealth { get; set; } = [];
public IReadOnlyList<AnalyticsAssetHealth> AssetHealth { get; set; } = [];
public IReadOnlyList<AnalyticsWarningSeverityCount> WarningSeverityCounts { get; set; } = [];
public IReadOnlyList<AnalyticsWarningTypeCount> WarningTypeCounts { get; set; } = [];
public IReadOnlyList<AnalyticsWarningHotspot> WarningHotspots { get; set; } = [];
public IReadOnlyList<AnalyticsPovSummary> PovSummary { get; set; } = [];
public IReadOnlyList<AnalyticsMultiPovChapter> MultiPovChapters { get; set; } = [];
public IReadOnlyList<AnalyticsCharacterAppearance> CharacterAppearances { get; set; } = [];
public IReadOnlyList<AnalyticsBusyScene> BusyScenes { get; set; } = [];
public IReadOnlyList<AnalyticsLocationUsage> LocationUsage { get; set; } = [];
public IReadOnlyList<AnalyticsReviewNote> PacingReviewNotes { get; set; } = [];
}
public sealed class AnalyticsMetricGraphViewModel
{
public string MetricName { get; set; } = string.Empty;
public IReadOnlyList<AnalyticsMetricPoint> Points { get; set; } = [];
}
public sealed class AnalyticsReviewNote
{
public string MetricName { get; set; } = string.Empty;
public string SceneRange { get; set; } = string.Empty;
public string ValuePattern { get; set; } = string.Empty;
public string ReviewNote { get; set; } = string.Empty;
public int? SceneID { get; set; }
}

View File

@ -0,0 +1,303 @@
@model AnalyticsViewModel
@{
ViewData["Title"] = "Story Health";
var openThreads = Model.ThreadHealth.Where(x => x.IsOpenStatus).ToList();
var dormantThreads = Model.ThreadHealth.Where(x => x.IsDormant).ToList();
var unresolvedAssets = Model.AssetHealth.Where(x => !x.IsResolved).ToList();
var metricScenes = Model.MetricGraphs
.SelectMany(graph => graph.Points)
.GroupBy(point => point.SceneID)
.Select(group => group.OrderBy(point => point.GlobalSceneIndex).First())
.OrderBy(point => point.GlobalSceneIndex)
.Select(point => new
{
sceneId = point.SceneID,
axisLabel = $"{point.ChapterNumber:g}.{point.SceneNumber:g}",
chapterLabel = $"Chapter {point.ChapterNumber:g}",
sceneNumber = point.SceneNumber.ToString("g"),
sceneTitle = point.SceneTitle,
bookTitle = point.BookTitle,
timelineUrl = Url.Action("Index", "Timeline", new { projectId = Model.Project.ProjectID, bookId = point.BookID, selectedSceneId = point.SceneID })
})
.ToList();
var metricShapePayload = new
{
scenes = metricScenes,
metrics = Model.MetricGraphs.Select(graph => new
{
metricName = graph.MetricName,
points = graph.Points.Select(point => new { sceneId = point.SceneID, value = point.Value })
})
};
}
<section class="context-header">
<div class="context-title">
<span class="eyebrow">Workspace context</span>
<strong>@Model.Project.ProjectName</strong>
</div>
<nav class="context-nav" aria-label="Workspace navigation">
<a asp-controller="Projects" asp-action="Details" asp-route-id="@Model.Project.ProjectID">Project</a>
<a asp-controller="Timeline" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID" asp-route-bookId="@Model.BookID">Timeline</a>
<a asp-controller="Warnings" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID" asp-route-bookId="@Model.BookID">Warnings</a>
<a asp-controller="Characters" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID">Characters</a>
<a asp-controller="StoryAssets" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID">Assets</a>
<a asp-controller="Locations" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID">Locations</a>
<a asp-controller="PlotThreads" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID">Threads</a>
</nav>
</section>
<div class="page-heading compact">
<div>
<p class="eyebrow">Analytics / Story health</p>
<h1>@Model.Project.ProjectName</h1>
</div>
<div class="button-row">
<a class="btn btn-outline-primary" asp-controller="Timeline" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID" asp-route-bookId="@Model.BookID">Timeline</a>
<a class="btn btn-outline-secondary" asp-controller="Warnings" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID" asp-route-bookId="@Model.BookID">Warnings</a>
</div>
</div>
<section class="warning-filter-panel">
<form method="get" class="row g-2 align-items-end">
<input type="hidden" name="projectId" value="@Model.Project.ProjectID" />
<div class="col-md-3">
<label class="form-label">Book</label>
<select name="bookId" asp-items="Model.BookOptions" class="form-select form-select-sm">
<option value="">All books</option>
</select>
</div>
<div class="col-md-2">
<label class="form-label">Start chapter</label>
<input class="form-control form-control-sm" name="startChapterNumber" value="@Model.StartChapterNumber" />
</div>
<div class="col-md-2">
<label class="form-label">End chapter</label>
<input class="form-control form-control-sm" name="endChapterNumber" value="@Model.EndChapterNumber" />
</div>
<div class="col-md-2">
<label class="form-label">Start scene</label>
<input class="form-control form-control-sm" name="startSceneNumber" value="@Model.StartSceneNumber" />
</div>
<div class="col-md-2">
<label class="form-label">End scene</label>
<input class="form-control form-control-sm" name="endSceneNumber" value="@Model.EndSceneNumber" />
</div>
<div class="col-md-1">
<button class="btn btn-outline-primary btn-sm" type="submit">View</button>
</div>
</form>
</section>
<section class="health-kpi-grid">
<article class="plain-card"><span class="eyebrow">Open threads</span><strong>@openThreads.Count</strong></article>
<article class="plain-card"><span class="eyebrow">Dormant threads</span><strong>@dormantThreads.Count</strong></article>
<article class="plain-card"><span class="eyebrow">Unresolved assets</span><strong>@unresolvedAssets.Count</strong></article>
<article class="plain-card"><span class="eyebrow">Warning hotspots</span><strong>@Model.WarningHotspots.Count</strong></article>
</section>
<section class="analytics-section">
<div class="analytics-section-heading">
<h2>Scene metric shape</h2>
<span class="muted">@Model.MetricGraphs.Count metric@(Model.MetricGraphs.Count == 1 ? "" : "s")</span>
</div>
@if (!Model.MetricGraphs.Any() || !metricScenes.Any())
{
<p class="muted">Add scene metrics to begin seeing peaks and troughs across the story.</p>
}
else
{
<div class="metric-shape-chart" data-metric-shape-chart>
<p class="metric-shape-intro">Compare selected pacing and emotional metrics across scenes in narrative order. Click a point to open that scene on the timeline.</p>
<div class="metric-toggle-row" data-metric-toggles></div>
<div class="metric-chart-frame">
<canvas aria-label="Comparative scene metric line graph"></canvas>
<p class="muted d-none" data-chart-fallback>Metric chart could not load. The raw metric rows are still available below.</p>
</div>
<script type="application/json">@Html.Raw(System.Text.Json.JsonSerializer.Serialize(metricShapePayload))</script>
</div>
<details class="metric-raw-details">
<summary>Show raw metric rows</summary>
<div class="analytics-metric-stack mt-2">
@foreach (var graph in Model.MetricGraphs)
{
<div class="analytics-metric-row">
<div class="analytics-metric-label">@graph.MetricName</div>
<div class="analytics-sparkline" style="--point-count:@Math.Max(graph.Points.Count, 1)">
@foreach (var point in graph.Points)
{
<a class="analytics-bar"
style="--bar-height:@Math.Max(point.Percent, 4)%"
title="@graph.MetricName - Scene @point.SceneNumber: @point.SceneTitle (@point.Value)"
asp-controller="Timeline"
asp-action="Index"
asp-route-projectId="@Model.Project.ProjectID"
asp-route-bookId="@point.BookID"
asp-route-selectedSceneId="@point.SceneID">
<span>@point.Value</span>
</a>
}
</div>
</div>
}
</div>
</details>
}
</section>
<section class="analytics-grid-two">
<article class="plain-card">
<h2>Pacing review</h2>
@if (!Model.PacingReviewNotes.Any())
{
<p class="muted">No sustained pacing runs found in this scope.</p>
}
else
{
@foreach (var note in Model.PacingReviewNotes)
{
<div class="analytics-list-item">
<strong>@note.MetricName</strong>
<span>@note.SceneRange</span>
<span class="muted">@note.ValuePattern</span>
<p>@note.ReviewNote</p>
</div>
}
}
</article>
<article class="plain-card">
<h2>Revision summary</h2>
@foreach (var status in Model.RevisionSummary.Where(x => x.SceneCount > 0 || x.ChapterCount > 0))
{
<div class="analytics-count-row">
<span>@status.StatusName</span>
<strong>@status.SceneCount scenes</strong>
</div>
}
<h3>Needs attention</h3>
@foreach (var scene in Model.RevisionAttentionScenes.Take(8))
{
<a class="analytics-link-row" asp-controller="Timeline" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID" asp-route-bookId="@scene.BookID" asp-route-selectedSceneId="@scene.SceneID">
<span>@scene.RevisionStatusName</span>
<strong>Ch @scene.ChapterNumber / Scene @scene.SceneNumber: @scene.SceneTitle</strong>
</a>
}
</article>
</section>
<section class="analytics-grid-two">
<article class="plain-card">
<h2>Scene purpose balance</h2>
@foreach (var purpose in Model.PurposeSummary)
{
<div class="purpose-stat-row">
<span>@purpose.PurposeName</span>
<div class="purpose-stat-bar"><span style="width:@Math.Min(100, purpose.PercentOfScenes)%"></span></div>
<strong>@purpose.SceneCount</strong>
</div>
}
<h3>Missing purpose/outcome</h3>
@foreach (var gap in Model.SceneGaps.Take(8))
{
<a class="analytics-link-row" asp-controller="Timeline" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID" asp-route-bookId="@gap.BookID" asp-route-selectedSceneId="@gap.SceneID">
<span>@(gap.MissingPurpose ? "No purpose" : "") @(gap.MissingOutcome ? "No outcome" : "")</span>
<strong>Ch @gap.ChapterNumber / Scene @gap.SceneNumber: @gap.SceneTitle</strong>
</a>
}
</article>
<article class="plain-card">
<h2>Warning hotspots</h2>
@foreach (var count in Model.WarningSeverityCounts)
{
<div class="warning-count-row"><span class="severity-pill @count.SeverityName.ToLowerInvariant()">@count.SeverityName</span><strong>@count.WarningCount</strong></div>
}
@foreach (var hotspot in Model.WarningHotspots.Take(8))
{
<a class="analytics-link-row" asp-controller="Warnings" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID" asp-route-bookId="@Model.BookID" asp-route-sceneId="@hotspot.SceneID">
<span>@hotspot.WarningCount warning@(hotspot.WarningCount == 1 ? "" : "s")</span>
<strong>Ch @hotspot.ChapterNumber / Scene @hotspot.SceneNumber: @hotspot.SceneTitle</strong>
</a>
}
</article>
</section>
<section class="analytics-grid-two">
<article class="plain-card">
<h2>Plot thread health</h2>
@foreach (var thread in Model.ThreadHealth.Where(x => x.IsHighImportanceOpen || x.IsDormant || x.HasClueWithoutPayoff || x.HasQuestionWithoutAnswer).Take(12))
{
<div class="analytics-list-item">
<strong>@thread.ThreadTitle</strong>
<span>@thread.PlotLineName / @thread.ThreadStatusName / importance @thread.Importance</span>
<span class="muted">@thread.LastTouchedSceneLabel</span>
<div class="purpose-chip-row">
@if (thread.IsDormant) { <span class="purpose-chip">Dormant</span> }
@if (thread.HasClueWithoutPayoff) { <span class="purpose-chip">Clue without payoff</span> }
@if (thread.HasQuestionWithoutAnswer) { <span class="purpose-chip">Question open</span> }
</div>
</div>
}
</article>
<article class="plain-card">
<h2>Story asset health</h2>
@foreach (var asset in Model.AssetHealth.Where(x => !x.IsResolved || x.HasDependencies || x.Importance >= 8).Take(12))
{
<a class="analytics-link-row" asp-controller="StoryAssets" asp-action="Details" asp-route-id="@asset.StoryAssetID">
<span>@asset.KindName / importance @asset.Importance</span>
<strong>@asset.AssetName</strong>
<span>@(asset.CurrentStateName ?? "No state") / @(asset.CurrentLocationPath ?? "No location")</span>
</a>
}
</article>
</section>
<section class="analytics-grid-three">
<article class="plain-card">
<h2>POV distribution</h2>
@foreach (var pov in Model.PovSummary)
{
<div class="analytics-count-row"><span>@pov.POVName</span><strong>@pov.SceneCount</strong></div>
}
@foreach (var chapter in Model.MultiPovChapters.Take(6))
{
<p class="muted">Ch @chapter.ChapterNumber: @chapter.ChapterTitle has @chapter.POVCount POVs.</p>
}
</article>
<article class="plain-card">
<h2>Character appearances</h2>
@foreach (var character in Model.CharacterAppearances.Take(10))
{
<a class="analytics-link-row" asp-controller="Characters" asp-action="Details" asp-route-id="@character.CharacterID">
<span>@character.SceneCount scenes</span>
<strong>@character.CharacterName</strong>
</a>
}
@foreach (var busy in Model.BusyScenes.Take(4))
{
<p class="muted">Busy scene: Ch @busy.ChapterNumber / Scene @busy.SceneNumber has @busy.CharacterCount characters.</p>
}
</article>
<article class="plain-card">
<h2>Location usage</h2>
@foreach (var location in Model.LocationUsage.Take(10))
{
@if (location.LocationID.HasValue)
{
<a class="analytics-link-row" asp-controller="Locations" asp-action="Details" asp-route-id="@location.LocationID">
<span>@location.SceneCount scenes</span>
<strong>@location.LocationName</strong>
</a>
}
else
{
<div class="analytics-count-row"><span>@location.LocationName</span><strong>@location.SceneCount</strong></div>
}
}
</article>
</section>

View File

@ -0,0 +1,81 @@
@model BookDetailViewModel
@{
ViewData["Title"] = Model.Book.BookTitle;
}
<nav class="breadcrumb-trail" aria-label="Breadcrumb">
<a asp-controller="Projects" asp-action="Index">Projects</a>
<a asp-controller="Projects" asp-action="Details" asp-route-id="@Model.Project.ProjectID">@Model.Project.ProjectName</a>
<span>@Model.Book.BookTitle</span>
</nav>
<section class="context-header">
<div class="context-title">
<span class="eyebrow">Current book</span>
<strong>@Model.Project.ProjectName / @Model.Book.BookTitle</strong>
</div>
<nav class="context-nav" aria-label="Book navigation">
<a asp-controller="Timeline" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID" asp-route-bookId="@Model.Book.BookID">Timeline</a>
<a asp-controller="Analytics" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID" asp-route-bookId="@Model.Book.BookID">Story health</a>
<a asp-controller="Warnings" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID" asp-route-bookId="@Model.Book.BookID">Warnings</a>
<a asp-controller="Characters" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID">Characters</a>
<a asp-controller="StoryAssets" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID">Assets</a>
<a asp-controller="Locations" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID">Locations</a>
<a asp-controller="PlotThreads" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID">Threads</a>
</nav>
</section>
<div class="page-heading">
<div>
<p class="eyebrow"><a asp-controller="Projects" asp-action="Details" asp-route-id="@Model.Project.ProjectID">@Model.Project.ProjectName</a></p>
<h1>Book @Model.Book.BookNumber: @Model.Book.BookTitle</h1>
@if (!string.IsNullOrWhiteSpace(Model.Book.Description))
{
<p class="lead-text">@Model.Book.Description</p>
}
</div>
<div class="button-row">
<a class="btn btn-outline-secondary" asp-action="Edit" asp-route-id="@Model.Book.BookID">Edit book</a>
<a class="btn btn-outline-primary" asp-controller="Timeline" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID" asp-route-bookId="@Model.Book.BookID">Timeline</a>
<a class="btn btn-outline-primary" asp-controller="Analytics" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID" asp-route-bookId="@Model.Book.BookID">Story health</a>
<a class="btn btn-primary" asp-controller="Chapters" asp-action="Create" asp-route-bookId="@Model.Book.BookID">Add chapter</a>
</div>
</div>
<section class="list-section">
<h2>Chapters</h2>
@if (!Model.Chapters.Any())
{
<p class="muted">No chapters yet.</p>
}
else
{
<div class="table-responsive">
<table class="table align-middle">
<thead>
<tr>
<th>Chapter</th>
<th>Title</th>
<th>Status</th>
<th>Summary</th>
<th></th>
</tr>
</thead>
<tbody>
@foreach (var chapter in Model.Chapters)
{
<tr>
<td class="narrow">@chapter.ChapterNumber</td>
<td><a asp-controller="Chapters" asp-action="Details" asp-route-id="@chapter.ChapterID">@chapter.ChapterTitle</a></td>
<td><span class="status-pill">@chapter.RevisionStatusName</span></td>
<td>@chapter.Summary</td>
<td class="text-end">
<a class="btn btn-outline-secondary btn-sm" asp-controller="Chapters" asp-action="Edit" asp-route-id="@chapter.ChapterID">Edit</a>
</td>
</tr>
}
</tbody>
</table>
</div>
}
</section>

View File

@ -0,0 +1,38 @@
@model BookEditViewModel
@{
ViewData["Title"] = Model.BookID == 0 ? "New Book" : "Edit Book";
}
<div class="page-heading compact">
<div>
<p class="eyebrow">@Model.Project?.ProjectName</p>
<h1>@ViewData["Title"]</h1>
</div>
<a class="btn btn-outline-secondary" asp-controller="Projects" asp-action="Details" asp-route-id="@Model.ProjectID">Back to project</a>
</div>
<form asp-action="Save" method="post" class="edit-panel">
<input asp-for="BookID" type="hidden" />
<input asp-for="ProjectID" type="hidden" />
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<div class="row g-3">
<div class="col-md-3">
<label asp-for="BookNumber" class="form-label"></label>
<input asp-for="BookNumber" class="form-control" />
</div>
<div class="col-md-9">
<label asp-for="BookTitle" class="form-label"></label>
<input asp-for="BookTitle" class="form-control" />
<span asp-validation-for="BookTitle" class="text-danger"></span>
</div>
<div class="col-12">
<label asp-for="Description" class="form-label"></label>
<textarea asp-for="Description" class="form-control" rows="4"></textarea>
</div>
</div>
<button class="btn btn-primary mt-3" type="submit">Save book</button>
</form>
@section Scripts {
<partial name="_ValidationScriptsPartial" />
}

View File

@ -0,0 +1,101 @@
@model ChapterDetailViewModel
@{
ViewData["Title"] = Model.Chapter.ChapterTitle;
}
<nav class="breadcrumb-trail" aria-label="Breadcrumb">
<a asp-controller="Projects" asp-action="Index">Projects</a>
<a asp-controller="Projects" asp-action="Details" asp-route-id="@Model.Project.ProjectID">@Model.Project.ProjectName</a>
<a asp-controller="Books" asp-action="Details" asp-route-id="@Model.Book.BookID">@Model.Book.BookTitle</a>
<span>Chapter @Model.Chapter.ChapterNumber</span>
</nav>
<section class="context-header">
<div class="context-title">
<span class="eyebrow">Current chapter</span>
<strong>@Model.Book.BookTitle / Chapter @Model.Chapter.ChapterNumber</strong>
</div>
<nav class="context-nav" aria-label="Chapter navigation">
<a asp-controller="Timeline" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID" asp-route-bookId="@Model.Book.BookID">Timeline</a>
<a asp-controller="Analytics" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID" asp-route-bookId="@Model.Book.BookID" asp-route-startChapterNumber="@Model.Chapter.ChapterNumber" asp-route-endChapterNumber="@Model.Chapter.ChapterNumber">Story health</a>
<a asp-controller="Warnings" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID" asp-route-bookId="@Model.Book.BookID">Warnings</a>
<a asp-controller="Characters" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID">Characters</a>
<a asp-controller="StoryAssets" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID">Assets</a>
<a asp-controller="Locations" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID">Locations</a>
</nav>
</section>
<div class="page-heading">
<div>
<p class="eyebrow">
<a asp-controller="Projects" asp-action="Details" asp-route-id="@Model.Project.ProjectID">@Model.Project.ProjectName</a>
/ <a asp-controller="Books" asp-action="Details" asp-route-id="@Model.Book.BookID">@Model.Book.BookTitle</a>
</p>
<h1>Chapter @Model.Chapter.ChapterNumber: @Model.Chapter.ChapterTitle</h1>
@if (!string.IsNullOrWhiteSpace(Model.Chapter.Summary))
{
<p class="lead-text">@Model.Chapter.Summary</p>
}
</div>
<div class="button-row">
<a class="btn btn-outline-secondary" asp-action="Edit" asp-route-id="@Model.Chapter.ChapterID">Edit chapter</a>
<a class="btn btn-outline-primary" asp-controller="Timeline" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID" asp-route-bookId="@Model.Book.BookID">Timeline</a>
<a class="btn btn-primary" asp-controller="Scenes" asp-action="Create" asp-route-chapterId="@Model.Chapter.ChapterID">Add scene</a>
</div>
</div>
<section class="list-section">
<h2>Scenes</h2>
@if (!Model.Scenes.Any())
{
<p class="muted">No scenes yet.</p>
}
else
{
<div class="table-responsive">
<table class="table align-middle">
<thead>
<tr>
<th>Scene</th>
<th>Title</th>
<th>Status</th>
<th>Time</th>
<th>Summary</th>
<th>Order</th>
<th></th>
</tr>
</thead>
<tbody>
@foreach (var scene in Model.Scenes)
{
<tr>
<td class="narrow">@scene.SceneNumber</td>
<td><a asp-controller="Scenes" asp-action="Edit" asp-route-id="@scene.SceneID">@scene.SceneTitle</a></td>
<td><span class="status-pill">@scene.RevisionStatusName</span></td>
<td>@scene.TimeLabel</td>
<td>@scene.Summary</td>
<td>
<div class="order-buttons">
<form asp-controller="Scenes" asp-action="MoveUp" method="post">
<input type="hidden" name="id" value="@scene.SceneID" />
<input type="hidden" name="chapterId" value="@Model.Chapter.ChapterID" />
<button class="btn btn-outline-secondary btn-sm" type="submit">Up</button>
</form>
<form asp-controller="Scenes" asp-action="MoveDown" method="post">
<input type="hidden" name="id" value="@scene.SceneID" />
<input type="hidden" name="chapterId" value="@Model.Chapter.ChapterID" />
<button class="btn btn-outline-secondary btn-sm" type="submit">Down</button>
</form>
</div>
</td>
<td class="text-end">
<a class="btn btn-outline-secondary btn-sm" asp-controller="Timeline" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID" asp-route-bookId="@Model.Book.BookID" asp-route-selectedSceneId="@scene.SceneID">Open in timeline</a>
<a class="btn btn-outline-secondary btn-sm" asp-controller="Scenes" asp-action="Edit" asp-route-id="@scene.SceneID">Edit</a>
</td>
</tr>
}
</tbody>
</table>
</div>
}
</section>

View File

@ -0,0 +1,42 @@
@model ChapterEditViewModel
@{
ViewData["Title"] = Model.ChapterID == 0 ? "New Chapter" : "Edit Chapter";
}
<div class="page-heading compact">
<div>
<p class="eyebrow">@Model.Book?.BookTitle</p>
<h1>@ViewData["Title"]</h1>
</div>
<a class="btn btn-outline-secondary" asp-controller="Books" asp-action="Details" asp-route-id="@Model.BookID">Back to book</a>
</div>
<form asp-action="Save" method="post" class="edit-panel">
<input asp-for="ChapterID" type="hidden" />
<input asp-for="BookID" type="hidden" />
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<div class="row g-3">
<div class="col-md-3">
<label asp-for="ChapterNumber" class="form-label"></label>
<input asp-for="ChapterNumber" class="form-control" />
</div>
<div class="col-md-6">
<label asp-for="ChapterTitle" class="form-label"></label>
<input asp-for="ChapterTitle" class="form-control" />
<span asp-validation-for="ChapterTitle" class="text-danger"></span>
</div>
<div class="col-md-3">
<label asp-for="RevisionStatusID" class="form-label"></label>
<select asp-for="RevisionStatusID" asp-items="Model.RevisionStatuses" class="form-select"></select>
</div>
<div class="col-12">
<label asp-for="Summary" class="form-label"></label>
<textarea asp-for="Summary" class="form-control" rows="4"></textarea>
</div>
</div>
<button class="btn btn-primary mt-3" type="submit">Save chapter</button>
</form>
@section Scripts {
<partial name="_ValidationScriptsPartial" />
}

View File

@ -0,0 +1,156 @@
@model CharacterDetailViewModel
@{
ViewData["Title"] = Model.Character.CharacterName;
var firstAppearance = Model.Appearances.FirstOrDefault();
}
<nav class="breadcrumb-trail" aria-label="Breadcrumb">
<a asp-controller="Projects" asp-action="Index">Projects</a>
<a asp-controller="Projects" asp-action="Details" asp-route-id="@Model.Project.ProjectID">@Model.Project.ProjectName</a>
<a asp-action="Index" asp-route-projectId="@Model.Project.ProjectID">Characters</a>
<span>@Model.Character.CharacterName</span>
</nav>
<section class="context-header">
<div class="context-title">
<span class="eyebrow">Project context</span>
<strong>@Model.Project.ProjectName</strong>
</div>
<nav class="context-nav" aria-label="Project navigation">
<a asp-controller="Timeline" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID" asp-route-selectedSceneId="@firstAppearance?.SceneID">Show on timeline</a>
<a asp-controller="Analytics" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID">Story health</a>
<a asp-controller="Warnings" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID">Warnings</a>
<a asp-action="Index" asp-route-projectId="@Model.Project.ProjectID">Characters</a>
</nav>
</section>
<div class="page-heading">
<div>
<p class="eyebrow">Character</p>
<h1>@Model.Character.CharacterName</h1>
<p class="lead-text">@Model.Character.DefaultDescription</p>
</div>
<div class="button-row">
<a class="btn btn-outline-secondary" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID">Characters</a>
<a class="btn btn-outline-secondary" asp-controller="Timeline" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID" asp-route-selectedSceneId="@firstAppearance?.SceneID">Back to timeline</a>
<a class="btn btn-outline-primary" asp-action="Edit" asp-route-id="@Model.Character.CharacterID">Edit character</a>
</div>
</div>
<section class="asset-detail-grid">
<article class="plain-card">
<h2>Profile</h2>
<p><strong>Short name:</strong> @Model.Character.ShortName</p>
<p><strong>Sex:</strong> @Model.Character.Sex</p>
<p><strong>Age:</strong> @Model.DisplayAge</p>
<p><strong>Height:</strong> @Model.Character.Height</p>
<p><strong>Eye colour:</strong> @Model.Character.EyeColour</p>
</article>
<article class="plain-card">
<h2>Relationships</h2>
@if (!Model.Relationships.Any())
{
<p class="muted">No relationships yet.</p>
}
else
{
<div class="asset-mini-list">
@foreach (var relationship in Model.Relationships)
{
<div class="asset-mini-item">
<strong>@relationship.CharacterAName -> @relationship.CharacterBName</strong>
<span>@relationship.RelationshipTypeName</span>
</div>
}
</div>
}
</article>
</section>
<section class="list-section mt-3">
<h2>Scene appearances</h2>
@if (!Model.Appearances.Any())
{
<p class="muted">No scene appearances yet.</p>
}
else
{
<div class="asset-mini-list">
@foreach (var appearance in Model.Appearances)
{
<div class="asset-mini-item">
<strong>@appearance.BookTitle / Ch @appearance.ChapterNumber / Scene @appearance.SceneNumber: @appearance.SceneTitle</strong>
<span>@appearance.RoleInSceneTypeName / @appearance.PresenceTypeName</span>
<span>@appearance.OutfitDescription @appearance.PhysicalCondition @appearance.EmotionalState</span>
<a asp-controller="Timeline" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID" asp-route-selectedSceneId="@appearance.SceneID">Open in timeline</a>
</div>
}
</div>
}
</section>
<section class="list-section mt-3">
<h2>Attribute history</h2>
@if (!Model.AttributeEvents.Any())
{
<p class="muted">No attribute events yet.</p>
}
else
{
<div class="asset-mini-list">
@foreach (var item in Model.AttributeEvents)
{
<div class="asset-mini-item">
<strong>@item.AttributeName: @item.AttributeValue</strong>
<span>@item.BookTitle / Ch @item.ChapterNumber / Scene @item.SceneNumber: @item.SceneTitle</span>
<span>@item.Description</span>
<a asp-controller="Timeline" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID" asp-route-selectedSceneId="@item.SceneID">Open in timeline</a>
</div>
}
</div>
}
</section>
<section class="list-section mt-3">
<h2>Knowledge</h2>
@if (!Model.KnowledgeItems.Any())
{
<p class="muted">No knowledge entries yet.</p>
}
else
{
<div class="asset-mini-list">
@foreach (var item in Model.KnowledgeItems)
{
<div class="asset-mini-item">
<strong>@item.KnowledgeStateName: @(item.AssetName ?? item.ThreadTitle)</strong>
<span>@item.BookTitle / Ch @item.ChapterNumber / Scene @item.SceneNumber: @item.SceneTitle</span>
<span>@item.Description</span>
<a asp-controller="Timeline" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID" asp-route-selectedSceneId="@item.SceneID">Open in timeline</a>
</div>
}
</div>
}
</section>
<section class="list-section mt-3">
<h2>Relationship events</h2>
@if (!Model.RelationshipEvents.Any())
{
<p class="muted">No relationship events yet.</p>
}
else
{
<div class="asset-mini-list">
@foreach (var item in Model.RelationshipEvents)
{
<div class="asset-mini-item">
<strong>@item.CharacterAName -> @item.CharacterBName: @item.RelationshipStateName</strong>
<span>@item.BookTitle / Ch @item.ChapterNumber / Scene @item.SceneNumber: @item.SceneTitle</span>
<span>Intensity @item.Intensity. @item.Description</span>
<a asp-controller="Timeline" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID" asp-route-selectedSceneId="@item.SceneID">Open in timeline</a>
</div>
}
</div>
}
</section>

View File

@ -0,0 +1,61 @@
@model CharacterEditViewModel
@{
ViewData["Title"] = Model.CharacterID == 0 ? "New Character" : "Edit Character";
}
<div class="page-heading compact">
<div>
<p class="eyebrow">Character</p>
<h1>@ViewData["Title"]</h1>
</div>
</div>
<form asp-action="Save" method="post" class="edit-panel">
<input asp-for="CharacterID" type="hidden" />
<input asp-for="ProjectID" type="hidden" />
<div class="row g-3">
<div class="col-md-8">
<label asp-for="CharacterName" class="form-label"></label>
<input asp-for="CharacterName" class="form-control" />
<span asp-validation-for="CharacterName" class="text-danger"></span>
</div>
<div class="col-md-4">
<label asp-for="ShortName" class="form-label"></label>
<input asp-for="ShortName" class="form-control" />
</div>
<div class="col-md-4">
<label asp-for="Sex" class="form-label"></label>
<input asp-for="Sex" class="form-control" />
</div>
<div class="col-md-4">
<label asp-for="BirthDate" class="form-label"></label>
<input asp-for="BirthDate" class="form-control" />
</div>
<div class="col-md-4">
<label asp-for="AgeAtSeriesStart" class="form-label"></label>
<input asp-for="AgeAtSeriesStart" class="form-control" />
</div>
<div class="col-md-6">
<label asp-for="Height" class="form-label"></label>
<input asp-for="Height" class="form-control" />
</div>
<div class="col-md-6">
<label asp-for="EyeColour" class="form-label"></label>
<input asp-for="EyeColour" class="form-control" />
</div>
<div class="col-12">
<label asp-for="DefaultDescription" class="form-label"></label>
<textarea asp-for="DefaultDescription" class="form-control" rows="5"></textarea>
</div>
</div>
<div class="button-row mt-3">
<button class="btn btn-primary" type="submit">Save character</button>
<a class="btn btn-outline-secondary" asp-action="Index" asp-route-projectId="@Model.ProjectID">Cancel</a>
</div>
</form>
@section Scripts {
<partial name="_ValidationScriptsPartial" />
}

View File

@ -0,0 +1,61 @@
@model CharacterListViewModel
@{
ViewData["Title"] = "Characters";
}
<section class="context-header">
<div class="context-title"><span class="eyebrow">Project context</span><strong>@Model.Project.ProjectName</strong></div>
<nav class="context-nav" aria-label="Project navigation">
<a asp-controller="Projects" asp-action="Details" asp-route-id="@Model.Project.ProjectID">Project</a>
<a asp-controller="Timeline" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID">Timeline</a>
<a asp-controller="Analytics" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID">Story health</a>
<a asp-controller="Warnings" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID">Warnings</a>
<a asp-controller="StoryAssets" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID">Assets</a>
<a asp-controller="Locations" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID">Locations</a>
<a asp-controller="PlotThreads" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID">Threads</a>
</nav>
</section>
<div class="page-heading">
<div>
<p class="eyebrow">Characters</p>
<h1>@Model.Project.ProjectName</h1>
</div>
<div class="button-row">
<a class="btn btn-outline-secondary" asp-controller="Projects" asp-action="Details" asp-route-id="@Model.Project.ProjectID">Project</a>
<a class="btn btn-primary" asp-action="Create" asp-route-projectId="@Model.Project.ProjectID">Add character</a>
</div>
</div>
<section class="list-section">
@if (!Model.Characters.Any())
{
<p class="muted">No characters yet.</p>
}
else
{
<div class="asset-card-grid">
@foreach (var character in Model.Characters)
{
<article class="asset-card character-card">
<div>
<p class="eyebrow">Character</p>
<h2><a asp-action="Details" asp-route-id="@character.CharacterID">@character.CharacterName</a></h2>
<p class="muted">@character.DefaultDescription</p>
</div>
<div class="asset-card-footer">
@if (!string.IsNullOrWhiteSpace(character.ShortName))
{
<span class="purpose-chip">@character.ShortName</span>
}
@if (character.AgeAtSeriesStart.HasValue)
{
<span class="status-pill">Age @character.AgeAtSeriesStart</span>
}
<a class="btn btn-outline-secondary btn-sm ms-auto" asp-action="Edit" asp-route-id="@character.CharacterID">Edit</a>
</div>
</article>
}
</div>
}
</section>

View File

@ -0,0 +1,8 @@
@{
ViewData["Title"] = "Home Page";
}
<div class="text-center">
<h1 class="display-4">Welcome</h1>
<p>Learn about <a href="https://learn.microsoft.com/aspnet/core">building Web apps with ASP.NET Core</a>.</p>
</div>

View File

@ -0,0 +1,6 @@
@{
ViewData["Title"] = "Privacy Policy";
}
<h1>@ViewData["Title"]</h1>
<p>Use this page to detail your site's privacy policy.</p>

View File

@ -0,0 +1,166 @@
@model LocationDetailViewModel
@{
ViewData["Title"] = Model.Location.LocationName;
var firstScene = Model.Scenes.FirstOrDefault();
}
<nav class="breadcrumb-trail" aria-label="Breadcrumb">
<a asp-controller="Projects" asp-action="Index">Projects</a>
<a asp-controller="Projects" asp-action="Details" asp-route-id="@Model.Project.ProjectID">@Model.Project.ProjectName</a>
<a asp-action="Index" asp-route-projectId="@Model.Project.ProjectID">Locations</a>
<span>@Model.Location.LocationName</span>
</nav>
<section class="context-header">
<div class="context-title">
<span class="eyebrow">Project context</span>
<strong>@Model.Project.ProjectName</strong>
</div>
<nav class="context-nav" aria-label="Project navigation">
<a asp-controller="Timeline" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID" asp-route-selectedSceneId="@firstScene?.SceneID">Show scene usage</a>
<a asp-controller="Analytics" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID">Story health</a>
<a asp-controller="Warnings" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID">Warnings</a>
<a asp-action="Index" asp-route-projectId="@Model.Project.ProjectID">Locations</a>
</nav>
</section>
<div class="page-heading compact">
<div>
<p class="eyebrow">Location detail</p>
<h1>@Model.Location.LocationName</h1>
<p class="muted">@Model.Location.LocationPath</p>
</div>
<div class="button-row">
<a class="btn btn-outline-secondary" asp-action="Edit" asp-route-id="@Model.Location.LocationID">Edit</a>
<a class="btn btn-outline-secondary" asp-controller="Timeline" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID" asp-route-selectedSceneId="@firstScene?.SceneID">Back to timeline</a>
<a class="btn btn-outline-primary" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID">All locations</a>
</div>
</div>
<section class="edit-panel">
<div class="row g-3">
<div class="col-md-4">
<p class="eyebrow">Type</p>
<p>@(Model.Location.LocationTypeName ?? "Not set")</p>
</div>
<div class="col-md-4">
<p class="eyebrow">Parent</p>
<p>@(Model.Location.ParentLocationName ?? "Top level")</p>
</div>
<div class="col-12">
<p class="eyebrow">Description</p>
<p>@(Model.Location.Description ?? "No description yet.")</p>
</div>
</div>
</section>
<section class="edit-panel">
<h2>Child locations</h2>
@if (!Model.ChildLocations.Any())
{
<p class="muted">No child locations yet.</p>
}
else
{
<div class="location-chip-list">
@foreach (var child in Model.ChildLocations)
{
<a class="location-chip" asp-action="Details" asp-route-id="@child.LocationID">@child.LocationName</a>
}
</div>
}
</section>
<section class="edit-panel">
<h2>Relationships</h2>
@if (!Model.Relationships.Any())
{
<p class="muted">No relationships defined yet.</p>
}
else
{
<div class="location-relationship-list">
@foreach (var relationship in Model.Relationships)
{
<div class="location-relationship-card">
<div>
<strong>@relationship.FromLocationName</strong>
<span>@relationship.RelationshipTypeName.ToLowerInvariant()</span>
<strong>@relationship.ToLocationName</strong>
@if (relationship.IsBidirectional)
{
<span class="location-badge">both ways</span>
}
<p class="muted">@relationship.Notes</p>
</div>
<form asp-action="ArchiveRelationship" method="post">
<input type="hidden" name="id" value="@relationship.LocationRelationshipID" />
<input type="hidden" name="returnLocationId" value="@Model.Location.LocationID" />
<button class="btn btn-outline-secondary btn-sm" type="submit">Archive</button>
</form>
</div>
}
</div>
}
<form asp-action="AddRelationship" method="post" class="asset-create-form mt-3">
<input asp-for="NewRelationship.ProjectID" type="hidden" name="ProjectID" />
<input asp-for="NewRelationship.FromLocationID" type="hidden" name="FromLocationID" />
<div class="row g-2">
<div class="col-md-5">
<label class="form-label" for="ToLocationID">To location</label>
<select class="form-select form-select-sm" id="ToLocationID" name="ToLocationID" asp-items="Model.LocationOptions"></select>
</div>
<div class="col-md-4">
<label class="form-label" for="LocationRelationshipTypeID">Relationship</label>
<select class="form-select form-select-sm" id="LocationRelationshipTypeID" name="LocationRelationshipTypeID" asp-items="Model.RelationshipTypeOptions"></select>
</div>
<div class="col-md-3">
<label class="form-label" for="Strength">Strength</label>
<input class="form-control form-control-sm" id="Strength" name="Strength" type="number" min="1" max="10" />
</div>
<div class="col-12 location-flags">
<label><input type="checkbox" name="IsBidirectional" value="true" /> Bidirectional</label>
<label><input type="checkbox" name="CanHearNormalSpeech" value="true" /> Can hear normal speech</label>
<label><input type="checkbox" name="CanHearLoudNoise" value="true" /> Can hear loud noise</label>
<label><input type="checkbox" name="CanSeeMovement" value="true" /> Can see movement</label>
<label><input type="checkbox" name="CanSeeClearly" value="true" /> Can see clearly</label>
<label><input type="checkbox" name="CanTravelDirectly" value="true" /> Can travel directly</label>
</div>
<div class="col-12">
<textarea class="form-control form-control-sm" name="Notes" rows="2" placeholder="Relationship notes"></textarea>
</div>
</div>
<button class="btn btn-outline-primary btn-sm mt-2" type="submit">Add relationship</button>
</form>
</section>
<section class="edit-panel">
<h2>Used here</h2>
<div class="row g-3">
<div class="col-md-4">
<h3>Scenes</h3>
@foreach (var scene in Model.Scenes)
{
<p><strong>Scene @scene.SceneNumber</strong> <a asp-controller="Timeline" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID" asp-route-selectedSceneId="@scene.SceneID">@scene.SceneTitle</a></p>
}
@if (!Model.Scenes.Any()) { <p class="muted">No scenes set here.</p> }
</div>
<div class="col-md-4">
<h3>Characters</h3>
@foreach (var character in Model.Characters)
{
<p><strong>@character.CharacterName</strong> <span class="muted">Scene @character.SceneNumber</span></p>
}
@if (!Model.Characters.Any()) { <p class="muted">No character placements yet.</p> }
</div>
<div class="col-md-4">
<h3>Assets</h3>
@foreach (var asset in Model.Assets)
{
<p><strong>@asset.AssetName</strong> <span class="muted">@asset.Description</span></p>
}
@if (!Model.Assets.Any()) { <p class="muted">No asset placements yet.</p> }
</div>
</div>
</section>

View File

@ -0,0 +1,49 @@
@model LocationEditViewModel
@{
ViewData["Title"] = Model.LocationID == 0 ? "New Location" : "Edit Location";
}
<div class="page-heading compact">
<div>
<p class="eyebrow">Location</p>
<h1>@ViewData["Title"]</h1>
</div>
</div>
<form asp-action="Save" method="post" class="edit-panel">
<input asp-for="LocationID" type="hidden" />
<input asp-for="ProjectID" type="hidden" />
<div class="row g-3">
<div class="col-md-8">
<label asp-for="LocationName" class="form-label"></label>
<input asp-for="LocationName" class="form-control" />
<span asp-validation-for="LocationName" class="text-danger"></span>
</div>
<div class="col-md-4">
<label asp-for="LocationTypeID" class="form-label"></label>
<select asp-for="LocationTypeID" asp-items="Model.LocationTypeOptions" class="form-select">
<option value="">Not set</option>
</select>
</div>
<div class="col-12">
<label asp-for="ParentLocationID" class="form-label"></label>
<select asp-for="ParentLocationID" asp-items="Model.ParentLocationOptions" class="form-select">
<option value="">Top level</option>
</select>
</div>
<div class="col-12">
<label asp-for="Description" class="form-label"></label>
<textarea asp-for="Description" class="form-control" rows="5"></textarea>
</div>
</div>
<div class="button-row mt-3">
<button class="btn btn-primary" type="submit">Save location</button>
<a class="btn btn-outline-secondary" asp-action="Index" asp-route-projectId="@Model.ProjectID">Cancel</a>
</div>
</form>
@section Scripts {
<partial name="_ValidationScriptsPartial" />
}

View File

@ -0,0 +1,59 @@
@model LocationListViewModel
@{
ViewData["Title"] = "Locations";
}
<section class="context-header">
<div class="context-title"><span class="eyebrow">Project context</span><strong>@Model.Project.ProjectName</strong></div>
<nav class="context-nav" aria-label="Project navigation">
<a asp-controller="Projects" asp-action="Details" asp-route-id="@Model.Project.ProjectID">Project</a>
<a asp-controller="Timeline" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID">Timeline</a>
<a asp-controller="Analytics" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID">Story health</a>
<a asp-controller="Warnings" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID">Warnings</a>
<a asp-controller="Characters" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID">Characters</a>
<a asp-controller="StoryAssets" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID">Assets</a>
<a asp-controller="PlotThreads" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID">Threads</a>
</nav>
</section>
<div class="page-heading compact">
<div>
<p class="eyebrow">Locations</p>
<h1>@Model.Project.ProjectName</h1>
</div>
<a class="btn btn-primary" asp-action="Create" asp-route-projectId="@Model.Project.ProjectID">New location</a>
</div>
@if (!Model.Locations.Any())
{
<section class="empty-panel">
<h2>No locations yet</h2>
<p>Add buildings, floors, rooms, streets, and other places that matter to the story.</p>
</section>
}
else
{
<section class="edit-panel location-tree">
@foreach (var location in Model.Locations)
{
<div class="location-tree-row" style="--location-depth:@location.Depth">
<div>
<a asp-action="Details" asp-route-id="@location.LocationID"><strong>@location.LocationName</strong></a>
@if (!string.IsNullOrWhiteSpace(location.LocationTypeName))
{
<span class="location-badge">@location.LocationTypeName</span>
}
<span class="muted">@location.LocationPath</span>
</div>
<div class="button-row compact-buttons">
<a class="btn btn-outline-secondary btn-sm" asp-action="Edit" asp-route-id="@location.LocationID">Edit</a>
<form asp-action="Archive" method="post">
<input type="hidden" name="id" value="@location.LocationID" />
<input type="hidden" name="projectId" value="@Model.Project.ProjectID" />
<button class="btn btn-outline-danger btn-sm" type="submit">Archive</button>
</form>
</div>
</div>
}
</section>
}

View File

@ -0,0 +1,70 @@
@model PlotLineEditViewModel
@{
ViewData["Title"] = Model.PlotLineID == 0 ? "New Plot Line" : "Edit Plot Line";
}
<div class="page-heading compact">
<div>
<p class="eyebrow">@Model.Project?.ProjectName</p>
<h1>@ViewData["Title"]</h1>
</div>
<a class="btn btn-outline-secondary" asp-action="Index" asp-route-projectId="@Model.ProjectID">Back to plot lines</a>
</div>
<form asp-action="Save" method="post" class="edit-panel">
<input asp-for="PlotLineID" type="hidden" />
<input asp-for="ProjectID" type="hidden" />
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<div class="row g-3">
<div class="col-md-8">
<label asp-for="PlotLineName" class="form-label"></label>
<input asp-for="PlotLineName" class="form-control" />
<span asp-validation-for="PlotLineName" class="text-danger"></span>
</div>
<div class="col-md-4">
<label asp-for="PlotLineTypeID" class="form-label"></label>
<select asp-for="PlotLineTypeID" asp-items="Model.PlotLineTypes" class="form-select"></select>
</div>
<div class="col-md-4">
<label asp-for="BookID" class="form-label"></label>
<select asp-for="BookID" asp-items="Model.BookOptions" class="form-select">
<option value="">Project-wide</option>
</select>
</div>
<div class="col-md-4">
<label asp-for="ParentPlotLineID" class="form-label"></label>
<select asp-for="ParentPlotLineID" 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" />
</div>
<div class="col-md-2">
<label asp-for="Colour" class="form-label"></label>
<input asp-for="Colour" type="color" class="form-control form-control-color" />
</div>
<div class="col-12">
<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</span>
</label>
</div>
<div class="col-md-4">
<label class="purpose-check">
<input asp-for="IsVisibleOnTimeline" />
<span>Visible on timeline</span>
</label>
</div>
</div>
<button class="btn btn-primary mt-3" type="submit">Save plot line</button>
</form>
@section Scripts {
<partial name="_ValidationScriptsPartial" />
}

View File

@ -0,0 +1,66 @@
@model PlotLineListViewModel
@{
ViewData["Title"] = "Plot Lines";
}
<section class="context-header">
<div class="context-title"><span class="eyebrow">Project context</span><strong>@Model.Project.ProjectName</strong></div>
<nav class="context-nav" aria-label="Project navigation">
<a asp-controller="Projects" asp-action="Details" asp-route-id="@Model.Project.ProjectID">Project</a>
<a asp-controller="Timeline" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID">Timeline</a>
<a asp-controller="Analytics" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID">Story health</a>
<a asp-controller="Warnings" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID">Warnings</a>
<a asp-controller="PlotThreads" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID">Threads</a>
</nav>
</section>
<div class="page-heading">
<div>
<p class="eyebrow"><a asp-controller="Projects" asp-action="Details" asp-route-id="@Model.Project.ProjectID">@Model.Project.ProjectName</a></p>
<h1>Plot lines</h1>
</div>
<div class="button-row">
<a class="btn btn-outline-primary" asp-controller="Timeline" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID">Timeline</a>
<a class="btn btn-primary" asp-action="Create" asp-route-projectId="@Model.Project.ProjectID">Add plot line</a>
</div>
</div>
<section class="list-section">
@if (!Model.PlotLines.Any())
{
<p class="muted">No plot lines yet. Add one to create the first timeline lane.</p>
}
else
{
<div class="table-responsive">
<table class="table align-middle">
<thead>
<tr>
<th>Colour</th>
<th>Name</th>
<th>Type</th>
<th>Book</th>
<th>Timeline</th>
<th></th>
</tr>
</thead>
<tbody>
@foreach (var plotLine in Model.PlotLines)
{
<tr>
<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.BookTitle ?? "Project-wide")</td>
<td>@(plotLine.IsVisibleOnTimeline ? "Visible" : "Hidden")</td>
<td class="text-end">
<a class="btn btn-outline-secondary btn-sm" asp-controller="PlotThreads" asp-action="Create" asp-route-projectId="@Model.Project.ProjectID" asp-route-plotLineId="@plotLine.PlotLineID">Add thread</a>
<a class="btn btn-outline-secondary btn-sm" asp-action="Edit" asp-route-id="@plotLine.PlotLineID">Edit</a>
</td>
</tr>
}
</tbody>
</table>
</div>
}
</section>

View File

@ -0,0 +1,68 @@
@model PlotThreadEditViewModel
@{
ViewData["Title"] = Model.PlotThreadID == 0 ? "New Plot Thread" : "Edit Plot Thread";
}
<div class="page-heading compact">
<div>
<p class="eyebrow">@Model.Project?.ProjectName</p>
<h1>@ViewData["Title"]</h1>
</div>
<a class="btn btn-outline-secondary" asp-action="Index" asp-route-projectId="@Model.ProjectID">Back to plot threads</a>
</div>
<form asp-action="Save" method="post" class="edit-panel">
<input asp-for="PlotThreadID" type="hidden" />
<input asp-for="ProjectID" type="hidden" />
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<div class="row g-3">
<div class="col-md-6">
<label asp-for="PlotLineID" class="form-label"></label>
<select asp-for="PlotLineID" asp-items="Model.PlotLineOptions" class="form-select"></select>
</div>
<div class="col-md-6">
<label asp-for="ThreadTitle" class="form-label"></label>
<input asp-for="ThreadTitle" class="form-control" />
<span asp-validation-for="ThreadTitle" class="text-danger"></span>
</div>
<div class="col-md-4">
<label asp-for="ThreadTypeID" class="form-label"></label>
<select asp-for="ThreadTypeID" asp-items="Model.ThreadTypeOptions" class="form-select"></select>
</div>
<div class="col-md-4">
<label asp-for="ThreadStatusID" class="form-label"></label>
<select asp-for="ThreadStatusID" asp-items="Model.ThreadStatusOptions" class="form-select"></select>
</div>
<div class="col-md-4">
<label asp-for="Importance" class="form-label"></label>
<input asp-for="Importance" type="number" min="1" max="10" class="form-control" />
</div>
<div class="col-12">
<label asp-for="Summary" class="form-label"></label>
<textarea asp-for="Summary" class="form-control" rows="4"></textarea>
</div>
<div class="col-md-4">
<label asp-for="IntroducedSceneID" class="form-label"></label>
<select asp-for="IntroducedSceneID" asp-items="Model.SceneOptions" class="form-select">
<option value="">Not set</option>
</select>
</div>
<div class="col-md-4">
<label asp-for="PlannedResolutionSceneID" class="form-label"></label>
<select asp-for="PlannedResolutionSceneID" asp-items="Model.SceneOptions" class="form-select">
<option value="">Not set</option>
</select>
</div>
<div class="col-md-4">
<label asp-for="ActualResolutionSceneID" class="form-label"></label>
<select asp-for="ActualResolutionSceneID" asp-items="Model.SceneOptions" class="form-select">
<option value="">Not set</option>
</select>
</div>
</div>
<button class="btn btn-primary mt-3" type="submit">Save thread</button>
</form>
@section Scripts {
<partial name="_ValidationScriptsPartial" />
}

View File

@ -0,0 +1,66 @@
@model PlotThreadListViewModel
@{
ViewData["Title"] = "Plot Threads";
}
<section class="context-header">
<div class="context-title"><span class="eyebrow">Project context</span><strong>@Model.Project.ProjectName</strong></div>
<nav class="context-nav" aria-label="Project navigation">
<a asp-controller="Projects" asp-action="Details" asp-route-id="@Model.Project.ProjectID">Project</a>
<a asp-controller="Timeline" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID">Timeline</a>
<a asp-controller="Analytics" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID">Story health</a>
<a asp-controller="Warnings" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID">Warnings</a>
<a asp-controller="Characters" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID">Characters</a>
<a asp-controller="StoryAssets" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID">Assets</a>
<a asp-controller="Locations" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID">Locations</a>
</nav>
</section>
<div class="page-heading">
<div>
<p class="eyebrow"><a asp-controller="Projects" asp-action="Details" asp-route-id="@Model.Project.ProjectID">@Model.Project.ProjectName</a></p>
<h1>Plot threads</h1>
</div>
<div class="button-row">
<a class="btn btn-outline-primary" asp-controller="Timeline" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID">Timeline</a>
<a class="btn btn-outline-primary" asp-controller="PlotLines" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID">Plot lines</a>
<a class="btn btn-primary" asp-action="Create" asp-route-projectId="@Model.Project.ProjectID">Add thread</a>
</div>
</div>
<section class="list-section">
@if (!Model.PlotThreads.Any())
{
<p class="muted">No plot threads yet. Threads are the unresolved or developing strands that create markers on plot line lanes.</p>
}
else
{
<div class="table-responsive">
<table class="table align-middle">
<thead>
<tr>
<th>Thread</th>
<th>Plot line</th>
<th>Type</th>
<th>Status</th>
<th>Importance</th>
<th></th>
</tr>
</thead>
<tbody>
@foreach (var thread in Model.PlotThreads)
{
<tr>
<td><a asp-action="Edit" asp-route-id="@thread.PlotThreadID">@thread.ThreadTitle</a></td>
<td>@thread.PlotLineName</td>
<td>@thread.ThreadTypeName</td>
<td><span class="status-pill">@thread.ThreadStatusName</span></td>
<td>@thread.Importance</td>
<td class="text-end"><a class="btn btn-outline-secondary btn-sm" asp-action="Edit" asp-route-id="@thread.PlotThreadID">Edit</a></td>
</tr>
}
</tbody>
</table>
</div>
}
</section>

View File

@ -0,0 +1,84 @@
@model ProjectDetailViewModel
@{
ViewData["Title"] = Model.Project.ProjectName;
}
<nav class="breadcrumb-trail" aria-label="Breadcrumb">
<a asp-controller="Projects" asp-action="Index">Projects</a>
<span>@Model.Project.ProjectName</span>
</nav>
<section class="context-header">
<div class="context-title">
<span class="eyebrow">Current project</span>
<strong>@Model.Project.ProjectName</strong>
</div>
<nav class="context-nav" aria-label="Project navigation">
<a asp-controller="Timeline" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID">Timeline</a>
<a asp-controller="Analytics" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID">Story health</a>
<a asp-controller="Warnings" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID">Warnings</a>
<a asp-controller="Characters" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID">Characters</a>
<a asp-controller="StoryAssets" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID">Assets</a>
<a asp-controller="Locations" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID">Locations</a>
<a asp-controller="PlotThreads" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID">Threads</a>
</nav>
</section>
<div class="page-heading">
<div>
<p class="eyebrow">Project / Series</p>
<h1>@Model.Project.ProjectName</h1>
@if (!string.IsNullOrWhiteSpace(Model.Project.Description))
{
<p class="lead-text">@Model.Project.Description</p>
}
</div>
<div class="button-row">
<a class="btn btn-outline-secondary" asp-action="Edit" asp-route-id="@Model.Project.ProjectID">Edit project</a>
<a class="btn btn-outline-primary" asp-controller="Timeline" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID">Timeline</a>
<a class="btn btn-outline-primary" asp-controller="PlotLines" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID">Plot lines</a>
<a class="btn btn-outline-primary" asp-controller="PlotThreads" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID">Plot threads</a>
<a class="btn btn-outline-primary" asp-controller="StoryAssets" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID">Story assets</a>
<a class="btn btn-outline-primary" asp-controller="Characters" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID">Characters</a>
<a class="btn btn-outline-primary" asp-controller="Locations" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID">Locations</a>
<a class="btn btn-outline-primary" asp-controller="Warnings" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID">Warnings</a>
<a class="btn btn-outline-primary" asp-controller="Analytics" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID">Story health</a>
<a class="btn btn-primary" asp-controller="Books" asp-action="Create" asp-route-projectId="@Model.Project.ProjectID">Add book</a>
</div>
</div>
<section class="list-section">
<h2>Books</h2>
@if (!Model.Books.Any())
{
<p class="muted">No books yet. Add the first book to begin the hierarchy.</p>
}
else
{
<div class="table-responsive">
<table class="table align-middle">
<thead>
<tr>
<th>Order</th>
<th>Book</th>
<th>Description</th>
<th></th>
</tr>
</thead>
<tbody>
@foreach (var book in Model.Books)
{
<tr>
<td class="narrow">@book.BookNumber</td>
<td><a asp-controller="Books" asp-action="Details" asp-route-id="@book.BookID">@book.BookTitle</a></td>
<td>@book.Description</td>
<td class="text-end">
<a class="btn btn-outline-secondary btn-sm" asp-controller="Books" asp-action="Edit" asp-route-id="@book.BookID">Edit</a>
</td>
</tr>
}
</tbody>
</table>
</div>
}
</section>

View File

@ -0,0 +1,31 @@
@model ProjectEditViewModel
@{
ViewData["Title"] = Model.ProjectID == 0 ? "New Project" : "Edit Project";
}
<div class="page-heading compact">
<div>
<p class="eyebrow">Project / Series</p>
<h1>@ViewData["Title"]</h1>
</div>
<a class="btn btn-outline-secondary" asp-action="Index">Back to projects</a>
</div>
<form asp-action="Save" method="post" class="edit-panel">
<input asp-for="ProjectID" type="hidden" />
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<div class="mb-3">
<label asp-for="ProjectName" class="form-label"></label>
<input asp-for="ProjectName" class="form-control" />
<span asp-validation-for="ProjectName" class="text-danger"></span>
</div>
<div class="mb-3">
<label asp-for="Description" class="form-label"></label>
<textarea asp-for="Description" class="form-control" rows="5"></textarea>
</div>
<button class="btn btn-primary" type="submit">Save project</button>
</form>
@section Scripts {
<partial name="_ValidationScriptsPartial" />
}

View File

@ -0,0 +1,37 @@
@model IReadOnlyList<Project>
@{
ViewData["Title"] = "Projects";
}
<div class="page-heading">
<div>
<p class="eyebrow">Series planning</p>
<h1>Projects</h1>
</div>
<a class="btn btn-primary" asp-action="Create">New project</a>
</div>
@if (!Model.Any())
{
<section class="empty-panel">
<h2>Start with a series or standalone novel</h2>
<p>Create a project, then add books, chapters and scenes in narrative order.</p>
<a class="btn btn-primary" asp-action="Create">Create your first project</a>
</section>
}
else
{
<div class="content-grid">
@foreach (var project in Model)
{
<article class="plain-card">
<h2>@project.ProjectName</h2>
<p>@(string.IsNullOrWhiteSpace(project.Description) ? "No description yet." : project.Description)</p>
<div class="button-row">
<a class="btn btn-outline-primary btn-sm" asp-action="Details" asp-route-id="@project.ProjectID">Open</a>
<a class="btn btn-outline-secondary btn-sm" asp-action="Edit" asp-route-id="@project.ProjectID">Edit</a>
</div>
</article>
}
</div>
}

View File

@ -0,0 +1,53 @@
@model SceneEditViewModel
@{
ViewData["Title"] = Model.SceneID == 0 ? "New Scene" : "Edit Scene";
}
<nav class="breadcrumb-trail" aria-label="Breadcrumb">
<a asp-controller="Projects" asp-action="Index">Projects</a>
@if (Model.Project is not null)
{
<a asp-controller="Projects" asp-action="Details" asp-route-id="@Model.Project.ProjectID">@Model.Project.ProjectName</a>
}
@if (Model.Book is not null)
{
<a asp-controller="Books" asp-action="Details" asp-route-id="@Model.Book.BookID">@Model.Book.BookTitle</a>
}
@if (Model.Chapter is not null)
{
<a asp-controller="Chapters" asp-action="Details" asp-route-id="@Model.ChapterID">Chapter @Model.Chapter.ChapterNumber</a>
}
<span>@(Model.SceneID == 0 ? "New scene" : $"Scene {Model.SceneNumber}")</span>
</nav>
@if (Model.Project is not null)
{
<section class="context-header">
<div class="context-title">
<span class="eyebrow">Working context</span>
<strong>@Model.Project.ProjectName@if (Model.Book is not null) { <span> / @Model.Book.BookTitle</span> }</strong>
</div>
<nav class="context-nav" aria-label="Scene navigation">
<a asp-controller="Timeline" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID" asp-route-bookId="@Model.Book?.BookID" asp-route-selectedSceneId="@Model.SceneID">Open scene in timeline</a>
<a asp-controller="Chapters" asp-action="Details" asp-route-id="@Model.ChapterID">Back to chapter</a>
<a asp-controller="Analytics" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID" asp-route-bookId="@Model.Book?.BookID">Story health</a>
<a asp-controller="Warnings" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID" asp-route-bookId="@Model.Book?.BookID" asp-route-sceneId="@Model.SceneID">Warnings</a>
</nav>
</section>
}
<div class="page-heading compact">
<div>
<p class="eyebrow">@Model.Book?.BookTitle / Chapter @Model.Chapter?.ChapterNumber</p>
<h1>@ViewData["Title"]</h1>
</div>
<a class="btn btn-outline-secondary" asp-controller="Chapters" asp-action="Details" asp-route-id="@Model.ChapterID">Back to chapter</a>
</div>
<section class="edit-panel scene-editor">
<partial name="_SceneInspector" model="Model" />
</section>
@section Scripts {
<partial name="_ValidationScriptsPartial" />
}

View File

@ -0,0 +1,67 @@
@model SceneMovePreviewViewModel
@{
ViewData["Title"] = "Move Scene Preview";
}
<div class="page-heading compact">
<div>
<p class="eyebrow">Move scene</p>
<h1>@Model.Preview.SceneTitle</h1>
</div>
</div>
<section class="edit-panel move-preview-panel">
<h2>Preview</h2>
<div class="row g-3">
<div class="col-md-6">
<p class="eyebrow">Current position</p>
<p><strong>@Model.Preview.SourceBookTitle</strong></p>
<p>Chapter @Model.Preview.SourceChapterNumber: @Model.Preview.SourceChapterTitle</p>
<p>Scene @Model.Preview.SceneNumber: @Model.Preview.SceneTitle</p>
</div>
<div class="col-md-6">
<p class="eyebrow">Proposed position</p>
<p><strong>@Model.Preview.TargetBookTitle</strong></p>
<p>Chapter @Model.Preview.TargetChapterNumber: @Model.Preview.TargetChapterTitle</p>
<p>Position: @Model.Preview.Position.ToLowerInvariant()</p>
</div>
</div>
<div class="focused-validation-summary">
<span>@Model.Preview.DependencyCount dependenc@(Model.Preview.DependencyCount == 1 ? "y" : "ies") attached</span>
<span>@Model.Preview.ActiveWarningCount active warning@(Model.Preview.ActiveWarningCount == 1 ? "" : "s") on this scene</span>
<span>After confirming, PlotLine will run validation for the target book.</span>
</div>
@if (Model.CurrentWarnings.Any())
{
<div class="warning-mini-list mt-3">
@foreach (var warning in Model.CurrentWarnings.Take(5))
{
<div class="warning-mini-card">
<span class="severity-pill @warning.SeverityName.ToLowerInvariant()">@warning.SeverityName</span>
<strong>@warning.WarningTypeName</strong>
<p>@warning.Message</p>
</div>
}
</div>
}
<div class="button-row mt-3">
<form asp-action="ConfirmMove" method="post">
<input type="hidden" name="SceneID" value="@Model.Request.SceneID" />
<input type="hidden" name="TargetChapterID" value="@Model.Request.TargetChapterID" />
<input type="hidden" name="Position" value="@Model.Request.Position" />
<input type="hidden" name="RenumberScenes" value="@Model.Request.RenumberScenes.ToString().ToLowerInvariant()" />
<input type="hidden" name="ReturnProjectID" value="@Model.Request.ReturnProjectID" />
<input type="hidden" name="ReturnBookID" value="@Model.Request.ReturnBookID" />
<button class="btn btn-primary" type="submit">Confirm move</button>
</form>
<a class="btn btn-outline-secondary"
asp-controller="Timeline"
asp-action="Index"
asp-route-projectId="@Model.Request.ReturnProjectID"
asp-route-bookId="@Model.Request.ReturnBookID"
asp-route-selectedSceneId="@Model.Request.SceneID">Cancel</a>
</div>
</section>

View File

@ -0,0 +1,900 @@
@model SceneEditViewModel
@{
var revisionStatus = Model.RevisionStatuses.FirstOrDefault(x => x.Value == Model.RevisionStatusID.ToString())?.Text ?? "Not set";
var primaryLocation = Model.LocationOptions.FirstOrDefault(x => x.Value == Model.PrimaryLocationID?.ToString())?.Text ?? "No primary location";
var povCharacter = Model.SceneCharacters.FirstOrDefault(x => x.RoleInSceneTypeName?.Contains("POV", StringComparison.OrdinalIgnoreCase) == true)?.CharacterName
?? Model.SceneCharacters.FirstOrDefault()?.CharacterName
?? "No POV";
var timeLabel = !string.IsNullOrWhiteSpace(Model.RelativeTimeText)
? Model.RelativeTimeText
: Model.StartDateTime?.ToString("dd MMM yyyy HH:mm")
?? Model.StartDateTime?.ToString("dd MMM yyyy")
?? "Time not set";
var selectedPurposeLabels = Model.ScenePurposeTypes
.Where(x => Model.SelectedPurposeTypeIds.Contains(x.ScenePurposeTypeID))
.Select(x => x.PurposeName)
.ToList();
var dependencyCount = Model.DependenciesThisSceneNeeds.Count + Model.DependenciesNeedingThisScene.Count;
}
<div class="scene-inspector-root">
<section class="inspector-section scene-overview-card" data-section-title="Overview" data-section-id="inspector-overview" data-inspector-search-text="@($"{Model.SceneTitle} {povCharacter} {primaryLocation} {timeLabel} {revisionStatus} {string.Join(' ', selectedPurposeLabels)}")">
<div>
<p class="eyebrow">Working context</p>
<h3>@(string.IsNullOrWhiteSpace(Model.SceneTitle) ? "Untitled scene" : Model.SceneTitle)</h3>
<p class="muted">
@if (Model.Chapter is not null)
{
<span>Chapter @Model.Chapter.ChapterNumber</span>
}
<span>Scene @Model.SceneNumber</span>
<span>@povCharacter</span>
<span>@primaryLocation</span>
</p>
</div>
<div class="overview-chip-grid">
<span class="overview-chip">@revisionStatus</span>
<span class="overview-chip">@timeLabel</span>
<span class="overview-chip @(Model.Warnings.Any() ? "attention" : "")">@Model.Warnings.Count warning@(Model.Warnings.Count == 1 ? "" : "s")</span>
<span class="overview-chip">@Model.SceneCharacters.Count character@(Model.SceneCharacters.Count == 1 ? "" : "s")</span>
<span class="overview-chip">@Model.AssetEvents.Count asset event@(Model.AssetEvents.Count == 1 ? "" : "s")</span>
<span class="overview-chip">@Model.ThreadEvents.Count thread event@(Model.ThreadEvents.Count == 1 ? "" : "s")</span>
<span class="overview-chip">@dependencyCount dependenc@(dependencyCount == 1 ? "y" : "ies")</span>
</div>
@if (selectedPurposeLabels.Any())
{
<div class="purpose-chip-row">
@foreach (var purpose in selectedPurposeLabels)
{
<span class="purpose-chip">@purpose</span>
}
</div>
}
@if (!string.IsNullOrWhiteSpace(Model.SceneOutcomeNotes))
{
<p class="overview-outcome">@Model.SceneOutcomeNotes</p>
}
</section>
<div class="inspector-quick-nav" data-inspector-nav>
<input class="form-control form-control-sm inspector-search" type="search" placeholder="Find in scene..." data-inspector-search />
<div class="inspector-nav-links">
<a href="#inspector-overview">Overview</a>
<a href="#inspector-timing">Time</a>
<a href="#inspector-purpose">Purpose</a>
<a href="#inspector-metrics">Metrics</a>
<a href="#inspector-warnings">Warnings</a>
<a href="#inspector-dependencies">Dependencies</a>
<a href="#inspector-move">Move</a>
<a href="#inspector-thread-events">Threads</a>
<a href="#inspector-asset-events">Assets</a>
<a href="#inspector-asset-locations">Locations</a>
<a href="#inspector-asset-custody">Custody</a>
<a href="#inspector-characters">Characters</a>
<a href="#inspector-character-attributes">Attributes</a>
<a href="#inspector-character-knowledge">Knowledge</a>
<a href="#inspector-relationships">Relationships</a>
</div>
</div>
<form asp-controller="Scenes" asp-action="Save" method="post" class="scene-inspector-form">
<input asp-for="SceneID" type="hidden" />
<input asp-for="ChapterID" type="hidden" />
<input asp-for="ReturnProjectID" type="hidden" />
<input asp-for="ReturnBookID" type="hidden" />
<input asp-for="ReturnToTimeline" type="hidden" />
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<div class="inspector-section" data-section-title="Timing & Structure" data-section-id="inspector-timing">
<h3>Timing & Structure</h3>
<div class="row g-2">
<div class="col-4">
<label asp-for="SceneNumber" class="form-label"></label>
<input asp-for="SceneNumber" class="form-control form-control-sm" />
</div>
<div class="col-8">
<label asp-for="RevisionStatusID" class="form-label"></label>
<select asp-for="RevisionStatusID" asp-items="Model.RevisionStatuses" class="form-select form-select-sm"></select>
</div>
<div class="col-12">
<label asp-for="SceneTitle" class="form-label"></label>
<input asp-for="SceneTitle" class="form-control" />
<span asp-validation-for="SceneTitle" class="text-danger"></span>
</div>
<div class="col-12">
<label asp-for="Summary" class="form-label"></label>
<textarea asp-for="Summary" class="form-control" rows="4"></textarea>
</div>
<div class="col-12">
<label asp-for="PrimaryLocationID" class="form-label"></label>
<select asp-for="PrimaryLocationID" asp-items="Model.LocationOptions" class="form-select form-select-sm">
<option value="">No primary location</option>
</select>
</div>
</div>
@if (Model.LocationConsistencyMessages.Any())
{
<div class="location-helper mt-2">
<h3>Location checks</h3>
<ul class="location-warning-list">
@foreach (var message in Model.LocationConsistencyMessages)
{
<li>@message</li>
}
</ul>
</div>
}
<h4 class="inspector-subheading">Time</h4>
<div class="row g-2">
<div class="col-sm-6">
<label asp-for="TimeModeID" class="form-label"></label>
<select asp-for="TimeModeID" asp-items="Model.TimeModes" class="form-select form-select-sm"></select>
</div>
<div class="col-sm-6">
<label asp-for="TimeConfidenceID" class="form-label"></label>
<select asp-for="TimeConfidenceID" asp-items="Model.TimeConfidences" class="form-select form-select-sm"></select>
</div>
<div class="col-sm-6">
<label asp-for="StartDateTime" class="form-label"></label>
<input asp-for="StartDateTime" class="form-control form-control-sm" />
</div>
<div class="col-sm-6">
<label asp-for="EndDateTime" class="form-label"></label>
<input asp-for="EndDateTime" class="form-control form-control-sm" />
</div>
<div class="col-sm-4">
<label asp-for="DurationAmount" class="form-label"></label>
<input asp-for="DurationAmount" class="form-control form-control-sm" />
</div>
<div class="col-sm-8">
<label asp-for="DurationUnitID" class="form-label"></label>
<select asp-for="DurationUnitID" asp-items="Model.DurationUnits" class="form-select form-select-sm">
<option value="">Not set</option>
</select>
</div>
<div class="col-12">
<label asp-for="RelativeTimeText" class="form-label"></label>
<input asp-for="RelativeTimeText" class="form-control form-control-sm" placeholder="e.g. Two days later" />
</div>
</div>
</div>
<div class="inspector-section" data-section-title="Scene Purpose & Outcome" data-section-id="inspector-purpose">
<h3>Purposes</h3>
<div class="purpose-list inspector-purpose-list">
@foreach (var purpose in Model.ScenePurposeTypes)
{
<label class="purpose-check">
@if (Model.SelectedPurposeTypeIds.Contains(purpose.ScenePurposeTypeID))
{
<input type="checkbox" name="SelectedPurposeTypeIds" value="@purpose.ScenePurposeTypeID" checked />
}
else
{
<input type="checkbox" name="SelectedPurposeTypeIds" value="@purpose.ScenePurposeTypeID" />
}
<span>@purpose.PurposeName</span>
</label>
}
</div>
<div class="purpose-notes-block mt-2">
<h3>Notes</h3>
<label asp-for="ScenePurposeNotes" class="form-label"></label>
<textarea asp-for="ScenePurposeNotes" class="form-control mb-2" rows="4"></textarea>
<label asp-for="SceneOutcomeNotes" class="form-label"></label>
<textarea asp-for="SceneOutcomeNotes" class="form-control" rows="4"></textarea>
</div>
</div>
<div class="inspector-section" data-section-title="Scene Metrics" data-section-id="inspector-metrics">
<h3>Scene Metrics</h3>
<div class="metric-slider-stack">
@for (var index = 0; index < Model.Metrics.Count; index++)
{
<div class="metric-slider">
<input asp-for="Metrics[index].MetricTypeID" type="hidden" />
<input asp-for="Metrics[index].MetricName" type="hidden" />
<input asp-for="Metrics[index].Description" type="hidden" />
<input asp-for="Metrics[index].MinValue" type="hidden" />
<input asp-for="Metrics[index].MaxValue" type="hidden" />
<div class="metric-slider-label">
<label asp-for="Metrics[index].Value">@Model.Metrics[index].MetricName</label>
<output>@Model.Metrics[index].Value</output>
</div>
<input asp-for="Metrics[index].Value" type="range" min="@Model.Metrics[index].MinValue" max="@Model.Metrics[index].MaxValue" class="form-range" oninput="this.previousElementSibling.querySelector('output').value = this.value" />
<input asp-for="Metrics[index].Notes" class="form-control form-control-sm" placeholder="Metric note" />
</div>
}
</div>
</div>
<div class="inspector-actions">
<button class="btn btn-primary" type="submit">Save scene</button>
@if (!Model.ReturnToTimeline)
{
<a class="btn btn-outline-secondary" asp-controller="Chapters" asp-action="Details" asp-route-id="@Model.ChapterID">Back to chapter</a>
}
</div>
</form>
<section class="inspector-section warning-inspector-section" data-section-title="Continuity Warnings" data-section-id="inspector-warnings">
<div class="inspector-section-heading">
<h3>Continuity Warnings</h3>
@if (Model.SceneID > 0 && Model.ReturnProjectID.HasValue)
{
<form asp-controller="Warnings" asp-action="ValidateScene" method="post">
<input type="hidden" name="projectId" value="@Model.ReturnProjectID" />
<input type="hidden" name="bookId" value="@Model.ReturnBookID" />
<input type="hidden" name="sceneId" value="@Model.SceneID" />
<input type="hidden" name="returnToTimeline" value="@Model.ReturnToTimeline.ToString().ToLowerInvariant()" />
<button class="btn btn-outline-primary btn-sm" type="submit">Validate scene</button>
</form>
}
</div>
@if (!Model.Warnings.Any())
{
<p class="muted">No active warnings for this scene.</p>
}
else
{
<div class="warning-mini-list">
@foreach (var warning in Model.Warnings)
{
<div class="warning-mini-card">
<span class="severity-pill @warning.SeverityName.ToLowerInvariant()">@warning.SeverityName</span>
<strong>@warning.WarningTypeName</strong>
<p>@warning.Message</p>
<div class="button-row compact-buttons">
<form asp-controller="Warnings" asp-action="Dismiss" method="post">
<input type="hidden" name="id" value="@warning.ContinuityWarningID" />
<input type="hidden" name="projectId" value="@Model.ReturnProjectID" />
<input type="hidden" name="bookId" value="@Model.ReturnBookID" />
<input type="hidden" name="sceneId" value="@Model.SceneID" />
<input type="hidden" name="returnToTimeline" value="@Model.ReturnToTimeline.ToString().ToLowerInvariant()" />
<button class="btn btn-outline-secondary btn-sm" type="submit">Dismiss</button>
</form>
<form asp-controller="Warnings" asp-action="MarkIntentional" method="post">
<input type="hidden" name="id" value="@warning.ContinuityWarningID" />
<input type="hidden" name="projectId" value="@Model.ReturnProjectID" />
<input type="hidden" name="bookId" value="@Model.ReturnBookID" />
<input type="hidden" name="sceneId" value="@Model.SceneID" />
<input type="hidden" name="returnToTimeline" value="@Model.ReturnToTimeline.ToString().ToLowerInvariant()" />
<button class="btn btn-outline-secondary btn-sm" type="submit">Intentional</button>
</form>
</div>
</div>
}
</div>
}
</section>
<section class="inspector-section scene-dependency-section" data-section-title="Scene Dependencies" data-section-id="inspector-dependencies">
<h3>Scene Dependencies</h3>
<div class="dependency-columns">
<div>
<p class="eyebrow">This scene depends on</p>
@if (!Model.DependenciesThisSceneNeeds.Any())
{
<p class="muted">No dependencies yet.</p>
}
else
{
@foreach (var dependency in Model.DependenciesThisSceneNeeds)
{
<div class="dependency-card">
<strong>Ch @dependency.SourceChapterNumber, Scene @dependency.SourceSceneNumber</strong>
<span>@dependency.SourceSceneTitle</span>
<span class="muted">@dependency.SceneDependencyTypeName @(dependency.IsHardDependency ? "/ hard" : "/ soft")</span>
<form asp-controller="Scenes" asp-action="DeleteSceneDependency" method="post">
<input type="hidden" name="id" value="@dependency.SceneDependencyID" />
<input type="hidden" name="sceneId" value="@Model.SceneID" />
<input type="hidden" name="projectId" value="@Model.ReturnProjectID" />
<input type="hidden" name="bookId" value="@Model.ReturnBookID" />
<button class="btn btn-outline-secondary btn-sm" type="submit">Remove</button>
</form>
</div>
}
}
</div>
<div>
<p class="eyebrow">Scenes depending on this</p>
@if (!Model.DependenciesNeedingThisScene.Any())
{
<p class="muted">No dependent scenes yet.</p>
}
else
{
@foreach (var dependency in Model.DependenciesNeedingThisScene)
{
<div class="dependency-card">
<strong>Ch @dependency.TargetChapterNumber, Scene @dependency.TargetSceneNumber</strong>
<span>@dependency.TargetSceneTitle</span>
<span class="muted">@dependency.SceneDependencyTypeName @(dependency.IsHardDependency ? "/ hard" : "/ soft")</span>
<form asp-controller="Scenes" asp-action="DeleteSceneDependency" method="post">
<input type="hidden" name="id" value="@dependency.SceneDependencyID" />
<input type="hidden" name="sceneId" value="@Model.SceneID" />
<input type="hidden" name="projectId" value="@Model.ReturnProjectID" />
<input type="hidden" name="bookId" value="@Model.ReturnBookID" />
<button class="btn btn-outline-secondary btn-sm" type="submit">Remove</button>
</form>
</div>
}
}
</div>
</div>
<form asp-controller="Scenes" asp-action="AddSceneDependency" method="post" class="asset-create-form mt-2">
<input type="hidden" name="ProjectID" value="@Model.ReturnProjectID" />
<input type="hidden" name="CurrentSceneID" value="@Model.SceneID" />
<input type="hidden" name="ReturnProjectID" value="@Model.ReturnProjectID" />
<input type="hidden" name="ReturnBookID" value="@Model.ReturnBookID" />
<div class="row g-2">
<div class="col-12">
<label class="form-label" for="DependencyDirection">Plain English</label>
<select class="form-select form-select-sm" id="DependencyDirection" name="Direction">
<option value="CurrentDependsOnRelated">This scene depends on another scene</option>
<option value="RelatedDependsOnCurrent">Another scene depends on this scene</option>
</select>
</div>
<div class="col-12">
<label class="form-label" for="RelatedSceneID">Related scene</label>
<select class="form-select form-select-sm" id="RelatedSceneID" name="RelatedSceneID" asp-items="Model.SceneOptions"></select>
</div>
<div class="col-8">
<label class="form-label" for="SceneDependencyTypeID">Dependency type</label>
<select class="form-select form-select-sm" id="SceneDependencyTypeID" name="SceneDependencyTypeID" asp-items="Model.SceneDependencyTypeOptions"></select>
</div>
<div class="col-4 d-flex align-items-end">
<label class="form-check mb-2">
<input class="form-check-input" name="IsHardDependency" type="checkbox" value="true" />
<span class="form-check-label">Hard</span>
</label>
</div>
<div class="col-12">
<textarea class="form-control form-control-sm" name="Description" rows="2" placeholder="Why this order matters"></textarea>
</div>
</div>
<button class="btn btn-outline-primary btn-sm mt-2" type="submit">Add dependency</button>
</form>
</section>
<section class="inspector-section scene-move-section" data-section-title="Move / Reorder" data-section-id="inspector-move">
<h3>Move / Reorder</h3>
<form asp-controller="Scenes" asp-action="PreviewMove" method="post" class="asset-create-form">
<input type="hidden" name="SceneID" value="@Model.SceneID" />
<input type="hidden" name="ReturnProjectID" value="@Model.ReturnProjectID" />
<input type="hidden" name="ReturnBookID" value="@Model.ReturnBookID" />
<div class="row g-2">
<div class="col-12">
<label class="form-label" for="TargetChapterID">Move to chapter</label>
<select class="form-select form-select-sm" id="TargetChapterID" name="TargetChapterID" asp-items="Model.ChapterOptions"></select>
</div>
<div class="col-6">
<label class="form-label" for="MovePosition">Position</label>
<select class="form-select form-select-sm" id="MovePosition" name="Position">
<option value="End">End of chapter</option>
<option value="Start">Start of chapter</option>
</select>
</div>
<div class="col-6 d-flex align-items-end">
<label class="form-check mb-2">
<input class="form-check-input" name="RenumberScenes" type="checkbox" value="true" />
<span class="form-check-label">Renumber scenes</span>
</label>
</div>
</div>
<button class="btn btn-outline-primary btn-sm mt-2" type="submit">Preview move</button>
</form>
<form asp-controller="Scenes" asp-action="RenumberChapter" method="post" class="mt-2">
<input type="hidden" name="chapterId" value="@Model.ChapterID" />
<input type="hidden" name="projectId" value="@Model.ReturnProjectID" />
<input type="hidden" name="bookId" value="@Model.ReturnBookID" />
<input type="hidden" name="sceneId" value="@Model.SceneID" />
<button class="btn btn-outline-secondary btn-sm" type="submit">Renumber this chapter</button>
</form>
</section>
<section class="inspector-section thread-events-section" data-section-title="Plot Thread Events" data-section-id="inspector-thread-events">
<h3>Plot Thread Events</h3>
@if (!Model.ThreadEvents.Any())
{
<p class="muted">No plot thread events attached to this scene yet.</p>
}
else
{
<div class="thread-event-list">
@foreach (var threadEvent in Model.ThreadEvents)
{
<div class="thread-event-item" style="--plot-colour:@threadEvent.Colour">
<div>
<span class="thread-event-marker">@threadEvent.MarkerText</span>
<strong>@threadEvent.EventTitle</strong>
<span class="muted">@threadEvent.PlotLineName / @threadEvent.ThreadTitle</span>
</div>
<form asp-controller="Scenes" asp-action="DeleteThreadEvent" method="post">
<input type="hidden" name="id" value="@threadEvent.ThreadEventID" />
<input type="hidden" name="sceneId" value="@Model.SceneID" />
<input type="hidden" name="projectId" value="@Model.ReturnProjectID" />
<input type="hidden" name="bookId" value="@Model.ReturnBookID" />
<button class="btn btn-outline-secondary btn-sm" type="submit">Remove</button>
</form>
</div>
}
</div>
}
<form asp-controller="Scenes" asp-action="AddThreadEvent" method="post" class="thread-event-create">
<input type="hidden" name="SceneID" value="@Model.SceneID" />
<input type="hidden" name="ReturnProjectID" value="@Model.ReturnProjectID" />
<input type="hidden" name="ReturnBookID" value="@Model.ReturnBookID" />
<p class="form-text mb-2">Choose an existing thread, or choose a plot line to create one from the event title.</p>
<div class="row g-2">
<div class="col-12">
<label class="form-label" for="PlotLineID">Plot line</label>
<select class="form-select form-select-sm" id="PlotLineID" name="PlotLineID" asp-items="Model.PlotLineOptions">
<option value="">Choose plot line</option>
</select>
</div>
<div class="col-12">
<label class="form-label" for="PlotThreadID">Existing plot thread</label>
<select class="form-select form-select-sm" id="PlotThreadID" name="PlotThreadID" asp-items="Model.PlotThreadOptions">
<option value="">Create or choose a thread</option>
</select>
</div>
<div class="col-8">
<label class="form-label" for="NewThreadTitle">New thread title</label>
<input class="form-control form-control-sm" id="NewThreadTitle" name="NewThreadTitle" placeholder="Optional" />
</div>
<div class="col-4">
<label class="form-label" for="NewThreadTypeID">Thread type</label>
<select class="form-select form-select-sm" id="NewThreadTypeID" name="NewThreadTypeID" asp-items="Model.ThreadTypeOptions">
<option value="">Default</option>
</select>
</div>
<div class="col-12">
<label class="form-label" for="EventTypeID">Event type</label>
<select class="form-select form-select-sm" id="EventTypeID" name="EventTypeID" asp-items="Model.ThreadEventTypeOptions"></select>
</div>
<div class="col-12">
<label class="form-label" for="EventTitle">Event title</label>
<input class="form-control form-control-sm" id="EventTitle" name="EventTitle" placeholder="Defaults to event type if blank" />
</div>
<div class="col-12">
<label class="form-label" for="EventDescription">Description</label>
<textarea class="form-control form-control-sm" id="EventDescription" name="EventDescription" rows="3"></textarea>
</div>
</div>
<button class="btn btn-outline-primary btn-sm mt-2" type="submit">Add thread event</button>
</form>
</section>
<section class="inspector-section asset-events-section" data-section-title="Story Assets" data-section-id="inspector-asset-events">
<h3>Story Assets</h3>
@if (!Model.AssetEvents.Any())
{
<p class="muted">No asset events attached to this scene yet.</p>
}
else
{
<div class="asset-event-list">
@foreach (var assetEvent in Model.AssetEvents)
{
<div class="asset-event-item">
<div>
<span class="asset-event-marker">@assetEvent.MarkerText</span>
<strong>@assetEvent.EventTitle</strong>
<span class="muted">@assetEvent.AssetName / @assetEvent.AssetEventTypeName</span>
@if (!string.IsNullOrWhiteSpace(assetEvent.ToStateName))
{
<span class="muted">State: @(assetEvent.FromStateName ?? "Any") -> @assetEvent.ToStateName</span>
}
</div>
<form asp-controller="Scenes" asp-action="DeleteAssetEvent" method="post">
<input type="hidden" name="id" value="@assetEvent.AssetEventID" />
<input type="hidden" name="sceneId" value="@Model.SceneID" />
<input type="hidden" name="projectId" value="@Model.ReturnProjectID" />
<input type="hidden" name="bookId" value="@Model.ReturnBookID" />
<button class="btn btn-outline-secondary btn-sm" type="submit">Remove</button>
</form>
</div>
}
</div>
}
<form asp-controller="Scenes" asp-action="AddAssetEvent" method="post" class="asset-create-form">
<input type="hidden" name="SceneID" value="@Model.SceneID" />
<input type="hidden" name="ReturnProjectID" value="@Model.ReturnProjectID" />
<input type="hidden" name="ReturnBookID" value="@Model.ReturnBookID" />
<p class="form-text mb-2">Choose an existing asset, or enter a new asset name to create it from this scene.</p>
<div class="row g-2">
<div class="col-12">
<label class="form-label" for="StoryAssetID">Asset</label>
<select class="form-select form-select-sm" id="StoryAssetID" name="StoryAssetID" asp-items="Model.AssetOptions">
<option value="">Create or choose an asset</option>
</select>
</div>
<div class="col-8">
<label class="form-label" for="NewAssetName">New asset name</label>
<input class="form-control form-control-sm" id="NewAssetName" name="NewAssetName" placeholder="Optional" />
</div>
<div class="col-4">
<label class="form-label" for="NewAssetKindID">Kind</label>
<select class="form-select form-select-sm" id="NewAssetKindID" name="NewAssetKindID" asp-items="Model.AssetKindOptions"></select>
</div>
<div class="col-12">
<label class="form-label" for="AssetEventTypeID">Event type</label>
<select class="form-select form-select-sm" id="AssetEventTypeID" name="AssetEventTypeID" asp-items="Model.AssetEventTypeOptions"></select>
</div>
<div class="col-6">
<label class="form-label" for="FromStateID">From state</label>
<select class="form-select form-select-sm" id="FromStateID" name="FromStateID" asp-items="Model.AssetStateOptions">
<option value="">Any</option>
</select>
</div>
<div class="col-6">
<label class="form-label" for="ToStateID">To state</label>
<select class="form-select form-select-sm" id="ToStateID" name="ToStateID" asp-items="Model.AssetStateOptions">
<option value="">No change</option>
</select>
</div>
<div class="col-12">
<label class="form-label" for="AssetEventTitle">Event title</label>
<input class="form-control form-control-sm" id="AssetEventTitle" name="EventTitle" placeholder="Defaults to event type if blank" />
</div>
<div class="col-12">
<label class="form-label" for="AssetEventDescription">Description</label>
<textarea class="form-control form-control-sm" id="AssetEventDescription" name="EventDescription" rows="3"></textarea>
</div>
</div>
<button class="btn btn-outline-primary btn-sm mt-2" type="submit">Add asset event</button>
</form>
</section>
<section class="inspector-section asset-events-section" data-section-title="Asset Locations" data-section-id="inspector-asset-locations">
<h3>Asset Locations</h3>
@if (!Model.SceneAssetLocations.Any())
{
<p class="muted">No assets have been placed in a location for this scene yet.</p>
}
else
{
<div class="asset-event-list">
@foreach (var assetLocation in Model.SceneAssetLocations)
{
<div class="asset-event-item">
<div>
<strong>@assetLocation.AssetName</strong>
<span class="muted" title="@assetLocation.LocationPath">@assetLocation.LocationName</span>
<span class="muted">@assetLocation.Description</span>
</div>
<form asp-controller="Scenes" asp-action="DeleteSceneAssetLocation" method="post">
<input type="hidden" name="id" value="@assetLocation.SceneAssetLocationID" />
<input type="hidden" name="sceneId" value="@Model.SceneID" />
<input type="hidden" name="projectId" value="@Model.ReturnProjectID" />
<input type="hidden" name="bookId" value="@Model.ReturnBookID" />
<button class="btn btn-outline-secondary btn-sm" type="submit">Remove</button>
</form>
</div>
}
</div>
}
<form asp-controller="Scenes" asp-action="AddSceneAssetLocation" method="post" class="asset-create-form">
<input type="hidden" name="SceneID" value="@Model.SceneID" />
<input type="hidden" name="ReturnProjectID" value="@Model.ReturnProjectID" />
<input type="hidden" name="ReturnBookID" value="@Model.ReturnBookID" />
<div class="row g-2">
<div class="col-12">
<label class="form-label" for="SceneAssetStoryAssetID">Asset</label>
<select class="form-select form-select-sm" id="SceneAssetStoryAssetID" name="StoryAssetID" asp-items="Model.AssetOptions"></select>
</div>
<div class="col-12">
<label class="form-label" for="SceneAssetLocationID">Location</label>
<select class="form-select form-select-sm" id="SceneAssetLocationID" name="LocationID" asp-items="Model.LocationOptions"></select>
</div>
<div class="col-12">
<input class="form-control form-control-sm" name="Description" placeholder="e.g. desk drawer, Beth's handbag" />
</div>
<div class="col-12">
<label class="form-check">
<input class="form-check-input" name="UpdateCurrentLocation" type="checkbox" value="true" checked />
<span class="form-check-label">Update asset's current location</span>
</label>
</div>
</div>
<button class="btn btn-outline-primary btn-sm mt-2" type="submit">Place asset</button>
</form>
</section>
<section class="inspector-section asset-events-section" data-section-title="Asset Custody" data-section-id="inspector-asset-custody">
<h3>Asset Custody</h3>
@if (!Model.AssetCustodyEvents.Any())
{
<p class="muted">No custody entries attached to this scene yet.</p>
}
else
{
<div class="asset-event-list">
@foreach (var custody in Model.AssetCustodyEvents)
{
<div class="asset-event-item custody">
<div>
<strong>@custody.CustodyEventTypeName</strong>
<span class="muted">@custody.AssetName</span>
<span class="muted">@custody.CustodianSummary</span>
</div>
<form asp-controller="Scenes" asp-action="DeleteAssetCustodyEvent" method="post">
<input type="hidden" name="id" value="@custody.AssetCustodyEventID" />
<input type="hidden" name="sceneId" value="@Model.SceneID" />
<input type="hidden" name="projectId" value="@Model.ReturnProjectID" />
<input type="hidden" name="bookId" value="@Model.ReturnBookID" />
<button class="btn btn-outline-secondary btn-sm" type="submit">Remove</button>
</form>
</div>
}
</div>
}
<form asp-controller="Scenes" asp-action="AddAssetCustodyEvent" method="post" class="asset-create-form">
<input type="hidden" name="SceneID" value="@Model.SceneID" />
<input type="hidden" name="ReturnProjectID" value="@Model.ReturnProjectID" />
<input type="hidden" name="ReturnBookID" value="@Model.ReturnBookID" />
<div class="row g-2">
<div class="col-12">
<label class="form-label" for="CustodyStoryAssetID">Asset</label>
<select class="form-select form-select-sm" id="CustodyStoryAssetID" name="StoryAssetID" asp-items="Model.AssetOptions"></select>
</div>
<div class="col-6">
<label class="form-label" for="AssetCustodyEventTypeID">Custody event</label>
<select class="form-select form-select-sm" id="AssetCustodyEventTypeID" name="AssetCustodyEventTypeID" asp-items="Model.AssetCustodyEventTypeOptions"></select>
</div>
<div class="col-6">
<label class="form-label" for="CustodyRoleID">Role</label>
<select class="form-select form-select-sm" id="CustodyRoleID" name="CustodyRoleID" asp-items="Model.CustodyRoleOptions"></select>
</div>
<div class="col-12">
<label class="form-label" for="CustodianCharacterIds">Existing custodians</label>
<select class="form-select form-select-sm" id="CustodianCharacterIds" name="CustodianCharacterIds" asp-items="Model.CharacterOptions" multiple></select>
</div>
<div class="col-12">
<label class="form-label" for="CustodianNames">Custodian names</label>
<input class="form-control form-control-sm" id="CustodianNames" name="CustodianNames" placeholder="Beth, Maggie" />
</div>
<div class="col-12">
<label class="form-label" for="CustodyDescription">Description</label>
<textarea class="form-control form-control-sm" id="CustodyDescription" name="Description" rows="3"></textarea>
</div>
</div>
<button class="btn btn-outline-primary btn-sm mt-2" type="submit">Add custody</button>
</form>
</section>
<section class="inspector-section character-section" data-section-title="Characters" data-section-id="inspector-characters">
<h3>Characters</h3>
@if (!Model.SceneCharacters.Any())
{
<p class="muted">No characters attached to this scene yet.</p>
}
else
{
<div class="asset-event-list">
@foreach (var character in Model.SceneCharacters)
{
<div class="asset-event-item character-item">
<div>
<strong>@character.CharacterName</strong>
<span class="muted">@character.RoleInSceneTypeName / @character.PresenceTypeName</span>
@if (!string.IsNullOrWhiteSpace(character.LocationName))
{
<span class="muted" title="@character.LocationPath">Location: @character.LocationName</span>
}
@if (!string.IsNullOrWhiteSpace(character.EntryLocationName) || !string.IsNullOrWhiteSpace(character.ExitLocationName))
{
<span class="muted">In: @(character.EntryLocationName ?? "not set") / Out: @(character.ExitLocationName ?? "not set")</span>
}
<span class="muted">@character.OutfitDescription @character.PhysicalCondition @character.EmotionalState</span>
</div>
<form asp-controller="Scenes" asp-action="DeleteSceneCharacter" method="post">
<input type="hidden" name="id" value="@character.SceneCharacterID" />
<input type="hidden" name="sceneId" value="@Model.SceneID" />
<input type="hidden" name="projectId" value="@Model.ReturnProjectID" />
<input type="hidden" name="bookId" value="@Model.ReturnBookID" />
<button class="btn btn-outline-secondary btn-sm" type="submit">Remove</button>
</form>
</div>
}
</div>
}
<form asp-controller="Scenes" asp-action="AddSceneCharacter" method="post" class="asset-create-form">
<input type="hidden" name="SceneID" value="@Model.SceneID" />
<input type="hidden" name="ReturnProjectID" value="@Model.ReturnProjectID" />
<input type="hidden" name="ReturnBookID" value="@Model.ReturnBookID" />
<div class="row g-2">
<div class="col-12">
<label class="form-label" for="CharacterID">Character</label>
<select class="form-select form-select-sm" id="CharacterID" name="CharacterID" asp-items="Model.CharacterOptions">
<option value="">Create or choose a character</option>
</select>
</div>
<div class="col-12">
<label class="form-label" for="NewCharacterName">New character name</label>
<input class="form-control form-control-sm" id="NewCharacterName" name="NewCharacterName" placeholder="Optional" />
</div>
<div class="col-6">
<label class="form-label" for="RoleInSceneTypeID">Role</label>
<select class="form-select form-select-sm" id="RoleInSceneTypeID" name="RoleInSceneTypeID" asp-items="Model.CharacterRoleOptions">
<option value="">Not set</option>
</select>
</div>
<div class="col-6">
<label class="form-label" for="PresenceTypeID">Presence</label>
<select class="form-select form-select-sm" id="PresenceTypeID" name="PresenceTypeID" asp-items="Model.PresenceTypeOptions">
<option value="">Not set</option>
</select>
</div>
<div class="col-12">
<label class="form-label" for="CharacterLocationID">Current location</label>
<select class="form-select form-select-sm" id="CharacterLocationID" name="LocationID" asp-items="Model.LocationOptions">
<option value="">Not set</option>
</select>
</div>
<div class="col-6">
<label class="form-label" for="EntryLocationID">Enters from</label>
<select class="form-select form-select-sm" id="EntryLocationID" name="EntryLocationID" asp-items="Model.LocationOptions">
<option value="">Not set</option>
</select>
</div>
<div class="col-6">
<label class="form-label" for="ExitLocationID">Exits to</label>
<select class="form-select form-select-sm" id="ExitLocationID" name="ExitLocationID" asp-items="Model.LocationOptions">
<option value="">Not set</option>
</select>
</div>
<div class="col-12"><input class="form-control form-control-sm" name="AppearanceNotes" placeholder="Appearance notes" /></div>
<div class="col-12"><input class="form-control form-control-sm" name="OutfitDescription" placeholder="Outfit description" /></div>
<div class="col-6"><input class="form-control form-control-sm" name="PhysicalCondition" placeholder="Physical condition" /></div>
<div class="col-6"><input class="form-control form-control-sm" name="EmotionalState" placeholder="Emotional state" /></div>
<div class="col-12"><textarea class="form-control form-control-sm" name="KnowledgeNotes" rows="2" placeholder="Knowledge notes"></textarea></div>
</div>
<button class="btn btn-outline-primary btn-sm mt-2" type="submit">Add character</button>
</form>
</section>
<section class="inspector-section character-section" data-section-title="Character Attributes" data-section-id="inspector-character-attributes">
<h3>Character Attributes</h3>
@if (Model.CharacterAttributeEvents.Any())
{
<div class="asset-event-list">
@foreach (var item in Model.CharacterAttributeEvents)
{
<div class="asset-event-item character-item">
<div>
<strong>@item.CharacterName: @item.AttributeName</strong>
<span class="muted">@item.AttributeValue</span>
<span class="muted">@item.Description</span>
</div>
</div>
}
</div>
}
else
{
<p class="muted">Character attribute history for this scene will appear here when available.</p>
}
<form asp-controller="Scenes" asp-action="AddCharacterAttributeEvent" method="post" class="asset-create-form">
<input type="hidden" name="SceneID" value="@Model.SceneID" />
<input type="hidden" name="ReturnProjectID" value="@Model.ReturnProjectID" />
<input type="hidden" name="ReturnBookID" value="@Model.ReturnBookID" />
<div class="row g-2">
<div class="col-12">
<select class="form-select form-select-sm" name="CharacterID" asp-items="Model.CharacterOptions"></select>
</div>
<div class="col-12">
<select class="form-select form-select-sm" name="CharacterAttributeTypeID" asp-items="Model.CharacterAttributeTypeOptions"></select>
</div>
<div class="col-12">
<input class="form-control form-control-sm" name="AttributeValue" placeholder="Value, e.g. Bruised ribs" />
</div>
<div class="col-12">
<textarea class="form-control form-control-sm" name="Description" rows="2" placeholder="Description"></textarea>
</div>
</div>
<button class="btn btn-outline-primary btn-sm mt-2" type="submit">Add attribute</button>
</form>
</section>
<section class="inspector-section character-section" data-section-title="Character Knowledge" data-section-id="inspector-character-knowledge">
<h3>Character Knowledge</h3>
@if (Model.CharacterKnowledge.Any())
{
<div class="asset-event-list">
@foreach (var item in Model.CharacterKnowledge)
{
<div class="asset-event-item character-item">
<div>
<strong>@item.CharacterName @item.KnowledgeStateName</strong>
<span class="muted">@(item.AssetName ?? item.ThreadTitle)</span>
<span class="muted">@item.Description</span>
</div>
</div>
}
</div>
}
<form asp-controller="Scenes" asp-action="AddCharacterKnowledge" method="post" class="asset-create-form">
<input type="hidden" name="SceneID" value="@Model.SceneID" />
<input type="hidden" name="ReturnProjectID" value="@Model.ReturnProjectID" />
<input type="hidden" name="ReturnBookID" value="@Model.ReturnBookID" />
<div class="row g-2">
<div class="col-12"><select class="form-select form-select-sm" name="CharacterID" asp-items="Model.CharacterOptions"></select></div>
<div class="col-12">
<select class="form-select form-select-sm" name="StoryAssetID" asp-items="Model.AssetOptions">
<option value="">No asset</option>
</select>
</div>
<div class="col-12">
<select class="form-select form-select-sm" name="PlotThreadID" asp-items="Model.PlotThreadOptions">
<option value="">No plot thread</option>
</select>
</div>
<div class="col-12"><select class="form-select form-select-sm" name="KnowledgeStateID" asp-items="Model.KnowledgeStateOptions"></select></div>
<div class="col-12"><textarea class="form-control form-control-sm" name="Description" rows="2" placeholder="Description"></textarea></div>
</div>
<button class="btn btn-outline-primary btn-sm mt-2" type="submit">Add knowledge</button>
</form>
</section>
<section class="inspector-section character-section" data-section-title="Relationship Events" data-section-id="inspector-relationships">
<h3>Relationship Events</h3>
@if (Model.RelationshipEvents.Any())
{
<div class="asset-event-list">
@foreach (var item in Model.RelationshipEvents)
{
<div class="asset-event-item character-item">
<div>
<strong>@item.CharacterAName -> @item.CharacterBName</strong>
<span class="muted">@item.RelationshipStateName / intensity @item.Intensity</span>
<span class="muted">@item.Description</span>
</div>
</div>
}
</div>
}
<form asp-controller="Scenes" asp-action="AddRelationshipEvent" method="post" class="asset-create-form">
<input type="hidden" name="SceneID" value="@Model.SceneID" />
<input type="hidden" name="ReturnProjectID" value="@Model.ReturnProjectID" />
<input type="hidden" name="ReturnBookID" value="@Model.ReturnBookID" />
<div class="row g-2">
<div class="col-12">
<select class="form-select form-select-sm" name="CharacterRelationshipID" asp-items="Model.RelationshipOptions">
<option value="">Create or choose relationship</option>
</select>
</div>
<div class="col-6"><select class="form-select form-select-sm" name="CharacterAID" asp-items="Model.CharacterOptions"><option value="">Character A</option></select></div>
<div class="col-6"><select class="form-select form-select-sm" name="CharacterBID" asp-items="Model.CharacterOptions"><option value="">Character B</option></select></div>
<div class="col-12"><select class="form-select form-select-sm" name="RelationshipTypeID" asp-items="Model.RelationshipTypeOptions"><option value="">Relationship type</option></select></div>
<div class="col-8"><select class="form-select form-select-sm" name="RelationshipStateID" asp-items="Model.RelationshipStateOptions"></select></div>
<div class="col-4"><input class="form-control form-control-sm" name="Intensity" type="number" min="1" max="10" placeholder="1-10" /></div>
<div class="col-12">
<label class="form-check">
<input class="form-check-input" name="IsKnownToOtherCharacter" type="checkbox" value="true" />
<span class="form-check-label">Other character knows</span>
</label>
</div>
<div class="col-12"><textarea class="form-control form-control-sm" name="Description" rows="2" placeholder="Description"></textarea></div>
</div>
<button class="btn btn-outline-primary btn-sm mt-2" type="submit">Add relationship event</button>
</form>
</section>
</div>

View File

@ -0,0 +1,25 @@
@model ErrorViewModel
@{
ViewData["Title"] = "Error";
}
<h1 class="text-danger">Error.</h1>
<h2 class="text-danger">An error occurred while processing your request.</h2>
@if (Model.ShowRequestId)
{
<p>
<strong>Request ID:</strong> <code>@Model.RequestId</code>
</p>
}
<h3>Development Mode</h3>
<p>
Swapping to <strong>Development</strong> environment will display more detailed information about the error that occurred.
</p>
<p>
<strong>The Development environment shouldn't be enabled for deployed applications.</strong>
It can result in displaying sensitive information from exceptions to end users.
For local debugging, enable the <strong>Development</strong> environment by setting the <strong>ASPNETCORE_ENVIRONMENT</strong> environment variable to <strong>Development</strong>
and restarting the app.
</p>

View File

@ -0,0 +1,46 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>@ViewData["Title"] - PlotLine</title>
<script type="importmap"></script>
<link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.min.css" />
<link rel="stylesheet" href="~/css/site.css" asp-append-version="true" />
<link rel="stylesheet" href="~/PlotLine.styles.css" asp-append-version="true" />
</head>
<body>
<header>
<nav class="navbar navbar-expand-sm navbar-toggleable-sm navbar-light bg-white border-bottom plotline-nav mb-3">
<div class="container-fluid">
<a class="navbar-brand fw-semibold" asp-area="" asp-controller="Projects" asp-action="Index">PlotLine</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target=".navbar-collapse" aria-controls="navbarSupportedContent"
aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="navbar-collapse collapse d-sm-inline-flex justify-content-between">
<ul class="navbar-nav flex-grow-1">
<li class="nav-item">
<a class="nav-link text-dark" asp-area="" asp-controller="Projects" asp-action="Index">Projects</a>
</li>
<li class="nav-item">
<a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Index">About</a>
</li>
</ul>
</div>
</div>
</nav>
</header>
<div class="container-fluid plotline-shell @ViewData["ShellClass"]">
<main role="main" class="pb-3">
@RenderBody()
</main>
</div>
<script src="~/lib/jquery/dist/jquery.min.js"></script>
<script src="~/lib/bootstrap/dist/js/bootstrap.bundle.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.9/dist/chart.umd.min.js"></script>
<script src="~/js/site.js" asp-append-version="true"></script>
@await RenderSectionAsync("Scripts", required: false)
</body>
</html>

View File

@ -0,0 +1,48 @@
/* Please see documentation at https://learn.microsoft.com/aspnet/core/client-side/bundling-and-minification
for details on configuring this project to bundle and minify static web assets. */
a.navbar-brand {
white-space: normal;
text-align: center;
word-break: break-all;
}
a {
color: #0077cc;
}
.btn-primary {
color: #fff;
background-color: #1b6ec2;
border-color: #1861ac;
}
.nav-pills .nav-link.active, .nav-pills .show > .nav-link {
color: #fff;
background-color: #1b6ec2;
border-color: #1861ac;
}
.border-top {
border-top: 1px solid #e5e5e5;
}
.border-bottom {
border-bottom: 1px solid #e5e5e5;
}
.box-shadow {
box-shadow: 0 .25rem .75rem rgba(0, 0, 0, .05);
}
button.accept-policy {
font-size: 1rem;
line-height: inherit;
}
.footer {
position: absolute;
bottom: 0;
width: 100%;
white-space: nowrap;
line-height: 60px;
}

View File

@ -0,0 +1,2 @@
<script src="~/lib/jquery-validation/dist/jquery.validate.min.js"></script>
<script src="~/lib/jquery-validation-unobtrusive/dist/jquery.validate.unobtrusive.min.js"></script>

View File

@ -0,0 +1,158 @@
@model StoryAssetDetailViewModel
@{
ViewData["Title"] = Model.Asset.AssetName;
var firstAssetEvent = Model.Events.FirstOrDefault();
var firstCustodyEvent = Model.CustodyEvents.FirstOrDefault();
var firstSceneId = firstAssetEvent?.SceneID ?? firstCustodyEvent?.SceneID;
}
<nav class="breadcrumb-trail" aria-label="Breadcrumb">
<a asp-controller="Projects" asp-action="Index">Projects</a>
<a asp-controller="Projects" asp-action="Details" asp-route-id="@Model.Project.ProjectID">@Model.Project.ProjectName</a>
<a asp-action="Index" asp-route-projectId="@Model.Project.ProjectID">Story assets</a>
<span>@Model.Asset.AssetName</span>
</nav>
<section class="context-header">
<div class="context-title">
<span class="eyebrow">Project context</span>
<strong>@Model.Project.ProjectName</strong>
</div>
<nav class="context-nav" aria-label="Project navigation">
<a asp-controller="Timeline" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID" asp-route-selectedSceneId="@firstSceneId">Show on timeline</a>
<a asp-controller="Analytics" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID">Story health</a>
<a asp-controller="Warnings" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID">Warnings</a>
<a asp-action="Index" asp-route-projectId="@Model.Project.ProjectID">Assets</a>
</nav>
</section>
<div class="page-heading">
<div>
<p class="eyebrow">@Model.Asset.KindName</p>
<h1>@Model.Asset.AssetName</h1>
<p class="lead-text">@Model.Asset.Description</p>
</div>
<div class="button-row">
<a class="btn btn-outline-secondary" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID">Story assets</a>
<a class="btn btn-outline-secondary" asp-controller="Timeline" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID" asp-route-selectedSceneId="@firstSceneId">Back to timeline</a>
<a class="btn btn-outline-primary" asp-action="Edit" asp-route-id="@Model.Asset.StoryAssetID">Edit asset</a>
</div>
</div>
<section class="asset-detail-grid">
<article class="plain-card">
<h2>Current state</h2>
<p><strong>Importance:</strong> @Model.Asset.Importance / 10</p>
<p><strong>State:</strong> @(Model.Asset.CurrentStateName ?? "No state")</p>
<p><strong>Location:</strong> @(Model.Asset.CurrentLocationPath ?? "No location")</p>
<p><strong>Resolved:</strong> @(Model.Asset.IsResolved ? "Yes" : "No")</p>
</article>
<article class="plain-card">
<h2>Dependencies</h2>
@if (!Model.Dependencies.Any())
{
<p class="muted">No dependencies yet.</p>
}
else
{
<div class="asset-mini-list">
@foreach (var dependency in Model.Dependencies)
{
<div class="asset-mini-item">
<strong>@dependency.SourceAssetName @dependency.DependencyTypeName.ToLowerInvariant() @dependency.TargetAssetName</strong>
<span>@dependency.SourceRequiredStateName before @dependency.TargetBlockedStateName</span>
<form asp-action="DeleteDependency" method="post">
<input type="hidden" name="id" value="@dependency.AssetDependencyID" />
<input type="hidden" name="returnAssetId" value="@Model.Asset.StoryAssetID" />
<button class="btn btn-outline-secondary btn-sm" type="submit">Remove</button>
</form>
</div>
}
</div>
}
<form asp-action="AddDependency" method="post" class="asset-create-form">
<input type="hidden" name="ProjectID" value="@Model.Project.ProjectID" />
<input type="hidden" name="returnAssetId" value="@Model.Asset.StoryAssetID" />
<div class="row g-2">
<div class="col-md-6">
<label class="form-label">Source asset</label>
<select class="form-select form-select-sm" name="SourceAssetID" asp-items="Model.AssetOptions"></select>
</div>
<div class="col-md-6">
<label class="form-label">Target asset</label>
<select class="form-select form-select-sm" name="TargetAssetID" asp-items="Model.AssetOptions"></select>
</div>
<div class="col-md-4">
<label class="form-label">Dependency</label>
<select class="form-select form-select-sm" name="AssetDependencyTypeID" asp-items="Model.AssetDependencyTypeOptions"></select>
</div>
<div class="col-md-4">
<label class="form-label">Source required state</label>
<select class="form-select form-select-sm" name="SourceRequiredStateID" asp-items="Model.AssetStateOptions">
<option value="">Any</option>
</select>
</div>
<div class="col-md-4">
<label class="form-label">Target blocked state</label>
<select class="form-select form-select-sm" name="TargetBlockedStateID" asp-items="Model.AssetStateOptions">
<option value="">Any</option>
</select>
</div>
<div class="col-12">
<label class="form-label">Description</label>
<textarea class="form-control form-control-sm" name="Description" rows="2"></textarea>
</div>
</div>
<button class="btn btn-outline-primary btn-sm mt-2" type="submit">Add dependency</button>
</form>
</article>
</section>
<section class="list-section mt-3">
<h2>Events across scenes</h2>
@if (!Model.Events.Any())
{
<p class="muted">No scene events yet.</p>
}
else
{
<div class="asset-mini-list">
@foreach (var assetEvent in Model.Events)
{
<div class="asset-mini-item">
<span class="asset-event-marker">@assetEvent.MarkerText</span>
<div>
<strong>@assetEvent.EventTitle</strong>
<span>@assetEvent.BookTitle / Ch @assetEvent.ChapterNumber / Scene @assetEvent.SceneNumber: @assetEvent.SceneTitle</span>
<span>@assetEvent.FromStateName -> @assetEvent.ToStateName</span>
<a asp-controller="Timeline" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID" asp-route-selectedSceneId="@assetEvent.SceneID">Open in timeline</a>
</div>
</div>
}
</div>
}
</section>
<section class="list-section mt-3">
<h2>Custody</h2>
@if (!Model.CustodyEvents.Any())
{
<p class="muted">No custody events yet.</p>
}
else
{
<div class="asset-mini-list">
@foreach (var custody in Model.CustodyEvents)
{
<div class="asset-mini-item">
<strong>@custody.CustodyEventTypeName</strong>
<span>@custody.CustodianSummary</span>
<span>@custody.BookTitle / Ch @custody.ChapterNumber / Scene @custody.SceneNumber: @custody.SceneTitle</span>
<a asp-controller="Timeline" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID" asp-route-selectedSceneId="@custody.SceneID">Open in timeline</a>
</div>
}
</div>
}
</section>

View File

@ -0,0 +1,64 @@
@model StoryAssetEditViewModel
@{
ViewData["Title"] = Model.StoryAssetID == 0 ? "New Story Asset" : "Edit Story Asset";
}
<div class="page-heading compact">
<div>
<p class="eyebrow">Story asset</p>
<h1>@ViewData["Title"]</h1>
</div>
</div>
<form asp-action="Save" method="post" class="edit-panel">
<input asp-for="StoryAssetID" type="hidden" />
<input asp-for="ProjectID" type="hidden" />
<div class="row g-3">
<div class="col-md-8">
<label asp-for="AssetName" class="form-label"></label>
<input asp-for="AssetName" class="form-control" />
<span asp-validation-for="AssetName" class="text-danger"></span>
</div>
<div class="col-md-4">
<label asp-for="AssetKindID" class="form-label"></label>
<select asp-for="AssetKindID" asp-items="Model.AssetKindOptions" class="form-select"></select>
</div>
<div class="col-md-4">
<label asp-for="Importance" class="form-label"></label>
<input asp-for="Importance" class="form-range" min="1" max="10" />
<span asp-validation-for="Importance" class="text-danger"></span>
</div>
<div class="col-md-4">
<label asp-for="CurrentStateID" class="form-label"></label>
<select asp-for="CurrentStateID" asp-items="Model.AssetStateOptions" class="form-select">
<option value="">No state</option>
</select>
</div>
<div class="col-md-4">
<label asp-for="CurrentLocationID" class="form-label"></label>
<select asp-for="CurrentLocationID" asp-items="Model.LocationOptions" class="form-select">
<option value="">No location</option>
</select>
</div>
<div class="col-md-4 d-flex align-items-end">
<div class="form-check mb-2">
<input asp-for="IsResolved" class="form-check-input" />
<label asp-for="IsResolved" class="form-check-label"></label>
</div>
</div>
<div class="col-12">
<label asp-for="Description" class="form-label"></label>
<textarea asp-for="Description" class="form-control" rows="5"></textarea>
</div>
</div>
<div class="button-row mt-3">
<button class="btn btn-primary" type="submit">Save asset</button>
<a class="btn btn-outline-secondary" asp-action="Index" asp-route-projectId="@Model.ProjectID">Cancel</a>
</div>
</form>
@section Scripts {
<partial name="_ValidationScriptsPartial" />
}

View File

@ -0,0 +1,59 @@
@model StoryAssetListViewModel
@{
ViewData["Title"] = "Story Assets";
}
<section class="context-header">
<div class="context-title"><span class="eyebrow">Project context</span><strong>@Model.Project.ProjectName</strong></div>
<nav class="context-nav" aria-label="Project navigation">
<a asp-controller="Projects" asp-action="Details" asp-route-id="@Model.Project.ProjectID">Project</a>
<a asp-controller="Timeline" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID">Timeline</a>
<a asp-controller="Analytics" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID">Story health</a>
<a asp-controller="Warnings" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID">Warnings</a>
<a asp-controller="Characters" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID">Characters</a>
<a asp-controller="Locations" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID">Locations</a>
<a asp-controller="PlotThreads" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID">Threads</a>
</nav>
</section>
<div class="page-heading">
<div>
<p class="eyebrow">Story assets</p>
<h1>@Model.Project.ProjectName</h1>
</div>
<div class="button-row">
<a class="btn btn-outline-secondary" asp-controller="Projects" asp-action="Details" asp-route-id="@Model.Project.ProjectID">Project</a>
<a class="btn btn-primary" asp-action="Create" asp-route-projectId="@Model.Project.ProjectID">Add asset</a>
</div>
</div>
<section class="list-section">
@if (!Model.Assets.Any())
{
<p class="muted">No story assets yet. Add an object, clue, secret or other story item to track it across scenes.</p>
}
else
{
<div class="asset-card-grid">
@foreach (var asset in Model.Assets)
{
<article class="asset-card">
<div>
<p class="eyebrow">@asset.KindName</p>
<h2><a asp-action="Details" asp-route-id="@asset.StoryAssetID">@asset.AssetName</a></h2>
<p class="muted">@asset.Description</p>
</div>
<div class="asset-card-footer">
<span class="status-pill">Importance @asset.Importance</span>
<span class="purpose-chip">@(asset.CurrentStateName ?? "No state")</span>
@if (asset.IsResolved)
{
<span class="purpose-chip">Resolved</span>
}
<a class="btn btn-outline-secondary btn-sm ms-auto" asp-action="Edit" asp-route-id="@asset.StoryAssetID">Edit</a>
</div>
</article>
}
</div>
}
</section>

View File

@ -0,0 +1,370 @@
@model TimelineViewModel
@{
ViewData["Title"] = "Timeline";
ViewData["ShellClass"] = "plotline-shell-wide";
var selectedBook = Model.Books.FirstOrDefault(x => x.Book.BookID == Model.SelectedBookID)?.Book;
var timelineMetricScenes = Model.Books
.SelectMany(book => book.Chapters.SelectMany(chapter => chapter.Scenes.Select(scene => new
{
sceneId = scene.SceneID,
axisLabel = $"{chapter.Chapter.ChapterNumber:g}.{scene.SceneNumber:g}",
chapterLabel = $"Chapter {chapter.Chapter.ChapterNumber:g}",
sceneNumber = scene.SceneNumber.ToString("g"),
sceneTitle = scene.SceneTitle,
bookTitle = book.Book.BookTitle,
timelineUrl = Url.Action("Index", "Timeline", new { projectId = Model.Project.ProjectID, bookId = book.Book.BookID, selectedSceneId = scene.SceneID })
})))
.ToList();
var timelineMetricShapePayload = new
{
scenes = timelineMetricScenes,
metrics = Model.MetricGraphs.Select(graph => new
{
metricName = graph.MetricName,
points = graph.Points.Select(point => new { sceneId = point.SceneID, value = point.Value })
})
};
}
<section class="context-header">
<div class="context-title">
<span class="eyebrow">Workspace context</span>
<strong>@Model.Project.ProjectName@if (selectedBook is not null) { <span> / @selectedBook.BookTitle</span> }</strong>
</div>
<nav class="context-nav" aria-label="Workspace navigation">
<a asp-controller="Projects" asp-action="Details" asp-route-id="@Model.Project.ProjectID">Project</a>
<a asp-controller="Timeline" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID" asp-route-bookId="@Model.SelectedBookID">Timeline</a>
<a asp-controller="Analytics" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID" asp-route-bookId="@Model.SelectedBookID">Story health</a>
<a asp-controller="Warnings" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID" asp-route-bookId="@Model.SelectedBookID">Warnings</a>
<a asp-controller="Characters" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID">Characters</a>
<a asp-controller="StoryAssets" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID">Assets</a>
<a asp-controller="Locations" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID">Locations</a>
<a asp-controller="PlotThreads" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID">Threads</a>
</nav>
</section>
<div class="workspace-heading">
<div>
<p class="eyebrow">Story timeline</p>
<h1>@Model.Project.ProjectName</h1>
</div>
<form method="get" class="timeline-filter">
<input type="hidden" name="projectId" value="@Model.Project.ProjectID" />
<select name="bookId" asp-items="Model.BookOptions" class="form-select">
<option value="">All books</option>
</select>
<button class="btn btn-outline-primary" type="submit">View</button>
</form>
<div class="button-row">
<form asp-controller="Warnings" asp-action="ValidateProject" method="post">
<input type="hidden" name="projectId" value="@Model.Project.ProjectID" />
<input type="hidden" name="bookId" value="@Model.SelectedBookID" />
<button class="btn btn-outline-primary" type="submit">Validate project</button>
</form>
@if (Model.SelectedBookID.HasValue)
{
<form asp-controller="Warnings" asp-action="ValidateBook" method="post">
<input type="hidden" name="projectId" value="@Model.Project.ProjectID" />
<input type="hidden" name="bookId" value="@Model.SelectedBookID" />
<button class="btn btn-outline-primary" type="submit">Validate book</button>
</form>
}
<a class="btn btn-outline-secondary" asp-controller="Warnings" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID" asp-route-bookId="@Model.SelectedBookID">Warnings</a>
<a class="btn btn-outline-secondary" asp-controller="Analytics" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID" asp-route-bookId="@Model.SelectedBookID">Story health</a>
</div>
</div>
@if (!Model.Books.Any())
{
<section class="empty-panel">
<h2>No timeline yet</h2>
<p>Add books, chapters and scenes to see them arranged here in narrative order.</p>
</section>
}
else
{
<div class="workspace-layout">
<main class="timeline-workspace">
<section class="timeline-shell workspace-timeline">
@foreach (var book in Model.Books)
{
<div class="timeline-book">
<div class="timeline-book-row">
<div class="timeline-book-title">Book @book.Book.BookNumber: @book.Book.BookTitle</div>
<div class="timeline-chapters">
@foreach (var chapter in book.Chapters)
{
<div class="timeline-chapter" style="--chapter-scene-count:@Math.Max(chapter.Scenes.Count, 1)">
<div class="timeline-chapter-title">
<span>Chapter @chapter.Chapter.ChapterNumber</span>
<strong>@chapter.Chapter.ChapterTitle</strong>
</div>
<div class="timeline-scenes">
@if (!chapter.Scenes.Any())
{
<div class="timeline-empty-scene">No scenes</div>
}
else
{
@foreach (var scene in chapter.Scenes)
{
var selectedClass = scene.SceneID == Model.SelectedSceneID ? "selected" : "";
<a class="timeline-scene-card @selectedClass"
asp-controller="Timeline"
asp-action="Index"
asp-route-projectId="@Model.Project.ProjectID"
asp-route-bookId="@Model.SelectedBookID"
asp-route-selectedSceneId="@scene.SceneID">
<span class="scene-number">Scene @scene.SceneNumber</span>
<strong>@scene.SceneTitle</strong>
<div class="scene-badge-row">
<span class="status-pill">@scene.RevisionStatusName</span>
@if (scene.WarningCount > 0)
{
<span class="warning-placeholder active">@scene.WarningCount warning@(scene.WarningCount == 1 ? "" : "s")</span>
}
else
{
<span class="warning-placeholder">Warnings 0</span>
}
@if (scene.DependencyCount > 0)
{
<span class="dependency-placeholder">@scene.DependencyCount dep@(scene.DependencyCount == 1 ? "" : "s")</span>
}
</div>
<span class="time-label">@scene.TimeLabel</span>
@if (!string.IsNullOrWhiteSpace(scene.PrimaryLocationName))
{
<span class="location-pill" title="@scene.PrimaryLocationPath">@scene.PrimaryLocationName</span>
}
@if (scene.PurposeLabels.Any())
{
<div class="purpose-chip-row">
@foreach (var purpose in scene.PurposeLabels.Take(3))
{
<span class="purpose-chip">@purpose.PurposeName</span>
}
</div>
}
<span class="scene-summary">@scene.Summary</span>
</a>
}
}
</div>
</div>
}
</div>
</div>
</div>
}
@if (Model.PlotLanes.Any() && Model.OrderedScenes.Any())
{
<div class="plot-lane-board" style="--scene-count:@Model.OrderedScenes.Count">
<div class="plot-lane-board-heading">
<span>Plot line lanes</span>
<small>Thread event markers by scene</small>
</div>
@foreach (var lane in Model.PlotLanes)
{
<div class="plot-lane-row" style="--plot-colour:@lane.PlotLine.Colour">
<a class="plot-lane-label" asp-controller="PlotLines" asp-action="Edit" asp-route-id="@lane.PlotLine.PlotLineID">
<span class="colour-swatch" style="background:@lane.PlotLine.Colour"></span>
<strong>@lane.PlotLine.PlotLineName</strong>
</a>
<div class="plot-lane-slots">
@foreach (var slot in lane.SceneSlots)
{
<div class="plot-lane-slot" title="Scene @slot.Scene.SceneNumber: @slot.Scene.SceneTitle">
@foreach (var threadEvent in slot.Events)
{
var markerClass = threadEvent.EventTypeName is "Resolved" or "Payoff"
? "resolved"
: threadEvent.EventTypeName is "Contradicted"
? "contradicted"
: "";
<a class="thread-marker @markerClass"
asp-controller="Timeline"
asp-action="Index"
asp-route-projectId="@Model.Project.ProjectID"
asp-route-bookId="@Model.SelectedBookID"
asp-route-selectedSceneId="@slot.Scene.SceneID"
title="@threadEvent.ThreadTitle: @threadEvent.EventTitle">
@threadEvent.MarkerText
</a>
}
</div>
}
</div>
</div>
}
</div>
}
else
{
<div class="future-lane-placeholder">
Add visible plot lines and thread events to see narrative lanes here.
</div>
}
@if (Model.AssetLanes.Any() && Model.OrderedScenes.Any())
{
<div class="asset-lane-board" style="--scene-count:@Model.OrderedScenes.Count">
<div class="plot-lane-board-heading">
<span>Story asset lanes</span>
<small>Asset event markers by scene</small>
</div>
@foreach (var lane in Model.AssetLanes)
{
<div class="asset-lane-row">
<a class="asset-lane-label" asp-controller="StoryAssets" asp-action="Details" asp-route-id="@lane.Asset.StoryAssetID">
<strong>@lane.Asset.AssetName</strong>
<span>@lane.Asset.KindName / @lane.Asset.Importance</span>
</a>
<div class="asset-lane-slots">
@foreach (var slot in lane.SceneSlots)
{
<div class="asset-lane-slot" title="Scene @slot.Scene.SceneNumber: @slot.Scene.SceneTitle">
@foreach (var assetEvent in slot.Events)
{
<a class="asset-marker"
asp-controller="StoryAssets"
asp-action="Details"
asp-route-id="@assetEvent.StoryAssetID"
title="@assetEvent.AssetName: @assetEvent.EventTitle">
@assetEvent.MarkerText
</a>
}
</div>
}
</div>
</div>
}
</div>
}
else
{
<div class="future-lane-placeholder">
Add story assets and asset events to see asset lanes here.
</div>
}
@if (Model.CharacterLanes.Any() && Model.OrderedScenes.Any())
{
<div class="character-lane-board" style="--scene-count:@Model.OrderedScenes.Count">
<div class="plot-lane-board-heading">
<span>Character appearances</span>
<small>First twelve project characters</small>
</div>
@foreach (var lane in Model.CharacterLanes)
{
<div class="character-lane-row">
<a class="character-lane-label" asp-controller="Characters" asp-action="Details" asp-route-id="@lane.Character.CharacterID">
<strong>@lane.Character.CharacterName</strong>
<span>@(lane.Character.ShortName ?? "Character")</span>
</a>
<div class="character-lane-slots">
@foreach (var slot in lane.SceneSlots)
{
<div class="character-lane-slot" title="Scene @slot.Scene.SceneNumber: @slot.Scene.SceneTitle">
@foreach (var appearance in slot.Appearances)
{
<a class="character-marker"
asp-controller="Timeline"
asp-action="Index"
asp-route-projectId="@Model.Project.ProjectID"
asp-route-bookId="@Model.SelectedBookID"
asp-route-selectedSceneId="@slot.Scene.SceneID"
title="@appearance.CharacterName: @appearance.RoleInSceneTypeName / @appearance.PresenceTypeName">
@(!string.IsNullOrWhiteSpace(appearance.PresenceTypeName) ? appearance.PresenceTypeName.Substring(0, 1) : "C")
</a>
}
</div>
}
</div>
</div>
}
</div>
}
else
{
<div class="future-lane-placeholder">
Add characters to scenes to see character appearance lanes here.
</div>
}
</section>
<section class="pacing-panel">
<div class="pacing-heading">
<div>
<p class="eyebrow">Pacing</p>
<h2>Metric shape</h2>
</div>
<span class="muted">@Model.OrderedScenes.Count scene@(Model.OrderedScenes.Count == 1 ? "" : "s")</span>
</div>
@if (!Model.MetricGraphs.Any() || !Model.OrderedScenes.Any())
{
<p class="muted">Add scenes to begin seeing emotional and pacing shape.</p>
}
else
{
<div class="metric-shape-chart" data-metric-shape-chart>
<p class="metric-shape-intro">Compare selected metrics across the same scene timeline. Click a point to focus that scene.</p>
<div class="metric-toggle-row" data-metric-toggles></div>
<div class="metric-chart-frame">
<canvas aria-label="Comparative scene metric line graph"></canvas>
<p class="muted d-none" data-chart-fallback>Metric chart could not load. The raw metric rows are still available below.</p>
</div>
<script type="application/json">@Html.Raw(System.Text.Json.JsonSerializer.Serialize(timelineMetricShapePayload))</script>
</div>
<details class="metric-raw-details">
<summary>Show raw metric rows</summary>
<div class="metric-graph-stack mt-2">
@foreach (var graph in Model.MetricGraphs)
{
<div class="metric-graph-row">
<div class="metric-graph-label">@graph.MetricName</div>
<div class="metric-graph-points" style="--scene-count:@Math.Max(graph.Points.Count, 1)">
@foreach (var point in graph.Points)
{
<a asp-controller="Timeline"
asp-action="Index"
asp-route-projectId="@Model.Project.ProjectID"
asp-route-bookId="@Model.SelectedBookID"
asp-route-selectedSceneId="@point.SceneID"
class="metric-point"
style="--metric-height:@point.Percent%"
title="Scene @point.SceneNumber: @point.SceneTitle - @point.Value">
<span>@point.Value</span>
</a>
}
</div>
</div>
}
</div>
</details>
}
</section>
</main>
<aside class="scene-inspector-panel">
@if (Model.SelectedScene is null)
{
<div class="inspector-empty">
<p class="eyebrow">Scene inspector</p>
<h2>Select a scene</h2>
<p>Click a scene card to edit its summary, timing, purposes, outcomes and metric sliders without leaving the timeline.</p>
</div>
}
else
{
<div class="inspector-header">
<p class="eyebrow">Scene inspector</p>
<h2>@Model.SelectedScene.SceneTitle</h2>
</div>
<partial name="~/Views/Scenes/_SceneInspector.cshtml" model="Model.SelectedScene" />
}
</aside>
</div>
}
@section Scripts {
<partial name="_ValidationScriptsPartial" />
}

View File

@ -0,0 +1,196 @@
@model WarningDashboardViewModel
@{
ViewData["Title"] = "Continuity Warnings";
}
<section class="context-header">
<div class="context-title">
<span class="eyebrow">Workspace context</span>
<strong>@Model.Project.ProjectName</strong>
</div>
<nav class="context-nav" aria-label="Workspace navigation">
<a asp-controller="Projects" asp-action="Details" asp-route-id="@Model.Project.ProjectID">Project</a>
<a asp-controller="Timeline" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID" asp-route-bookId="@Model.BookID">Timeline</a>
<a asp-controller="Analytics" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID" asp-route-bookId="@Model.BookID">Story health</a>
<a asp-controller="Characters" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID">Characters</a>
<a asp-controller="StoryAssets" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID">Assets</a>
<a asp-controller="Locations" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID">Locations</a>
<a asp-controller="PlotThreads" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID">Threads</a>
</nav>
</section>
<div class="page-heading compact">
<div>
<p class="eyebrow">Continuity</p>
<h1>@Model.Project.ProjectName</h1>
</div>
<div class="button-row">
<form asp-action="ValidateProject" method="post">
<input type="hidden" name="projectId" value="@Model.Project.ProjectID" />
<input type="hidden" name="bookId" value="@Model.BookID" />
<button class="btn btn-primary" type="submit">Validate project</button>
</form>
@if (Model.BookID.HasValue)
{
<form asp-action="ValidateBook" method="post">
<input type="hidden" name="projectId" value="@Model.Project.ProjectID" />
<input type="hidden" name="bookId" value="@Model.BookID" />
<button class="btn btn-outline-primary" type="submit">Validate book</button>
</form>
}
</div>
</div>
@if (Model.LastValidationSummary is not null)
{
<section class="validation-summary">
<strong>Validation complete.</strong>
<span>@Model.LastValidationSummary.ErrorCount errors</span>
<span>@Model.LastValidationSummary.WarningCount warnings</span>
<span>@Model.LastValidationSummary.InfoCount info</span>
</section>
}
<section class="warning-filter-panel">
<form method="get" class="row g-2 align-items-end">
<input type="hidden" name="projectId" value="@Model.Project.ProjectID" />
<div class="col-md-3">
<label class="form-label">Book</label>
<select name="bookId" asp-items="Model.BookOptions" class="form-select form-select-sm">
<option value="">All books</option>
</select>
</div>
<div class="col-md-3">
<label class="form-label">Type</label>
<select name="warningTypeId" asp-items="Model.WarningTypeOptions" class="form-select form-select-sm">
<option value="">All types</option>
</select>
</div>
<div class="col-md-2">
<label class="form-label">Severity</label>
<select name="warningSeverityId" asp-items="Model.SeverityOptions" class="form-select form-select-sm">
<option value="">All severities</option>
</select>
</div>
<div class="col-md-2">
<label class="form-label">Entity</label>
<select name="entityType" asp-items="Model.EntityTypeOptions" class="form-select form-select-sm">
<option value="">All entities</option>
</select>
</div>
<div class="col-md-2">
<label class="form-check">
<input class="form-check-input" name="includeDismissed" type="checkbox" value="true" checked="@Model.IncludeDismissed" />
<span class="form-check-label">Dismissed</span>
</label>
<label class="form-check">
<input class="form-check-input" name="includeIntentional" type="checkbox" value="true" checked="@Model.IncludeIntentional" />
<span class="form-check-label">Intentional</span>
</label>
</div>
<div class="col-12">
<button class="btn btn-outline-primary btn-sm" type="submit">Filter warnings</button>
</div>
</form>
</section>
<section class="warning-dashboard-grid">
<article class="plain-card">
<h2>By severity</h2>
@foreach (var count in Model.CountsBySeverity)
{
<div class="warning-count-row">
<span class="severity-pill @count.Key.ToLowerInvariant()">@count.Key</span>
<strong>@count.Value</strong>
</div>
}
</article>
<article class="plain-card">
<h2>By type</h2>
@foreach (var count in Model.CountsByType.Take(8))
{
<div class="warning-count-row">
<span>@count.Key</span>
<strong>@count.Value</strong>
</div>
}
</article>
</section>
<section class="list-section">
<h2>Warnings</h2>
@if (!Model.Warnings.Any())
{
<p class="muted">No warnings match this view.</p>
}
else
{
<div class="warning-list">
@foreach (var warning in Model.Warnings)
{
<article class="warning-card">
<div>
<div class="warning-card-title">
<span class="severity-pill @warning.SeverityName.ToLowerInvariant()">@warning.SeverityName</span>
<strong>@warning.WarningTypeName</strong>
@if (warning.IsDismissed) { <span class="location-badge">dismissed</span> }
@if (warning.IsIntentional) { <span class="location-badge">intentional</span> }
</div>
<p>@warning.Message</p>
@if (!string.IsNullOrWhiteSpace(warning.Details))
{
<p class="muted">@warning.Details</p>
}
<p class="muted">
@(warning.BookTitle ?? "Project")
@if (warning.SceneID.HasValue)
{
<span>/ Scene @warning.SceneNumber: @warning.SceneTitle</span>
}
<span>/ @warning.EntityType</span>
<span>/ Last seen @warning.LastDetectedDate.ToString("dd MMM yyyy HH:mm")</span>
</p>
</div>
<div class="warning-actions">
@if (warning.SceneID.HasValue)
{
<a class="btn btn-outline-primary btn-sm"
asp-controller="Timeline"
asp-action="Index"
asp-route-projectId="@Model.Project.ProjectID"
asp-route-bookId="@warning.BookID"
asp-route-selectedSceneId="@warning.SceneID">Open scene</a>
}
@if (warning.IsDismissed || warning.IsIntentional)
{
<form asp-action="Restore" method="post">
<input type="hidden" name="id" value="@warning.ContinuityWarningID" />
<input type="hidden" name="projectId" value="@Model.Project.ProjectID" />
<input type="hidden" name="bookId" value="@Model.BookID" />
<input type="hidden" name="sceneId" value="@Model.SceneID" />
<button class="btn btn-outline-secondary btn-sm" type="submit">Restore</button>
</form>
}
else
{
<form asp-action="Dismiss" method="post">
<input type="hidden" name="id" value="@warning.ContinuityWarningID" />
<input type="hidden" name="projectId" value="@Model.Project.ProjectID" />
<input type="hidden" name="bookId" value="@Model.BookID" />
<input type="hidden" name="sceneId" value="@Model.SceneID" />
<button class="btn btn-outline-secondary btn-sm" type="submit">Dismiss</button>
</form>
<form asp-action="MarkIntentional" method="post">
<input type="hidden" name="id" value="@warning.ContinuityWarningID" />
<input type="hidden" name="projectId" value="@Model.Project.ProjectID" />
<input type="hidden" name="bookId" value="@Model.BookID" />
<input type="hidden" name="sceneId" value="@Model.SceneID" />
<button class="btn btn-outline-secondary btn-sm" type="submit">Intentional</button>
</form>
}
</div>
</article>
}
</div>
}
</section>

View File

@ -0,0 +1,4 @@
@using PlotLine
@using PlotLine.Models
@using PlotLine.ViewModels
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

View File

@ -0,0 +1,3 @@
@{
Layout = "_Layout";
}

View File

@ -0,0 +1,8 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}

12
PlotLine/appsettings.json Normal file
View File

@ -0,0 +1,12 @@
{
"ConnectionStrings": {
"DefaultConnection": "Server=localhost;Database=PlotLine;User Id=PlotLineApp;Password=t6eb1cvd9bNB27czde#h9r;MultipleActiveResultSets=true;Encrypt=false;"
},
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

282
PlotLine/wwwroot/js/site.js Normal file
View File

@ -0,0 +1,282 @@
(() => {
const roots = document.querySelectorAll(".scene-inspector-root");
if (!roots.length) {
return;
}
const slugify = (value) => value
.toLowerCase()
.replace(/&/g, "and")
.replace(/[^a-z0-9]+/g, "-")
.replace(/^-|-$/g, "");
roots.forEach((root) => {
const sceneId = root.querySelector("[name='SceneID']")?.value || "new";
const storagePrefix = `plotline.inspector.${sceneId}.`;
const defaultOpen = new Set(["overview", "timing", "characters"]);
const sections = [...root.querySelectorAll(".inspector-section")];
sections.forEach((section) => {
if (section.dataset.enhanced === "true") {
return;
}
const heading = section.querySelector("h3, h2");
const title = section.dataset.sectionTitle || heading?.textContent?.trim() || "Overview";
const slug = slugify(title);
const sectionId = section.dataset.sectionId || `inspector-${slug}`;
const storageKey = sectionId.replace(/^inspector-/, "");
section.id = sectionId;
section.dataset.searchText = `${title} ${section.textContent}`.toLowerCase();
section.dataset.enhanced = "true";
const body = document.createElement("div");
body.className = "inspector-accordion-body";
while (section.firstChild) {
body.appendChild(section.firstChild);
}
const button = document.createElement("button");
button.type = "button";
button.className = "inspector-accordion-toggle";
button.setAttribute("aria-expanded", "false");
const count = section.querySelectorAll(".asset-event-item, .thread-event-item, .warning-mini-card, .dependency-card, .metric-slider").length;
button.innerHTML = `<span>${title}</span><span class="accordion-count">${count || ""}</span>`;
section.append(button, body);
const stored = localStorage.getItem(`${storagePrefix}${storageKey}`);
const isOpen = stored === null ? defaultOpen.has(storageKey) : stored === "open";
section.classList.toggle("is-open", isOpen);
button.setAttribute("aria-expanded", String(isOpen));
button.addEventListener("click", () => {
const nextOpen = !section.classList.contains("is-open");
section.classList.toggle("is-open", nextOpen);
button.setAttribute("aria-expanded", String(nextOpen));
localStorage.setItem(`${storagePrefix}${storageKey}`, nextOpen ? "open" : "closed");
});
});
root.querySelectorAll(".inspector-nav-links a").forEach((link) => {
link.addEventListener("click", (event) => {
const target = root.querySelector(link.getAttribute("href"));
if (!target) {
return;
}
event.preventDefault();
target.classList.add("is-open");
target.querySelector(".inspector-accordion-toggle")?.setAttribute("aria-expanded", "true");
target.scrollIntoView({ behavior: "smooth", block: "start" });
});
});
const search = root.querySelector("[data-inspector-search]");
search?.addEventListener("input", () => {
const query = search.value.trim().toLowerCase();
sections.forEach((section) => {
const matches = !query || section.dataset.searchText.includes(query);
section.classList.toggle("is-filtered-out", !matches);
if (matches && query) {
section.classList.add("is-open");
section.querySelector(".inspector-accordion-toggle")?.setAttribute("aria-expanded", "true");
}
});
});
if ("IntersectionObserver" in window) {
const navLinks = [...root.querySelectorAll(".inspector-nav-links a")];
const observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (!entry.isIntersecting) {
return;
}
navLinks.forEach((link) => {
link.classList.toggle("active", link.getAttribute("href") === `#${entry.target.id}`);
});
});
}, { root: root.closest(".scene-inspector-panel") || null, rootMargin: "-10% 0px -70% 0px" });
sections.forEach((section) => observer.observe(section));
}
});
})();
(() => {
const chartRoots = document.querySelectorAll("[data-metric-shape-chart]");
if (!chartRoots.length) {
return;
}
const colours = [
"#2f6f63",
"#b35f3b",
"#6750a4",
"#c58b2b",
"#7d3f52",
"#33759b",
"#9c4d8f",
"#a33d3d",
"#6f8f3d",
"#d08a2f",
"#444b57"
];
const defaultMetrics = new Set(["Overall Intensity", "Tension", "Emotional Weight", "Action"]);
const chapterGuidePlugin = {
id: "plotlineChapterGuides",
afterDraw(chart) {
const scenes = chart.config.options.plugins.plotlineScenes || [];
const { ctx, chartArea, scales } = chart;
if (!scenes.length || !scales.x) {
return;
}
ctx.save();
ctx.strokeStyle = "rgba(47, 111, 99, 0.16)";
ctx.fillStyle = "rgba(82, 75, 65, 0.58)";
ctx.lineWidth = 1;
ctx.font = "700 10px system-ui, -apple-system, Segoe UI, sans-serif";
let previousChapter = null;
scenes.forEach((scene, index) => {
if (scene.chapterLabel === previousChapter) {
return;
}
const x = scales.x.getPixelForValue(index);
ctx.beginPath();
ctx.moveTo(x, chartArea.top);
ctx.lineTo(x, chartArea.bottom);
ctx.stroke();
ctx.fillText(scene.chapterLabel, Math.min(x + 4, chartArea.right - 70), chartArea.bottom + 22);
previousChapter = scene.chapterLabel;
});
ctx.restore();
}
};
if (window.Chart) {
Chart.register(chapterGuidePlugin);
}
chartRoots.forEach((root) => {
const payloadScript = root.querySelector("script[type='application/json']");
const canvas = root.querySelector("canvas");
const toggleHost = root.querySelector("[data-metric-toggles]");
const fallback = root.querySelector("[data-chart-fallback]");
if (!payloadScript || !canvas) {
return;
}
let payload;
try {
payload = JSON.parse(payloadScript.textContent || "{}");
} catch {
fallback?.classList.remove("d-none");
return;
}
if (!window.Chart || !payload.scenes?.length || !payload.metrics?.length) {
fallback?.classList.remove("d-none");
return;
}
const labels = payload.scenes.map((scene) => scene.axisLabel);
const sceneIndexById = new Map(payload.scenes.map((scene, index) => [scene.sceneId, index]));
const datasets = payload.metrics.map((metric, metricIndex) => {
const pointMap = new Map(metric.points.map((point) => [point.sceneId, point.value]));
const colour = colours[metricIndex % colours.length];
return {
label: metric.metricName,
data: payload.scenes.map((scene) => pointMap.get(scene.sceneId) ?? null),
borderColor: colour,
backgroundColor: colour,
pointBackgroundColor: colour,
pointBorderColor: "#fff",
pointBorderWidth: 1.5,
pointRadius: 3,
pointHoverRadius: 5,
borderWidth: 2,
tension: 0.35,
spanGaps: true,
hidden: !defaultMetrics.has(metric.metricName)
};
});
toggleHost.innerHTML = "";
datasets.forEach((dataset, index) => {
const id = `metric-toggle-${Math.random().toString(36).slice(2)}-${index}`;
const label = document.createElement("label");
label.className = "metric-toggle-pill";
label.style.setProperty("--metric-colour", dataset.borderColor);
label.innerHTML = `<input type="checkbox" id="${id}" ${dataset.hidden ? "" : "checked"}><span>${dataset.label}</span>`;
toggleHost.appendChild(label);
label.querySelector("input").addEventListener("change", (event) => {
chart.setDatasetVisibility(index, event.target.checked);
chart.update();
});
});
const chart = new Chart(canvas, {
type: "line",
data: { labels, datasets },
options: {
responsive: true,
maintainAspectRatio: false,
interaction: { mode: "nearest", intersect: false },
scales: {
y: {
min: 1,
max: 10,
ticks: { stepSize: 1 },
grid: { color: "rgba(82, 75, 65, 0.10)" }
},
x: {
ticks: {
maxRotation: 0,
autoSkip: true,
maxTicksLimit: 16
},
grid: { display: false }
}
},
plugins: {
legend: { display: false },
plotlineScenes: payload.scenes,
tooltip: {
callbacks: {
title(items) {
const scene = payload.scenes[items[0].dataIndex];
return `${scene.chapterLabel}, Scene ${scene.sceneNumber}: ${scene.sceneTitle}`;
},
label(item) {
return `${item.dataset.label}: ${item.formattedValue}`;
},
afterBody(items) {
const scene = payload.scenes[items[0].dataIndex];
return scene.bookTitle ? scene.bookTitle : "";
}
}
}
}
}
});
canvas.addEventListener("click", (event) => {
const points = chart.getElementsAtEventForMode(event, "nearest", { intersect: true }, true);
if (!points.length) {
return;
}
const scene = payload.scenes[points[0].index];
if (scene?.timelineUrl) {
window.location.href = scene.timelineUrl;
}
});
});
})();

View File

@ -0,0 +1,22 @@
The MIT License (MIT)
Copyright (c) 2011-2021 Twitter, Inc.
Copyright (c) 2011-2021 The Bootstrap Authors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,597 @@
/*!
* Bootstrap Reboot v5.3.3 (https://getbootstrap.com/)
* Copyright 2011-2024 The Bootstrap Authors
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
*/
:root,
[data-bs-theme=light] {
--bs-blue: #0d6efd;
--bs-indigo: #6610f2;
--bs-purple: #6f42c1;
--bs-pink: #d63384;
--bs-red: #dc3545;
--bs-orange: #fd7e14;
--bs-yellow: #ffc107;
--bs-green: #198754;
--bs-teal: #20c997;
--bs-cyan: #0dcaf0;
--bs-black: #000;
--bs-white: #fff;
--bs-gray: #6c757d;
--bs-gray-dark: #343a40;
--bs-gray-100: #f8f9fa;
--bs-gray-200: #e9ecef;
--bs-gray-300: #dee2e6;
--bs-gray-400: #ced4da;
--bs-gray-500: #adb5bd;
--bs-gray-600: #6c757d;
--bs-gray-700: #495057;
--bs-gray-800: #343a40;
--bs-gray-900: #212529;
--bs-primary: #0d6efd;
--bs-secondary: #6c757d;
--bs-success: #198754;
--bs-info: #0dcaf0;
--bs-warning: #ffc107;
--bs-danger: #dc3545;
--bs-light: #f8f9fa;
--bs-dark: #212529;
--bs-primary-rgb: 13, 110, 253;
--bs-secondary-rgb: 108, 117, 125;
--bs-success-rgb: 25, 135, 84;
--bs-info-rgb: 13, 202, 240;
--bs-warning-rgb: 255, 193, 7;
--bs-danger-rgb: 220, 53, 69;
--bs-light-rgb: 248, 249, 250;
--bs-dark-rgb: 33, 37, 41;
--bs-primary-text-emphasis: #052c65;
--bs-secondary-text-emphasis: #2b2f32;
--bs-success-text-emphasis: #0a3622;
--bs-info-text-emphasis: #055160;
--bs-warning-text-emphasis: #664d03;
--bs-danger-text-emphasis: #58151c;
--bs-light-text-emphasis: #495057;
--bs-dark-text-emphasis: #495057;
--bs-primary-bg-subtle: #cfe2ff;
--bs-secondary-bg-subtle: #e2e3e5;
--bs-success-bg-subtle: #d1e7dd;
--bs-info-bg-subtle: #cff4fc;
--bs-warning-bg-subtle: #fff3cd;
--bs-danger-bg-subtle: #f8d7da;
--bs-light-bg-subtle: #fcfcfd;
--bs-dark-bg-subtle: #ced4da;
--bs-primary-border-subtle: #9ec5fe;
--bs-secondary-border-subtle: #c4c8cb;
--bs-success-border-subtle: #a3cfbb;
--bs-info-border-subtle: #9eeaf9;
--bs-warning-border-subtle: #ffe69c;
--bs-danger-border-subtle: #f1aeb5;
--bs-light-border-subtle: #e9ecef;
--bs-dark-border-subtle: #adb5bd;
--bs-white-rgb: 255, 255, 255;
--bs-black-rgb: 0, 0, 0;
--bs-font-sans-serif: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", "Liberation Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
--bs-font-monospace: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
--bs-gradient: linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0));
--bs-body-font-family: var(--bs-font-sans-serif);
--bs-body-font-size: 1rem;
--bs-body-font-weight: 400;
--bs-body-line-height: 1.5;
--bs-body-color: #212529;
--bs-body-color-rgb: 33, 37, 41;
--bs-body-bg: #fff;
--bs-body-bg-rgb: 255, 255, 255;
--bs-emphasis-color: #000;
--bs-emphasis-color-rgb: 0, 0, 0;
--bs-secondary-color: rgba(33, 37, 41, 0.75);
--bs-secondary-color-rgb: 33, 37, 41;
--bs-secondary-bg: #e9ecef;
--bs-secondary-bg-rgb: 233, 236, 239;
--bs-tertiary-color: rgba(33, 37, 41, 0.5);
--bs-tertiary-color-rgb: 33, 37, 41;
--bs-tertiary-bg: #f8f9fa;
--bs-tertiary-bg-rgb: 248, 249, 250;
--bs-heading-color: inherit;
--bs-link-color: #0d6efd;
--bs-link-color-rgb: 13, 110, 253;
--bs-link-decoration: underline;
--bs-link-hover-color: #0a58ca;
--bs-link-hover-color-rgb: 10, 88, 202;
--bs-code-color: #d63384;
--bs-highlight-color: #212529;
--bs-highlight-bg: #fff3cd;
--bs-border-width: 1px;
--bs-border-style: solid;
--bs-border-color: #dee2e6;
--bs-border-color-translucent: rgba(0, 0, 0, 0.175);
--bs-border-radius: 0.375rem;
--bs-border-radius-sm: 0.25rem;
--bs-border-radius-lg: 0.5rem;
--bs-border-radius-xl: 1rem;
--bs-border-radius-xxl: 2rem;
--bs-border-radius-2xl: var(--bs-border-radius-xxl);
--bs-border-radius-pill: 50rem;
--bs-box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
--bs-box-shadow-sm: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
--bs-box-shadow-lg: 0 1rem 3rem rgba(0, 0, 0, 0.175);
--bs-box-shadow-inset: inset 0 1px 2px rgba(0, 0, 0, 0.075);
--bs-focus-ring-width: 0.25rem;
--bs-focus-ring-opacity: 0.25;
--bs-focus-ring-color: rgba(13, 110, 253, 0.25);
--bs-form-valid-color: #198754;
--bs-form-valid-border-color: #198754;
--bs-form-invalid-color: #dc3545;
--bs-form-invalid-border-color: #dc3545;
}
[data-bs-theme=dark] {
color-scheme: dark;
--bs-body-color: #dee2e6;
--bs-body-color-rgb: 222, 226, 230;
--bs-body-bg: #212529;
--bs-body-bg-rgb: 33, 37, 41;
--bs-emphasis-color: #fff;
--bs-emphasis-color-rgb: 255, 255, 255;
--bs-secondary-color: rgba(222, 226, 230, 0.75);
--bs-secondary-color-rgb: 222, 226, 230;
--bs-secondary-bg: #343a40;
--bs-secondary-bg-rgb: 52, 58, 64;
--bs-tertiary-color: rgba(222, 226, 230, 0.5);
--bs-tertiary-color-rgb: 222, 226, 230;
--bs-tertiary-bg: #2b3035;
--bs-tertiary-bg-rgb: 43, 48, 53;
--bs-primary-text-emphasis: #6ea8fe;
--bs-secondary-text-emphasis: #a7acb1;
--bs-success-text-emphasis: #75b798;
--bs-info-text-emphasis: #6edff6;
--bs-warning-text-emphasis: #ffda6a;
--bs-danger-text-emphasis: #ea868f;
--bs-light-text-emphasis: #f8f9fa;
--bs-dark-text-emphasis: #dee2e6;
--bs-primary-bg-subtle: #031633;
--bs-secondary-bg-subtle: #161719;
--bs-success-bg-subtle: #051b11;
--bs-info-bg-subtle: #032830;
--bs-warning-bg-subtle: #332701;
--bs-danger-bg-subtle: #2c0b0e;
--bs-light-bg-subtle: #343a40;
--bs-dark-bg-subtle: #1a1d20;
--bs-primary-border-subtle: #084298;
--bs-secondary-border-subtle: #41464b;
--bs-success-border-subtle: #0f5132;
--bs-info-border-subtle: #087990;
--bs-warning-border-subtle: #997404;
--bs-danger-border-subtle: #842029;
--bs-light-border-subtle: #495057;
--bs-dark-border-subtle: #343a40;
--bs-heading-color: inherit;
--bs-link-color: #6ea8fe;
--bs-link-hover-color: #8bb9fe;
--bs-link-color-rgb: 110, 168, 254;
--bs-link-hover-color-rgb: 139, 185, 254;
--bs-code-color: #e685b5;
--bs-highlight-color: #dee2e6;
--bs-highlight-bg: #664d03;
--bs-border-color: #495057;
--bs-border-color-translucent: rgba(255, 255, 255, 0.15);
--bs-form-valid-color: #75b798;
--bs-form-valid-border-color: #75b798;
--bs-form-invalid-color: #ea868f;
--bs-form-invalid-border-color: #ea868f;
}
*,
*::before,
*::after {
box-sizing: border-box;
}
@media (prefers-reduced-motion: no-preference) {
:root {
scroll-behavior: smooth;
}
}
body {
margin: 0;
font-family: var(--bs-body-font-family);
font-size: var(--bs-body-font-size);
font-weight: var(--bs-body-font-weight);
line-height: var(--bs-body-line-height);
color: var(--bs-body-color);
text-align: var(--bs-body-text-align);
background-color: var(--bs-body-bg);
-webkit-text-size-adjust: 100%;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
}
hr {
margin: 1rem 0;
color: inherit;
border: 0;
border-top: var(--bs-border-width) solid;
opacity: 0.25;
}
h6, h5, h4, h3, h2, h1 {
margin-top: 0;
margin-bottom: 0.5rem;
font-weight: 500;
line-height: 1.2;
color: var(--bs-heading-color);
}
h1 {
font-size: calc(1.375rem + 1.5vw);
}
@media (min-width: 1200px) {
h1 {
font-size: 2.5rem;
}
}
h2 {
font-size: calc(1.325rem + 0.9vw);
}
@media (min-width: 1200px) {
h2 {
font-size: 2rem;
}
}
h3 {
font-size: calc(1.3rem + 0.6vw);
}
@media (min-width: 1200px) {
h3 {
font-size: 1.75rem;
}
}
h4 {
font-size: calc(1.275rem + 0.3vw);
}
@media (min-width: 1200px) {
h4 {
font-size: 1.5rem;
}
}
h5 {
font-size: 1.25rem;
}
h6 {
font-size: 1rem;
}
p {
margin-top: 0;
margin-bottom: 1rem;
}
abbr[title] {
-webkit-text-decoration: underline dotted;
text-decoration: underline dotted;
cursor: help;
-webkit-text-decoration-skip-ink: none;
text-decoration-skip-ink: none;
}
address {
margin-bottom: 1rem;
font-style: normal;
line-height: inherit;
}
ol,
ul {
padding-left: 2rem;
}
ol,
ul,
dl {
margin-top: 0;
margin-bottom: 1rem;
}
ol ol,
ul ul,
ol ul,
ul ol {
margin-bottom: 0;
}
dt {
font-weight: 700;
}
dd {
margin-bottom: 0.5rem;
margin-left: 0;
}
blockquote {
margin: 0 0 1rem;
}
b,
strong {
font-weight: bolder;
}
small {
font-size: 0.875em;
}
mark {
padding: 0.1875em;
color: var(--bs-highlight-color);
background-color: var(--bs-highlight-bg);
}
sub,
sup {
position: relative;
font-size: 0.75em;
line-height: 0;
vertical-align: baseline;
}
sub {
bottom: -0.25em;
}
sup {
top: -0.5em;
}
a {
color: rgba(var(--bs-link-color-rgb), var(--bs-link-opacity, 1));
text-decoration: underline;
}
a:hover {
--bs-link-color-rgb: var(--bs-link-hover-color-rgb);
}
a:not([href]):not([class]), a:not([href]):not([class]):hover {
color: inherit;
text-decoration: none;
}
pre,
code,
kbd,
samp {
font-family: var(--bs-font-monospace);
font-size: 1em;
}
pre {
display: block;
margin-top: 0;
margin-bottom: 1rem;
overflow: auto;
font-size: 0.875em;
}
pre code {
font-size: inherit;
color: inherit;
word-break: normal;
}
code {
font-size: 0.875em;
color: var(--bs-code-color);
word-wrap: break-word;
}
a > code {
color: inherit;
}
kbd {
padding: 0.1875rem 0.375rem;
font-size: 0.875em;
color: var(--bs-body-bg);
background-color: var(--bs-body-color);
border-radius: 0.25rem;
}
kbd kbd {
padding: 0;
font-size: 1em;
}
figure {
margin: 0 0 1rem;
}
img,
svg {
vertical-align: middle;
}
table {
caption-side: bottom;
border-collapse: collapse;
}
caption {
padding-top: 0.5rem;
padding-bottom: 0.5rem;
color: var(--bs-secondary-color);
text-align: left;
}
th {
text-align: inherit;
text-align: -webkit-match-parent;
}
thead,
tbody,
tfoot,
tr,
td,
th {
border-color: inherit;
border-style: solid;
border-width: 0;
}
label {
display: inline-block;
}
button {
border-radius: 0;
}
button:focus:not(:focus-visible) {
outline: 0;
}
input,
button,
select,
optgroup,
textarea {
margin: 0;
font-family: inherit;
font-size: inherit;
line-height: inherit;
}
button,
select {
text-transform: none;
}
[role=button] {
cursor: pointer;
}
select {
word-wrap: normal;
}
select:disabled {
opacity: 1;
}
[list]:not([type=date]):not([type=datetime-local]):not([type=month]):not([type=week]):not([type=time])::-webkit-calendar-picker-indicator {
display: none !important;
}
button,
[type=button],
[type=reset],
[type=submit] {
-webkit-appearance: button;
}
button:not(:disabled),
[type=button]:not(:disabled),
[type=reset]:not(:disabled),
[type=submit]:not(:disabled) {
cursor: pointer;
}
::-moz-focus-inner {
padding: 0;
border-style: none;
}
textarea {
resize: vertical;
}
fieldset {
min-width: 0;
padding: 0;
margin: 0;
border: 0;
}
legend {
float: left;
width: 100%;
padding: 0;
margin-bottom: 0.5rem;
font-size: calc(1.275rem + 0.3vw);
line-height: inherit;
}
@media (min-width: 1200px) {
legend {
font-size: 1.5rem;
}
}
legend + * {
clear: left;
}
::-webkit-datetime-edit-fields-wrapper,
::-webkit-datetime-edit-text,
::-webkit-datetime-edit-minute,
::-webkit-datetime-edit-hour-field,
::-webkit-datetime-edit-day-field,
::-webkit-datetime-edit-month-field,
::-webkit-datetime-edit-year-field {
padding: 0;
}
::-webkit-inner-spin-button {
height: auto;
}
[type=search] {
-webkit-appearance: textfield;
outline-offset: -2px;
}
/* rtl:raw:
[type="tel"],
[type="url"],
[type="email"],
[type="number"] {
direction: ltr;
}
*/
::-webkit-search-decoration {
-webkit-appearance: none;
}
::-webkit-color-swatch-wrapper {
padding: 0;
}
::-webkit-file-upload-button {
font: inherit;
-webkit-appearance: button;
}
::file-selector-button {
font: inherit;
-webkit-appearance: button;
}
output {
display: inline-block;
}
iframe {
border: 0;
}
summary {
display: list-item;
cursor: pointer;
}
progress {
vertical-align: baseline;
}
[hidden] {
display: none !important;
}
/*# sourceMappingURL=bootstrap-reboot.css.map */

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,594 @@
/*!
* Bootstrap Reboot v5.3.3 (https://getbootstrap.com/)
* Copyright 2011-2024 The Bootstrap Authors
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
*/
:root,
[data-bs-theme=light] {
--bs-blue: #0d6efd;
--bs-indigo: #6610f2;
--bs-purple: #6f42c1;
--bs-pink: #d63384;
--bs-red: #dc3545;
--bs-orange: #fd7e14;
--bs-yellow: #ffc107;
--bs-green: #198754;
--bs-teal: #20c997;
--bs-cyan: #0dcaf0;
--bs-black: #000;
--bs-white: #fff;
--bs-gray: #6c757d;
--bs-gray-dark: #343a40;
--bs-gray-100: #f8f9fa;
--bs-gray-200: #e9ecef;
--bs-gray-300: #dee2e6;
--bs-gray-400: #ced4da;
--bs-gray-500: #adb5bd;
--bs-gray-600: #6c757d;
--bs-gray-700: #495057;
--bs-gray-800: #343a40;
--bs-gray-900: #212529;
--bs-primary: #0d6efd;
--bs-secondary: #6c757d;
--bs-success: #198754;
--bs-info: #0dcaf0;
--bs-warning: #ffc107;
--bs-danger: #dc3545;
--bs-light: #f8f9fa;
--bs-dark: #212529;
--bs-primary-rgb: 13, 110, 253;
--bs-secondary-rgb: 108, 117, 125;
--bs-success-rgb: 25, 135, 84;
--bs-info-rgb: 13, 202, 240;
--bs-warning-rgb: 255, 193, 7;
--bs-danger-rgb: 220, 53, 69;
--bs-light-rgb: 248, 249, 250;
--bs-dark-rgb: 33, 37, 41;
--bs-primary-text-emphasis: #052c65;
--bs-secondary-text-emphasis: #2b2f32;
--bs-success-text-emphasis: #0a3622;
--bs-info-text-emphasis: #055160;
--bs-warning-text-emphasis: #664d03;
--bs-danger-text-emphasis: #58151c;
--bs-light-text-emphasis: #495057;
--bs-dark-text-emphasis: #495057;
--bs-primary-bg-subtle: #cfe2ff;
--bs-secondary-bg-subtle: #e2e3e5;
--bs-success-bg-subtle: #d1e7dd;
--bs-info-bg-subtle: #cff4fc;
--bs-warning-bg-subtle: #fff3cd;
--bs-danger-bg-subtle: #f8d7da;
--bs-light-bg-subtle: #fcfcfd;
--bs-dark-bg-subtle: #ced4da;
--bs-primary-border-subtle: #9ec5fe;
--bs-secondary-border-subtle: #c4c8cb;
--bs-success-border-subtle: #a3cfbb;
--bs-info-border-subtle: #9eeaf9;
--bs-warning-border-subtle: #ffe69c;
--bs-danger-border-subtle: #f1aeb5;
--bs-light-border-subtle: #e9ecef;
--bs-dark-border-subtle: #adb5bd;
--bs-white-rgb: 255, 255, 255;
--bs-black-rgb: 0, 0, 0;
--bs-font-sans-serif: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", "Liberation Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
--bs-font-monospace: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
--bs-gradient: linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0));
--bs-body-font-family: var(--bs-font-sans-serif);
--bs-body-font-size: 1rem;
--bs-body-font-weight: 400;
--bs-body-line-height: 1.5;
--bs-body-color: #212529;
--bs-body-color-rgb: 33, 37, 41;
--bs-body-bg: #fff;
--bs-body-bg-rgb: 255, 255, 255;
--bs-emphasis-color: #000;
--bs-emphasis-color-rgb: 0, 0, 0;
--bs-secondary-color: rgba(33, 37, 41, 0.75);
--bs-secondary-color-rgb: 33, 37, 41;
--bs-secondary-bg: #e9ecef;
--bs-secondary-bg-rgb: 233, 236, 239;
--bs-tertiary-color: rgba(33, 37, 41, 0.5);
--bs-tertiary-color-rgb: 33, 37, 41;
--bs-tertiary-bg: #f8f9fa;
--bs-tertiary-bg-rgb: 248, 249, 250;
--bs-heading-color: inherit;
--bs-link-color: #0d6efd;
--bs-link-color-rgb: 13, 110, 253;
--bs-link-decoration: underline;
--bs-link-hover-color: #0a58ca;
--bs-link-hover-color-rgb: 10, 88, 202;
--bs-code-color: #d63384;
--bs-highlight-color: #212529;
--bs-highlight-bg: #fff3cd;
--bs-border-width: 1px;
--bs-border-style: solid;
--bs-border-color: #dee2e6;
--bs-border-color-translucent: rgba(0, 0, 0, 0.175);
--bs-border-radius: 0.375rem;
--bs-border-radius-sm: 0.25rem;
--bs-border-radius-lg: 0.5rem;
--bs-border-radius-xl: 1rem;
--bs-border-radius-xxl: 2rem;
--bs-border-radius-2xl: var(--bs-border-radius-xxl);
--bs-border-radius-pill: 50rem;
--bs-box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
--bs-box-shadow-sm: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
--bs-box-shadow-lg: 0 1rem 3rem rgba(0, 0, 0, 0.175);
--bs-box-shadow-inset: inset 0 1px 2px rgba(0, 0, 0, 0.075);
--bs-focus-ring-width: 0.25rem;
--bs-focus-ring-opacity: 0.25;
--bs-focus-ring-color: rgba(13, 110, 253, 0.25);
--bs-form-valid-color: #198754;
--bs-form-valid-border-color: #198754;
--bs-form-invalid-color: #dc3545;
--bs-form-invalid-border-color: #dc3545;
}
[data-bs-theme=dark] {
color-scheme: dark;
--bs-body-color: #dee2e6;
--bs-body-color-rgb: 222, 226, 230;
--bs-body-bg: #212529;
--bs-body-bg-rgb: 33, 37, 41;
--bs-emphasis-color: #fff;
--bs-emphasis-color-rgb: 255, 255, 255;
--bs-secondary-color: rgba(222, 226, 230, 0.75);
--bs-secondary-color-rgb: 222, 226, 230;
--bs-secondary-bg: #343a40;
--bs-secondary-bg-rgb: 52, 58, 64;
--bs-tertiary-color: rgba(222, 226, 230, 0.5);
--bs-tertiary-color-rgb: 222, 226, 230;
--bs-tertiary-bg: #2b3035;
--bs-tertiary-bg-rgb: 43, 48, 53;
--bs-primary-text-emphasis: #6ea8fe;
--bs-secondary-text-emphasis: #a7acb1;
--bs-success-text-emphasis: #75b798;
--bs-info-text-emphasis: #6edff6;
--bs-warning-text-emphasis: #ffda6a;
--bs-danger-text-emphasis: #ea868f;
--bs-light-text-emphasis: #f8f9fa;
--bs-dark-text-emphasis: #dee2e6;
--bs-primary-bg-subtle: #031633;
--bs-secondary-bg-subtle: #161719;
--bs-success-bg-subtle: #051b11;
--bs-info-bg-subtle: #032830;
--bs-warning-bg-subtle: #332701;
--bs-danger-bg-subtle: #2c0b0e;
--bs-light-bg-subtle: #343a40;
--bs-dark-bg-subtle: #1a1d20;
--bs-primary-border-subtle: #084298;
--bs-secondary-border-subtle: #41464b;
--bs-success-border-subtle: #0f5132;
--bs-info-border-subtle: #087990;
--bs-warning-border-subtle: #997404;
--bs-danger-border-subtle: #842029;
--bs-light-border-subtle: #495057;
--bs-dark-border-subtle: #343a40;
--bs-heading-color: inherit;
--bs-link-color: #6ea8fe;
--bs-link-hover-color: #8bb9fe;
--bs-link-color-rgb: 110, 168, 254;
--bs-link-hover-color-rgb: 139, 185, 254;
--bs-code-color: #e685b5;
--bs-highlight-color: #dee2e6;
--bs-highlight-bg: #664d03;
--bs-border-color: #495057;
--bs-border-color-translucent: rgba(255, 255, 255, 0.15);
--bs-form-valid-color: #75b798;
--bs-form-valid-border-color: #75b798;
--bs-form-invalid-color: #ea868f;
--bs-form-invalid-border-color: #ea868f;
}
*,
*::before,
*::after {
box-sizing: border-box;
}
@media (prefers-reduced-motion: no-preference) {
:root {
scroll-behavior: smooth;
}
}
body {
margin: 0;
font-family: var(--bs-body-font-family);
font-size: var(--bs-body-font-size);
font-weight: var(--bs-body-font-weight);
line-height: var(--bs-body-line-height);
color: var(--bs-body-color);
text-align: var(--bs-body-text-align);
background-color: var(--bs-body-bg);
-webkit-text-size-adjust: 100%;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
}
hr {
margin: 1rem 0;
color: inherit;
border: 0;
border-top: var(--bs-border-width) solid;
opacity: 0.25;
}
h6, h5, h4, h3, h2, h1 {
margin-top: 0;
margin-bottom: 0.5rem;
font-weight: 500;
line-height: 1.2;
color: var(--bs-heading-color);
}
h1 {
font-size: calc(1.375rem + 1.5vw);
}
@media (min-width: 1200px) {
h1 {
font-size: 2.5rem;
}
}
h2 {
font-size: calc(1.325rem + 0.9vw);
}
@media (min-width: 1200px) {
h2 {
font-size: 2rem;
}
}
h3 {
font-size: calc(1.3rem + 0.6vw);
}
@media (min-width: 1200px) {
h3 {
font-size: 1.75rem;
}
}
h4 {
font-size: calc(1.275rem + 0.3vw);
}
@media (min-width: 1200px) {
h4 {
font-size: 1.5rem;
}
}
h5 {
font-size: 1.25rem;
}
h6 {
font-size: 1rem;
}
p {
margin-top: 0;
margin-bottom: 1rem;
}
abbr[title] {
-webkit-text-decoration: underline dotted;
text-decoration: underline dotted;
cursor: help;
-webkit-text-decoration-skip-ink: none;
text-decoration-skip-ink: none;
}
address {
margin-bottom: 1rem;
font-style: normal;
line-height: inherit;
}
ol,
ul {
padding-right: 2rem;
}
ol,
ul,
dl {
margin-top: 0;
margin-bottom: 1rem;
}
ol ol,
ul ul,
ol ul,
ul ol {
margin-bottom: 0;
}
dt {
font-weight: 700;
}
dd {
margin-bottom: 0.5rem;
margin-right: 0;
}
blockquote {
margin: 0 0 1rem;
}
b,
strong {
font-weight: bolder;
}
small {
font-size: 0.875em;
}
mark {
padding: 0.1875em;
color: var(--bs-highlight-color);
background-color: var(--bs-highlight-bg);
}
sub,
sup {
position: relative;
font-size: 0.75em;
line-height: 0;
vertical-align: baseline;
}
sub {
bottom: -0.25em;
}
sup {
top: -0.5em;
}
a {
color: rgba(var(--bs-link-color-rgb), var(--bs-link-opacity, 1));
text-decoration: underline;
}
a:hover {
--bs-link-color-rgb: var(--bs-link-hover-color-rgb);
}
a:not([href]):not([class]), a:not([href]):not([class]):hover {
color: inherit;
text-decoration: none;
}
pre,
code,
kbd,
samp {
font-family: var(--bs-font-monospace);
font-size: 1em;
}
pre {
display: block;
margin-top: 0;
margin-bottom: 1rem;
overflow: auto;
font-size: 0.875em;
}
pre code {
font-size: inherit;
color: inherit;
word-break: normal;
}
code {
font-size: 0.875em;
color: var(--bs-code-color);
word-wrap: break-word;
}
a > code {
color: inherit;
}
kbd {
padding: 0.1875rem 0.375rem;
font-size: 0.875em;
color: var(--bs-body-bg);
background-color: var(--bs-body-color);
border-radius: 0.25rem;
}
kbd kbd {
padding: 0;
font-size: 1em;
}
figure {
margin: 0 0 1rem;
}
img,
svg {
vertical-align: middle;
}
table {
caption-side: bottom;
border-collapse: collapse;
}
caption {
padding-top: 0.5rem;
padding-bottom: 0.5rem;
color: var(--bs-secondary-color);
text-align: right;
}
th {
text-align: inherit;
text-align: -webkit-match-parent;
}
thead,
tbody,
tfoot,
tr,
td,
th {
border-color: inherit;
border-style: solid;
border-width: 0;
}
label {
display: inline-block;
}
button {
border-radius: 0;
}
button:focus:not(:focus-visible) {
outline: 0;
}
input,
button,
select,
optgroup,
textarea {
margin: 0;
font-family: inherit;
font-size: inherit;
line-height: inherit;
}
button,
select {
text-transform: none;
}
[role=button] {
cursor: pointer;
}
select {
word-wrap: normal;
}
select:disabled {
opacity: 1;
}
[list]:not([type=date]):not([type=datetime-local]):not([type=month]):not([type=week]):not([type=time])::-webkit-calendar-picker-indicator {
display: none !important;
}
button,
[type=button],
[type=reset],
[type=submit] {
-webkit-appearance: button;
}
button:not(:disabled),
[type=button]:not(:disabled),
[type=reset]:not(:disabled),
[type=submit]:not(:disabled) {
cursor: pointer;
}
::-moz-focus-inner {
padding: 0;
border-style: none;
}
textarea {
resize: vertical;
}
fieldset {
min-width: 0;
padding: 0;
margin: 0;
border: 0;
}
legend {
float: right;
width: 100%;
padding: 0;
margin-bottom: 0.5rem;
font-size: calc(1.275rem + 0.3vw);
line-height: inherit;
}
@media (min-width: 1200px) {
legend {
font-size: 1.5rem;
}
}
legend + * {
clear: right;
}
::-webkit-datetime-edit-fields-wrapper,
::-webkit-datetime-edit-text,
::-webkit-datetime-edit-minute,
::-webkit-datetime-edit-hour-field,
::-webkit-datetime-edit-day-field,
::-webkit-datetime-edit-month-field,
::-webkit-datetime-edit-year-field {
padding: 0;
}
::-webkit-inner-spin-button {
height: auto;
}
[type=search] {
-webkit-appearance: textfield;
outline-offset: -2px;
}
[type="tel"],
[type="url"],
[type="email"],
[type="number"] {
direction: ltr;
}
::-webkit-search-decoration {
-webkit-appearance: none;
}
::-webkit-color-swatch-wrapper {
padding: 0;
}
::-webkit-file-upload-button {
font: inherit;
-webkit-appearance: button;
}
::file-selector-button {
font: inherit;
-webkit-appearance: button;
}
output {
display: inline-block;
}
iframe {
border: 0;
}
summary {
display: list-item;
cursor: pointer;
}
progress {
vertical-align: baseline;
}
[hidden] {
display: none !important;
}
/*# sourceMappingURL=bootstrap-reboot.rtl.css.map */

Some files were not shown because too many files have changed in this diff Show More