feat(templates): add service language template management

Add normalized service and template catalogs, scoped administration, migration, validation, and regression coverage.

Ref: IT-1601
This commit is contained in:
2026-10-17 22:02:43 +03:00
parent 36810dc5f1
commit 81654c0d33
73 changed files with 4061 additions and 820 deletions
@@ -0,0 +1,13 @@
@using HrynCo.NotificationService.Web.Controllers.Admin.ViewModels
@model ServiceLanguageDeleteViewModel
@{ ViewData["Title"] = "Delete service language and versions"; }
<h2>Delete language @Model.Code from @Model.ServiceName?</h2>
<div asp-validation-summary="All" class="text-danger" role="alert"></div>
<div class="alert alert-danger">This permanently deletes <strong>@Model.VersionCount template version(s)</strong> using this language in this service and removes the language code. Template definitions, their keys and shared variables are retained.</div>
<form method="post" action="/admin/services/@Model.ServiceId/languages/@Model.LanguageId/delete">
@Html.AntiForgeryToken()
<input asp-for="VersionCount" type="hidden" /><input asp-for="Revision" type="hidden" />
<div class="form-check mb-3"><input asp-for="ConfirmDelete" class="form-check-input" /><label asp-for="ConfirmDelete" class="form-check-label">I confirm deletion of @Model.VersionCount version(s) and language @Model.Code.</label></div>
<button type="submit" class="btn btn-danger">Delete @Model.VersionCount version(s) and language</button>
<a class="btn btn-secondary" href="/admin/services/@Model.ServiceId/edit">Cancel</a>
</form>
@@ -0,0 +1,48 @@
@using HrynCo.NotificationService.Web.Controllers.Admin.ViewModels
@model NotificationServiceEditViewModel
@{
Layout = "~/Views/Shared/_EditorLayout.cshtml";
ViewData["Title"] = Model.IsNew ? "Create Service" : "Edit Service";
ViewData["EditorTitle"] = ViewData["Title"];
}
@section Breadcrumbs {
<nav aria-label="Breadcrumb"><ol class="breadcrumb"><li class="breadcrumb-item"><a href="/admin/services">Services</a></li>@if (!Model.IsNew) { <li class="breadcrumb-item"><a href="/admin/services/@Model.Id">@Model.Name</a></li> }<li class="breadcrumb-item active" aria-current="page">@ViewData["Title"]</li></ol></nav>
}
<form asp-action="Save" id="serviceForm" method="post">
@Html.AntiForgeryToken()
<input asp-for="Id" type="hidden" /><input asp-for="Revision" type="hidden" />
<div asp-validation-summary="All" class="text-danger mb-3" role="alert"></div>
<label asp-for="Name" class="form-label fw-semibold">Service name</label>
<input asp-for="Name" class="form-control mb-3" maxlength="100" />
<h3 class="h5">Language codes</h3>
<p class="text-muted">Codes are case-sensitive and belong to this service. Existing template versions use these codes when sending. Renaming a service or code changes sender lookup; align publishers and email channel service names.</p>
<div class="table-responsive">
<table class="table service-language-table"><thead><tr><th>Code</th><th>Versions</th><th class="service-language-actions">Actions</th></tr></thead><tbody id="serviceLanguages">
@for (var i = 0; i < Model.Languages.Count; i++)
{
var countUnavailable = ViewData["LanguageCountsUnavailable"] is true && Model.Languages[i].Id != Guid.Empty;
<tr>
<td><input asp-for="Languages[i].Id" type="hidden" /><label for="service-code-@i" class="visually-hidden">Language code</label><input asp-for="Languages[i].Code" id="service-code-@i" class="form-control" maxlength="10" /></td>
<td>@(countUnavailable ? "Unavailable" : Model.Languages[i].VersionCount.ToString())</td>
<td class="service-language-actions">
<div class="service-language-action-buttons">
<button type="button" class="btn btn-outline-secondary remove-service-language" data-count="@Model.Languages[i].VersionCount" disabled="@countUnavailable">Remove</button>
@if (!Model.IsNew && Model.Languages[i].Id != Guid.Empty)
{
<a class="btn btn-outline-danger" href="/admin/services/@Model.Id/languages/@Model.Languages[i].Id/delete">Delete language and versions…</a>
}
</div>
</td>
</tr>
}
</tbody></table>
</div>
<button type="button" id="addServiceLanguage" class="btn btn-outline-primary">Add language code</button>
<div id="serviceLanguageAlert" role="alert" class="text-danger mt-3"></div>
</form>
<template id="serviceLanguageRow"><tr><td><input type="hidden" name="Languages[__index__].Id" value="00000000-0000-0000-0000-000000000000" /><label for="service-code-__index__" class="visually-hidden">Language code</label><input id="service-code-__index__" name="Languages[__index__].Code" class="form-control" maxlength="10" /></td><td>0</td><td class="service-language-actions"><div class="service-language-action-buttons"><button type="button" class="btn btn-outline-secondary remove-service-language" data-count="0">Remove</button></div></td></tr></template>
@section FormActions {
<button type="submit" form="serviceForm" class="btn btn-primary">Save service</button>
<a href="@(Model.IsNew ? "/admin/services" : $"/admin/services/{Model.Id}")" class="btn btn-secondary">Cancel</a>
}
@section Scripts { <script src="~/js/service-editor.js" asp-append-version="true"></script> }
@@ -0,0 +1,14 @@
@using HrynCo.NotificationService.DAL.Abstract.Templates
@model IReadOnlyList<NotificationServiceInfo>
@{ ViewData["Title"] = "Services"; }
<div class="page-header"><h2>Services</h2><a href="/admin/services/create" class="btn btn-primary">Create service</a></div>
@if (Model.Count == 0) { <p>No services yet. Create a service and its language codes before adding templates.</p> }
else
{
<div class="table-responsive"><table class="table table-hover"><thead><tr><th>Name</th><th>Language codes</th><th class="service-actions">Action</th></tr></thead><tbody>
@foreach (var service in Model)
{
<tr><td><a href="/admin/services/@service.Id">@service.Name</a></td><td>@string.Join(", ", service.Languages.Select(l => l.Code))</td><td class="service-actions"><a href="/admin/services/@service.Id/edit" class="btn btn-outline-primary btn-sm" aria-label="Edit service @service.Name">Edit</a></td></tr>
}
</tbody></table></div>
}