Files
hrynco-notification-service/HrynCo.NotificationService.Web/Controllers/Admin/AdminServicesController.cs
T
agrynco 81654c0d33 feat(templates): add service language template management
Add normalized service and template catalogs, scoped administration, migration, validation, and regression coverage.

Ref: IT-1601
2026-10-17 22:02:43 +03:00

126 lines
5.7 KiB
C#

using HrynCo.NotificationService.DAL.Abstract.Repositories;
using HrynCo.NotificationService.DAL.Abstract.Templates;
using HrynCo.NotificationService.Web.Controllers.Admin.ViewModels;
using Microsoft.AspNetCore.Mvc;
namespace HrynCo.NotificationService.Web.Controllers.Admin;
[Route("admin/services")]
public sealed class AdminServicesController(INotificationServiceRepository services, ILogger<AdminServicesController> logger) : Controller
{
[HttpGet("")]
public async Task<IActionResult> Index(CancellationToken ct) => View(await services.GetAllAsync(ct));
[HttpGet("create")]
public IActionResult Create() => View("Edit", new NotificationServiceEditViewModel());
[HttpGet("{id:guid}/edit")]
public async Task<IActionResult> Edit([FromRoute] Guid id, CancellationToken ct)
{
var service = await services.GetAsync(id, ct);
return service is null ? NotFound() : View(ToViewModel(service));
}
[HttpPost("save")]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Save(NotificationServiceEditViewModel model, CancellationToken ct)
{
if (!ModelState.IsValid)
return await ServiceEditorViewAsync(model, ct);
try
{
var service = new NotificationServiceInfo
{
Id = model.Id ?? Guid.Empty, Name = model.Name, Revision = model.Revision, Languages = model.Languages
};
await services.SaveAsync(service, model.IsNew, ct);
return RedirectToAction(nameof(AdminTemplatesController.Index), "AdminTemplates", new { serviceId = service.Id });
}
catch (EmailTemplateWriteException error)
{
ModelState.AddModelError("", error.Message);
}
catch (Exception error) when (error is not OperationCanceledException)
{
logger.LogError(error, "Failed to save notification service.");
ModelState.AddModelError("", "Could not save the service. Your entered values are retained; try again.");
}
return await ServiceEditorViewAsync(model, ct);
}
[HttpGet("{serviceId:guid}/languages/{languageId:guid}/delete")]
public async Task<IActionResult> ConfirmLanguageRemoval([FromRoute] Guid serviceId, [FromRoute] Guid languageId, CancellationToken ct)
{
var model = await RemovalModelAsync(serviceId, languageId, ct);
return model is null ? NotFound() : View("DeleteLanguage", model);
}
[HttpPost("{serviceId:guid}/languages/{languageId:guid}/delete")]
[ValidateAntiForgeryToken]
public async Task<IActionResult> RemoveLanguage([FromRoute] Guid serviceId, [FromRoute] Guid languageId, ServiceLanguageDeleteViewModel model, CancellationToken ct)
{
if (!ModelState.IsValid || !model.ConfirmDelete)
return await RemovalErrorAsync(serviceId, languageId, "Confirm deletion of the language and all listed versions to continue.", ct);
try
{
await services.RemoveLanguageAsync(serviceId, languageId, model.VersionCount, model.Revision, ct);
return RedirectToAction(nameof(Edit), new { id = serviceId });
}
catch (EmailTemplateWriteException error)
{
return await RemovalErrorAsync(serviceId, languageId, error.Message, ct);
}
catch (Exception error) when (error is not OperationCanceledException)
{
logger.LogError(error, "Failed to remove service language.");
return await RemovalErrorAsync(serviceId, languageId, "Could not delete the language. No changes were saved; review the count and try again.", ct);
}
}
private async Task<IActionResult> ServiceEditorViewAsync(NotificationServiceEditViewModel model, CancellationToken ct)
{
if (model.Id is not Guid id)
return View("Edit", model);
try
{
var persisted = await services.GetAsync(id, ct);
var counts = persisted?.Languages.ToDictionary(l => l.Id, l => l.VersionCount) ?? [];
foreach (var language in model.Languages)
language.VersionCount = counts.GetValueOrDefault(language.Id);
}
catch (Exception error) when (error is not OperationCanceledException)
{
logger.LogError(error, "Failed to refresh service language version counts.");
ViewData["LanguageCountsUnavailable"] = true;
ModelState.AddModelError("", "Could not refresh language usage counts. Removal is disabled until the service is available.");
}
return View("Edit", model);
}
private async Task<IActionResult> RemovalErrorAsync(Guid serviceId, Guid languageId, string error, CancellationToken ct)
{
var model = await RemovalModelAsync(serviceId, languageId, ct);
if (model is null)
return NotFound();
ModelState.Clear();
ModelState.AddModelError("", error);
return View("DeleteLanguage", model);
}
private async Task<ServiceLanguageDeleteViewModel?> RemovalModelAsync(Guid serviceId, Guid languageId, CancellationToken ct)
{
var service = await services.GetAsync(serviceId, ct);
var language = service?.Languages.SingleOrDefault(l => l.Id == languageId);
return language is null ? null : new ServiceLanguageDeleteViewModel
{
ServiceId = serviceId, LanguageId = languageId, ServiceName = service!.Name, Revision = service.Revision,
Code = language.Code, VersionCount = language.VersionCount
};
}
private static NotificationServiceEditViewModel ToViewModel(NotificationServiceInfo service) => new()
{
Id = service.Id, Name = service.Name, Revision = service.Revision, Languages = service.Languages.ToList()
};
}