From 50828d23ecf702a7f135afc490281f9a4c1f0717 Mon Sep 17 00:00:00 2001 From: Anatolii Grynchuk Date: Wed, 13 May 2026 02:08:43 +0300 Subject: [PATCH 1/2] refactor: replace internal UnitOfWork with NotificationUnitOfWork and NotificationBaseRepository - Consolidate unit of work implementation with NotificationUnitOfWork. - Refactor repositories to use NotificationBaseRepository for consistency. - Simplify request handlers by removing IUnitOfWork dependency. - Update related tests and service registration. --- Directory.Packages.props | 6 +- ...Co.NotificationService.DAL.Abstract.csproj | 8 +- .../Providers/EmailChannel.cs | 2 +- .../Providers/EmailChannelUsage.cs | 7 +- .../Core/NotificationBaseRepository.cs | 20 +++ .../Core/NotificationEfRepository.cs | 13 ++ .../Core/UnitOfWork.cs | 10 -- .../HrynCo.NotificationService.DAL.EF.csproj | 1 - .../NotificationUnitOfWork.cs | 10 ++ .../Repositories/EmailChannelRepository.cs | 41 ++--- .../EmailChannelUsageRepository.cs | 16 +- .../Repositories/EmailTemplateRepository.cs | 25 +-- .../ServiceCollectionExtensions.cs | 2 +- .../Core/RequestHandler.cs | 7 +- .../Create/CreateEmailChannelHandler.cs | 3 +- .../Delete/DeleteEmailChannelHandler.cs | 3 +- .../Get/GetEmailChannelHandler.cs | 4 +- .../GetAll/GetAllEmailChannelsHandler.cs | 4 +- .../GetByService/GetEmailChannelsHandler.cs | 4 +- .../GetChannelUsageSummaryHandler.cs | 4 +- .../EmailChannels/Send/SendEmailHandler.cs | 4 +- .../EmailChannels/TestSmtp/TestSmtpHandler.cs | 6 +- .../Update/UpdateEmailChannelHandler.cs | 3 +- .../Create/CreateEmailTemplateHandler.cs | 4 +- .../Delete/DeleteEmailTemplateHandler.cs | 3 +- .../Get/GetEmailTemplateHandler.cs | 4 +- .../GetAll/GetAllEmailTemplatesHandler.cs | 4 +- .../GetByService/GetEmailTemplatesHandler.cs | 4 +- .../Update/UpdateEmailTemplateHandler.cs | 3 +- .../HrynCo.NotificationService.Web.http | 49 +++++- .../Views/AdminTemplates/Edit.cshtml | 149 ++++++++++-------- .../wwwroot/css/admin.css | 39 ++++- 32 files changed, 276 insertions(+), 186 deletions(-) create mode 100644 HrynCo.NotificationService.DAL.EF/Core/NotificationBaseRepository.cs create mode 100644 HrynCo.NotificationService.DAL.EF/Core/NotificationEfRepository.cs delete mode 100644 HrynCo.NotificationService.DAL.EF/Core/UnitOfWork.cs create mode 100644 HrynCo.NotificationService.DAL.EF/NotificationUnitOfWork.cs diff --git a/Directory.Packages.props b/Directory.Packages.props index ebb6ee0..9d827a7 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -4,6 +4,8 @@ + + @@ -29,8 +31,6 @@ - - @@ -39,4 +39,4 @@ - + \ No newline at end of file diff --git a/HrynCo.NotificationService.DAL.Abstract/HrynCo.NotificationService.DAL.Abstract.csproj b/HrynCo.NotificationService.DAL.Abstract/HrynCo.NotificationService.DAL.Abstract.csproj index 0e19145..bcb034d 100644 --- a/HrynCo.NotificationService.DAL.Abstract/HrynCo.NotificationService.DAL.Abstract.csproj +++ b/HrynCo.NotificationService.DAL.Abstract/HrynCo.NotificationService.DAL.Abstract.csproj @@ -1,13 +1,13 @@  - - - - net10.0 enable enable + + + + diff --git a/HrynCo.NotificationService.DAL.Abstract/Providers/EmailChannel.cs b/HrynCo.NotificationService.DAL.Abstract/Providers/EmailChannel.cs index 3021251..5b1d1c6 100644 --- a/HrynCo.NotificationService.DAL.Abstract/Providers/EmailChannel.cs +++ b/HrynCo.NotificationService.DAL.Abstract/Providers/EmailChannel.cs @@ -12,4 +12,4 @@ public class EmailChannel : Entity public int? MonthlyLimit { get; set; } public int WarnThresholdPercent { get; set; } = 90; public bool IsActive { get; set; } = true; -} \ No newline at end of file +} diff --git a/HrynCo.NotificationService.DAL.Abstract/Providers/EmailChannelUsage.cs b/HrynCo.NotificationService.DAL.Abstract/Providers/EmailChannelUsage.cs index 18ac01f..c4d120d 100644 --- a/HrynCo.NotificationService.DAL.Abstract/Providers/EmailChannelUsage.cs +++ b/HrynCo.NotificationService.DAL.Abstract/Providers/EmailChannelUsage.cs @@ -1,13 +1,14 @@ -using HrynCo.DAL.Abstract.Entities; - namespace HrynCo.NotificationService.DAL.Abstract.Providers; /// /// Tracks email send counts per EmailChannel per calendar day. /// Monthly counts are derived by summing daily records within a month. /// -public class EmailChannelUsage : Entity +public class EmailChannelUsage { + public Guid Id { get; set; } + public DateTimeOffset Created { get; set; } + public DateTimeOffset? Updated { get; set; } public Guid ProviderId { get; set; } public DateOnly Date { get; set; } public int SentCount { get; set; } diff --git a/HrynCo.NotificationService.DAL.EF/Core/NotificationBaseRepository.cs b/HrynCo.NotificationService.DAL.EF/Core/NotificationBaseRepository.cs new file mode 100644 index 0000000..dcfe09e --- /dev/null +++ b/HrynCo.NotificationService.DAL.EF/Core/NotificationBaseRepository.cs @@ -0,0 +1,20 @@ +namespace HrynCo.NotificationService.DAL.EF.Core; + +using HrynCo.DAL.Abstract.Entities; +using HrynCo.DAL.EF.Core; + +public abstract class NotificationBaseRepository + : BaseRepository, NotificationDbContext, TEntity, Guid> where TEntity : Entity +{ + protected NotificationBaseRepository(NotificationDbContext dbContext) + { + DbContext = dbContext; + } + + private NotificationDbContext DbContext { get; set; } + + protected override NotificationEfRepository CreateEfRepository() + { + return new NotificationEfRepository(DbContext); + } +} \ No newline at end of file diff --git a/HrynCo.NotificationService.DAL.EF/Core/NotificationEfRepository.cs b/HrynCo.NotificationService.DAL.EF/Core/NotificationEfRepository.cs new file mode 100644 index 0000000..634751f --- /dev/null +++ b/HrynCo.NotificationService.DAL.EF/Core/NotificationEfRepository.cs @@ -0,0 +1,13 @@ +namespace HrynCo.NotificationService.DAL.EF.Core; + +using HrynCo.DAL.Abstract.Entities; +using HrynCo.DAL.EF.Core; + +public class NotificationEfRepository : BaseEfRepository + where TEntity : class, IEntity +{ + public NotificationEfRepository(NotificationDbContext dbContext) : + base(dbContext) + { + } +} \ No newline at end of file diff --git a/HrynCo.NotificationService.DAL.EF/Core/UnitOfWork.cs b/HrynCo.NotificationService.DAL.EF/Core/UnitOfWork.cs deleted file mode 100644 index f6a557a..0000000 --- a/HrynCo.NotificationService.DAL.EF/Core/UnitOfWork.cs +++ /dev/null @@ -1,10 +0,0 @@ -using HrynCo.DAL.EF.Core; - -namespace HrynCo.NotificationService.DAL.EF.Core; - -internal sealed class UnitOfWork : EfUnitOfWork -{ - public UnitOfWork(NotificationDbContext context) : base(context) - { - } -} diff --git a/HrynCo.NotificationService.DAL.EF/HrynCo.NotificationService.DAL.EF.csproj b/HrynCo.NotificationService.DAL.EF/HrynCo.NotificationService.DAL.EF.csproj index 5b7530d..216a1df 100644 --- a/HrynCo.NotificationService.DAL.EF/HrynCo.NotificationService.DAL.EF.csproj +++ b/HrynCo.NotificationService.DAL.EF/HrynCo.NotificationService.DAL.EF.csproj @@ -6,7 +6,6 @@ - runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/HrynCo.NotificationService.DAL.EF/NotificationUnitOfWork.cs b/HrynCo.NotificationService.DAL.EF/NotificationUnitOfWork.cs new file mode 100644 index 0000000..21d8929 --- /dev/null +++ b/HrynCo.NotificationService.DAL.EF/NotificationUnitOfWork.cs @@ -0,0 +1,10 @@ +namespace HrynCo.NotificationService.DAL.EF; + +using HrynCo.DAL.EF.Core; + +public class NotificationUnitOfWork : EfUnitOfWork +{ + public NotificationUnitOfWork(NotificationDbContext context) : base(context) + { + } +} diff --git a/HrynCo.NotificationService.DAL.EF/Repositories/EmailChannelRepository.cs b/HrynCo.NotificationService.DAL.EF/Repositories/EmailChannelRepository.cs index c7cbf3a..8234425 100644 --- a/HrynCo.NotificationService.DAL.EF/Repositories/EmailChannelRepository.cs +++ b/HrynCo.NotificationService.DAL.EF/Repositories/EmailChannelRepository.cs @@ -1,13 +1,13 @@ +namespace HrynCo.NotificationService.DAL.EF.Repositories; + using System.Text.Json; using HrynCo.NotificationService.DAL.Abstract.Providers; using HrynCo.NotificationService.DAL.Abstract.Repositories; -using HrynCo.DAL.EF.Core; +using HrynCo.NotificationService.DAL.EF.Core; using HrynCo.NotificationService.DAL.EF.Entities; using Microsoft.EntityFrameworkCore; -namespace HrynCo.NotificationService.DAL.EF.Repositories; - -internal sealed class EmailChannelRepository : EfRepository, IEmailChannelRepository +internal sealed class EmailChannelRepository : NotificationBaseRepository, IEmailChannelRepository { public EmailChannelRepository(NotificationDbContext dbContext) : base(dbContext) { @@ -15,20 +15,14 @@ internal sealed class EmailChannelRepository : EfRepository> GetAllAsync(CancellationToken ct = default) { - var entities = await DbSet - .AsNoTracking() - .OrderBy(x => x.ServiceName) - .ThenBy(x => x.Priority) - .ToListAsync(ct); + var entities = await EfRepository.Get().ToListAsync(ct); return entities.Select(MapToDomain).ToList(); } public async Task> GetByServiceAsync(string serviceName, CancellationToken ct = default) { - var entities = await DbSet - .AsNoTracking() - .Where(x => x.ServiceName == serviceName) + var entities = await EfRepository.Get(x => x.ServiceName == serviceName) .OrderBy(x => x.Priority) .ToListAsync(ct); @@ -38,8 +32,7 @@ internal sealed class EmailChannelRepository : EfRepository> GetAllWithUsageSummaryAsync( DateOnly today, CancellationToken ct = default) { - var rows = await DbSet - .AsNoTracking() + var rows = await EfRepository.Get() .OrderBy(c => c.ServiceName) .ThenBy(c => c.Priority) .Select(c => new @@ -61,30 +54,24 @@ internal sealed class EmailChannelRepository : EfRepository GetByIdAsync(Guid id, CancellationToken ct = default) { - EmailChannelEntity? entity = await DbSet.AsNoTracking().FirstOrDefaultAsync(x => x.Id == id, ct); + EmailChannelEntity? entity = await EfRepository.GetByIdAsync(id); return entity is null ? null : MapToDomain(entity); } public Task AddAsync(EmailChannel channel, CancellationToken ct = default) { - return base.AddAsync(MapToEntity(channel), ct); + return EfRepository.AddAsync(MapToEntity(channel)); } - public Task UpdateAsync(EmailChannel channel, CancellationToken ct = default) + public async Task UpdateAsync(EmailChannel channel, CancellationToken ct = default) { EmailChannelEntity entity = MapToEntity(channel); - entity.Updated = DateTimeOffset.UtcNow; - Update(entity); - return Task.CompletedTask; + await EfRepository.UpdateAsync(entity); } public async Task DeleteAsync(EmailChannel channel, CancellationToken ct = default) { - EmailChannelEntity? entity = await DbSet.FindAsync([channel.Id], ct); - if (entity is not null) - { - Delete(entity); - } + await EfRepository.DeleteAsync(channel.Id); } private static EmailChannel MapToDomain(EmailChannelEntity e) @@ -128,8 +115,8 @@ internal sealed class EmailChannelRepository : EfRepository JsonSerializer.Deserialize(json) - ?? throw new InvalidOperationException( - "Failed to deserialize SMTP EmailChannel settings."), + ?? throw new InvalidOperationException( + "Failed to deserialize SMTP EmailChannel settings."), _ => throw new InvalidOperationException($"Unknown or undefined email channel type: {type}") }; } diff --git a/HrynCo.NotificationService.DAL.EF/Repositories/EmailChannelUsageRepository.cs b/HrynCo.NotificationService.DAL.EF/Repositories/EmailChannelUsageRepository.cs index 62ab9a5..f8c38fb 100644 --- a/HrynCo.NotificationService.DAL.EF/Repositories/EmailChannelUsageRepository.cs +++ b/HrynCo.NotificationService.DAL.EF/Repositories/EmailChannelUsageRepository.cs @@ -1,11 +1,11 @@ using HrynCo.NotificationService.DAL.Abstract.Repositories; -using HrynCo.DAL.EF.Core; +using HrynCo.NotificationService.DAL.EF.Core; using HrynCo.NotificationService.DAL.EF.Entities; using Microsoft.EntityFrameworkCore; namespace HrynCo.NotificationService.DAL.EF.Repositories; -internal sealed class EmailChannelUsageRepository : EfRepository, IEmailChannelUsageRepository +internal sealed class EmailChannelUsageRepository : NotificationBaseRepository, IEmailChannelUsageRepository { public EmailChannelUsageRepository(NotificationDbContext dbContext) : base(dbContext) { @@ -13,7 +13,8 @@ internal sealed class EmailChannelUsageRepository : EfRepository GetDailyCountAsync(Guid providerId, DateOnly date, CancellationToken ct = default) { - EmailChannelUsageEntity? entity = await DbSet + EmailChannelUsageEntity? entity = await EfRepository.Get() + .AsNoTracking() .FirstOrDefaultAsync(x => x.ProviderId == providerId && x.Date == date, ct); return entity?.SentCount ?? 0; @@ -21,7 +22,7 @@ internal sealed class EmailChannelUsageRepository : EfRepository GetMonthlyCountAsync(Guid providerId, int year, int month, CancellationToken ct = default) { - return await DbSet + return await EfRepository.Get() .Where(x => x.ProviderId == providerId && x.Date.Year == year && x.Date.Month == month) @@ -30,15 +31,16 @@ internal sealed class EmailChannelUsageRepository : EfRepository x.ProviderId == providerId && x.Date == date, ct); if (entity is null) - await AddAsync(new EmailChannelUsageEntity { ProviderId = providerId, Date = date, SentCount = 1 }, ct); + await EfRepository.AddAsync(new EmailChannelUsageEntity { ProviderId = providerId, Date = date, SentCount = 1 }); else { entity.SentCount++; - Update(entity); + await EfRepository.UpdateAsync(entity); } } } \ No newline at end of file diff --git a/HrynCo.NotificationService.DAL.EF/Repositories/EmailTemplateRepository.cs b/HrynCo.NotificationService.DAL.EF/Repositories/EmailTemplateRepository.cs index 6bf0e25..32b1063 100644 --- a/HrynCo.NotificationService.DAL.EF/Repositories/EmailTemplateRepository.cs +++ b/HrynCo.NotificationService.DAL.EF/Repositories/EmailTemplateRepository.cs @@ -1,12 +1,13 @@ using HrynCo.NotificationService.DAL.Abstract.Repositories; using HrynCo.NotificationService.DAL.Abstract.Templates; -using HrynCo.DAL.EF.Core; +using HrynCo.NotificationService.DAL.EF.Core; using HrynCo.NotificationService.DAL.EF.Entities; using Microsoft.EntityFrameworkCore; namespace HrynCo.NotificationService.DAL.EF.Repositories; -internal sealed class EmailTemplateRepository : EfRepository, IEmailTemplateRepository +internal sealed class EmailTemplateRepository + : NotificationBaseRepository, IEmailTemplateRepository { public EmailTemplateRepository(NotificationDbContext dbContext) : base(dbContext) { @@ -14,7 +15,7 @@ internal sealed class EmailTemplateRepository : EfRepository> GetAllAsync(CancellationToken ct = default) { - List entities = await DbSet + List entities = await EfRepository.Get() .AsNoTracking() .ToListAsync(ct); return entities.Select(MapToDomain).ToList(); @@ -22,7 +23,7 @@ internal sealed class EmailTemplateRepository : EfRepository> GetByServiceAsync(string serviceName, CancellationToken ct = default) { - List entities = await DbSet + List entities = await EfRepository.Get() .AsNoTracking() .Where(x => x.ServiceName == serviceName) .ToListAsync(ct); @@ -32,7 +33,7 @@ internal sealed class EmailTemplateRepository : EfRepository GetAsync(string serviceName, string key, string languageCode, CancellationToken ct = default) { - EmailTemplateEntity? entity = await DbSet + EmailTemplateEntity? entity = await EfRepository.Get() .AsNoTracking() .FirstOrDefaultAsync( x => x.ServiceName == serviceName && x.Key == key && x.LanguageCode == languageCode, ct); @@ -40,22 +41,24 @@ internal sealed class EmailTemplateRepository : EfRepository - base.AddAsync(MapToEntity(EmailTemplate), ct); + public Task AddAsync(EmailTemplate EmailTemplate, CancellationToken ct = default) + { + return EfRepository.AddAsync(MapToEntity(EmailTemplate)); + } public Task UpdateAsync(EmailTemplate EmailTemplate, CancellationToken ct = default) { EmailTemplateEntity entity = MapToEntity(EmailTemplate); entity.Updated = DateTimeOffset.UtcNow; - Update(entity); - return Task.CompletedTask; + return EfRepository.UpdateAsync(entity); } public async Task DeleteAsync(EmailTemplate EmailTemplate, CancellationToken ct = default) { - EmailTemplateEntity? entity = await DbSet.FindAsync([EmailTemplate.Id], ct); + EmailTemplateEntity? entity = await EfRepository.Get() + .FirstOrDefaultAsync(x => x.Id == EmailTemplate.Id, ct); if (entity is not null) - Delete(entity); + await EfRepository.DeleteAsync(entity); } private static EmailTemplate MapToDomain(EmailTemplateEntity e) => new() diff --git a/HrynCo.NotificationService.DAL.EF/ServiceCollectionExtensions.cs b/HrynCo.NotificationService.DAL.EF/ServiceCollectionExtensions.cs index 10a9a7d..0587f4e 100644 --- a/HrynCo.NotificationService.DAL.EF/ServiceCollectionExtensions.cs +++ b/HrynCo.NotificationService.DAL.EF/ServiceCollectionExtensions.cs @@ -16,10 +16,10 @@ public static class ServiceCollectionExtensions services.AddDbContext(options => options.UseNpgsql(connectionString)); - services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); return services; } diff --git a/HrynCo.NotificationService.Services/Core/RequestHandler.cs b/HrynCo.NotificationService.Services/Core/RequestHandler.cs index 7fb281d..54e7c2c 100644 --- a/HrynCo.NotificationService.Services/Core/RequestHandler.cs +++ b/HrynCo.NotificationService.Services/Core/RequestHandler.cs @@ -1,4 +1,3 @@ -using HrynCo.DAL.Abstract; using HrynCo.NotificationService.Services.Logging; using MediatR; using Serilog; @@ -8,14 +7,12 @@ namespace HrynCo.NotificationService.Services.Core; public abstract class RequestHandler : IRequestHandler where TRequest : IRequest { - protected RequestHandler(IContextualSerilogLogger logger, IUnitOfWork unitOfWork) + protected RequestHandler(IContextualSerilogLogger logger) { Logger = logger.Logger; - UnitOfWork = unitOfWork; } protected ILogger Logger { get; } - protected IUnitOfWork UnitOfWork { get; } public Task Handle(TRequest request, CancellationToken cancellationToken) { @@ -23,4 +20,4 @@ public abstract class RequestHandler : IRequestHandler DoOnHandle(TRequest request, CancellationToken cancellationToken); -} \ No newline at end of file +} diff --git a/HrynCo.NotificationService.Services/EmailChannels/Create/CreateEmailChannelHandler.cs b/HrynCo.NotificationService.Services/EmailChannels/Create/CreateEmailChannelHandler.cs index d3d584b..ef3ff21 100644 --- a/HrynCo.NotificationService.Services/EmailChannels/Create/CreateEmailChannelHandler.cs +++ b/HrynCo.NotificationService.Services/EmailChannels/Create/CreateEmailChannelHandler.cs @@ -14,9 +14,8 @@ internal sealed class CreateEmailChannelHandler public CreateEmailChannelHandler( IContextualSerilogLogger logger, - IUnitOfWork unitOfWork, IEmailChannelRepository channels) - : base(logger, unitOfWork) + : base(logger) { _channels = channels; } diff --git a/HrynCo.NotificationService.Services/EmailChannels/Delete/DeleteEmailChannelHandler.cs b/HrynCo.NotificationService.Services/EmailChannels/Delete/DeleteEmailChannelHandler.cs index 785af9f..c1d64a9 100644 --- a/HrynCo.NotificationService.Services/EmailChannels/Delete/DeleteEmailChannelHandler.cs +++ b/HrynCo.NotificationService.Services/EmailChannels/Delete/DeleteEmailChannelHandler.cs @@ -13,9 +13,8 @@ internal sealed class DeleteEmailChannelHandler public DeleteEmailChannelHandler( IContextualSerilogLogger logger, - IUnitOfWork unitOfWork, IEmailChannelRepository channels) - : base(logger, unitOfWork) + : base(logger) { _channels = channels; } diff --git a/HrynCo.NotificationService.Services/EmailChannels/Get/GetEmailChannelHandler.cs b/HrynCo.NotificationService.Services/EmailChannels/Get/GetEmailChannelHandler.cs index 6276aa6..867fd2d 100644 --- a/HrynCo.NotificationService.Services/EmailChannels/Get/GetEmailChannelHandler.cs +++ b/HrynCo.NotificationService.Services/EmailChannels/Get/GetEmailChannelHandler.cs @@ -1,4 +1,3 @@ -using HrynCo.DAL.Abstract; using HrynCo.NotificationService.DAL.Abstract.Providers; using HrynCo.NotificationService.DAL.Abstract.Repositories; using HrynCo.NotificationService.Services.Core; @@ -14,9 +13,8 @@ internal sealed class GetEmailChannelHandler public GetEmailChannelHandler( IContextualSerilogLogger logger, - IUnitOfWork unitOfWork, IEmailChannelRepository channels) - : base(logger, unitOfWork) + : base(logger) { _channels = channels; } diff --git a/HrynCo.NotificationService.Services/EmailChannels/GetAll/GetAllEmailChannelsHandler.cs b/HrynCo.NotificationService.Services/EmailChannels/GetAll/GetAllEmailChannelsHandler.cs index 65fa009..51cc638 100644 --- a/HrynCo.NotificationService.Services/EmailChannels/GetAll/GetAllEmailChannelsHandler.cs +++ b/HrynCo.NotificationService.Services/EmailChannels/GetAll/GetAllEmailChannelsHandler.cs @@ -1,4 +1,3 @@ -using HrynCo.DAL.Abstract; using HrynCo.NotificationService.DAL.Abstract.Providers; using HrynCo.NotificationService.DAL.Abstract.Repositories; using HrynCo.NotificationService.Services.Core; @@ -14,9 +13,8 @@ internal sealed class GetAllEmailChannelsHandler public GetAllEmailChannelsHandler( IContextualSerilogLogger logger, - IUnitOfWork unitOfWork, IEmailChannelRepository channels) - : base(logger, unitOfWork) + : base(logger) { _channels = channels; } diff --git a/HrynCo.NotificationService.Services/EmailChannels/GetByService/GetEmailChannelsHandler.cs b/HrynCo.NotificationService.Services/EmailChannels/GetByService/GetEmailChannelsHandler.cs index 74b0870..e2dc157 100644 --- a/HrynCo.NotificationService.Services/EmailChannels/GetByService/GetEmailChannelsHandler.cs +++ b/HrynCo.NotificationService.Services/EmailChannels/GetByService/GetEmailChannelsHandler.cs @@ -1,4 +1,3 @@ -using HrynCo.DAL.Abstract; using HrynCo.NotificationService.DAL.Abstract.Providers; using HrynCo.NotificationService.DAL.Abstract.Repositories; using HrynCo.NotificationService.Services.Core; @@ -14,9 +13,8 @@ internal sealed class GetEmailChannelsHandler public GetEmailChannelsHandler( IContextualSerilogLogger logger, - IUnitOfWork unitOfWork, IEmailChannelRepository channels) - : base(logger, unitOfWork) + : base(logger) { _channels = channels; } diff --git a/HrynCo.NotificationService.Services/EmailChannels/GetUsageSummary/GetChannelUsageSummaryHandler.cs b/HrynCo.NotificationService.Services/EmailChannels/GetUsageSummary/GetChannelUsageSummaryHandler.cs index 95f19f9..57c76d8 100644 --- a/HrynCo.NotificationService.Services/EmailChannels/GetUsageSummary/GetChannelUsageSummaryHandler.cs +++ b/HrynCo.NotificationService.Services/EmailChannels/GetUsageSummary/GetChannelUsageSummaryHandler.cs @@ -1,4 +1,3 @@ -using HrynCo.DAL.Abstract; using HrynCo.NotificationService.DAL.Abstract.Repositories; using HrynCo.NotificationService.Services.Core; using HrynCo.NotificationService.Services.Logging; @@ -13,9 +12,8 @@ internal sealed class GetChannelUsageSummaryHandler public GetChannelUsageSummaryHandler( IContextualSerilogLogger logger, - IUnitOfWork unitOfWork, IEmailChannelRepository channelsRepository) - : base(logger, unitOfWork) + : base(logger) { _channelsRepository = channelsRepository; } diff --git a/HrynCo.NotificationService.Services/EmailChannels/Send/SendEmailHandler.cs b/HrynCo.NotificationService.Services/EmailChannels/Send/SendEmailHandler.cs index e79ff1e..ed5a9c5 100644 --- a/HrynCo.NotificationService.Services/EmailChannels/Send/SendEmailHandler.cs +++ b/HrynCo.NotificationService.Services/EmailChannels/Send/SendEmailHandler.cs @@ -1,6 +1,5 @@ using System.Net; using System.Net.Mail; -using HrynCo.DAL.Abstract; using HrynCo.NotificationService.DAL.Abstract.Providers; using HrynCo.NotificationService.DAL.Abstract.Repositories; using HrynCo.NotificationService.Services.Core; @@ -17,10 +16,9 @@ internal sealed class SendEmailHandler public SendEmailHandler( IContextualSerilogLogger logger, - IUnitOfWork unitOfWork, IEmailChannelRepository channels, IEmailChannelUsageRepository usage) - : base(logger, unitOfWork) + : base(logger) { _channels = channels; _usage = usage; diff --git a/HrynCo.NotificationService.Services/EmailChannels/TestSmtp/TestSmtpHandler.cs b/HrynCo.NotificationService.Services/EmailChannels/TestSmtp/TestSmtpHandler.cs index ba03b3d..573a9ee 100644 --- a/HrynCo.NotificationService.Services/EmailChannels/TestSmtp/TestSmtpHandler.cs +++ b/HrynCo.NotificationService.Services/EmailChannels/TestSmtp/TestSmtpHandler.cs @@ -1,6 +1,5 @@ using System.Net; using System.Net.Mail; -using HrynCo.DAL.Abstract; using HrynCo.NotificationService.Services.Core; using HrynCo.NotificationService.Services.Logging; using static HrynCo.NotificationService.Services.Core.ServiceResultHelper; @@ -11,9 +10,8 @@ internal sealed class TestSmtpHandler : RequestHandler> { public TestSmtpHandler( - IContextualSerilogLogger logger, - IUnitOfWork unitOfWork) - : base(logger, unitOfWork) + IContextualSerilogLogger logger) + : base(logger) { } diff --git a/HrynCo.NotificationService.Services/EmailChannels/Update/UpdateEmailChannelHandler.cs b/HrynCo.NotificationService.Services/EmailChannels/Update/UpdateEmailChannelHandler.cs index fdd654f..0f4da77 100644 --- a/HrynCo.NotificationService.Services/EmailChannels/Update/UpdateEmailChannelHandler.cs +++ b/HrynCo.NotificationService.Services/EmailChannels/Update/UpdateEmailChannelHandler.cs @@ -13,9 +13,8 @@ internal sealed class UpdateEmailChannelHandler public UpdateEmailChannelHandler( IContextualSerilogLogger logger, - IUnitOfWork unitOfWork, IEmailChannelRepository channels) - : base(logger, unitOfWork) + : base(logger) { _channels = channels; } diff --git a/HrynCo.NotificationService.Services/EmailTemplates/Create/CreateEmailTemplateHandler.cs b/HrynCo.NotificationService.Services/EmailTemplates/Create/CreateEmailTemplateHandler.cs index 269b4d6..7800374 100644 --- a/HrynCo.NotificationService.Services/EmailTemplates/Create/CreateEmailTemplateHandler.cs +++ b/HrynCo.NotificationService.Services/EmailTemplates/Create/CreateEmailTemplateHandler.cs @@ -1,4 +1,3 @@ -using HrynCo.DAL.Abstract; using HrynCo.NotificationService.DAL.Abstract.Repositories; using HrynCo.NotificationService.DAL.Abstract.Templates; using HrynCo.NotificationService.Services.Core; @@ -14,9 +13,8 @@ internal sealed class CreateEmailTemplateHandler public CreateEmailTemplateHandler( IContextualSerilogLogger logger, - IUnitOfWork unitOfWork, IEmailTemplateRepository templates) - : base(logger, unitOfWork) + : base(logger) { _templates = templates; } diff --git a/HrynCo.NotificationService.Services/EmailTemplates/Delete/DeleteEmailTemplateHandler.cs b/HrynCo.NotificationService.Services/EmailTemplates/Delete/DeleteEmailTemplateHandler.cs index da4e960..b22b3c6 100644 --- a/HrynCo.NotificationService.Services/EmailTemplates/Delete/DeleteEmailTemplateHandler.cs +++ b/HrynCo.NotificationService.Services/EmailTemplates/Delete/DeleteEmailTemplateHandler.cs @@ -13,9 +13,8 @@ internal sealed class DeleteEmailTemplateHandler public DeleteEmailTemplateHandler( IContextualSerilogLogger logger, - IUnitOfWork unitOfWork, IEmailTemplateRepository templates) - : base(logger, unitOfWork) + : base(logger) { _templates = templates; } diff --git a/HrynCo.NotificationService.Services/EmailTemplates/Get/GetEmailTemplateHandler.cs b/HrynCo.NotificationService.Services/EmailTemplates/Get/GetEmailTemplateHandler.cs index e685672..70819aa 100644 --- a/HrynCo.NotificationService.Services/EmailTemplates/Get/GetEmailTemplateHandler.cs +++ b/HrynCo.NotificationService.Services/EmailTemplates/Get/GetEmailTemplateHandler.cs @@ -1,4 +1,3 @@ -using HrynCo.DAL.Abstract; using HrynCo.NotificationService.DAL.Abstract.Repositories; using HrynCo.NotificationService.DAL.Abstract.Templates; using HrynCo.NotificationService.Services.Core; @@ -14,9 +13,8 @@ internal sealed class GetEmailTemplateHandler public GetEmailTemplateHandler( IContextualSerilogLogger logger, - IUnitOfWork unitOfWork, IEmailTemplateRepository templates) - : base(logger, unitOfWork) + : base(logger) { _templates = templates; } diff --git a/HrynCo.NotificationService.Services/EmailTemplates/GetAll/GetAllEmailTemplatesHandler.cs b/HrynCo.NotificationService.Services/EmailTemplates/GetAll/GetAllEmailTemplatesHandler.cs index 5a87f86..ac762f5 100644 --- a/HrynCo.NotificationService.Services/EmailTemplates/GetAll/GetAllEmailTemplatesHandler.cs +++ b/HrynCo.NotificationService.Services/EmailTemplates/GetAll/GetAllEmailTemplatesHandler.cs @@ -1,4 +1,3 @@ -using HrynCo.DAL.Abstract; using HrynCo.NotificationService.DAL.Abstract.Repositories; using HrynCo.NotificationService.DAL.Abstract.Templates; using HrynCo.NotificationService.Services.Core; @@ -14,9 +13,8 @@ internal sealed class GetAllEmailTemplatesHandler public GetAllEmailTemplatesHandler( IContextualSerilogLogger logger, - IUnitOfWork unitOfWork, IEmailTemplateRepository templates) - : base(logger, unitOfWork) + : base(logger) { _templates = templates; } diff --git a/HrynCo.NotificationService.Services/EmailTemplates/GetByService/GetEmailTemplatesHandler.cs b/HrynCo.NotificationService.Services/EmailTemplates/GetByService/GetEmailTemplatesHandler.cs index 979da8d..63cf086 100644 --- a/HrynCo.NotificationService.Services/EmailTemplates/GetByService/GetEmailTemplatesHandler.cs +++ b/HrynCo.NotificationService.Services/EmailTemplates/GetByService/GetEmailTemplatesHandler.cs @@ -1,4 +1,3 @@ -using HrynCo.DAL.Abstract; using HrynCo.NotificationService.DAL.Abstract.Repositories; using HrynCo.NotificationService.DAL.Abstract.Templates; using HrynCo.NotificationService.Services.Core; @@ -14,9 +13,8 @@ internal sealed class GetEmailTemplatesHandler public GetEmailTemplatesHandler( IContextualSerilogLogger logger, - IUnitOfWork unitOfWork, IEmailTemplateRepository templates) - : base(logger, unitOfWork) + : base(logger) { _templates = templates; } diff --git a/HrynCo.NotificationService.Services/EmailTemplates/Update/UpdateEmailTemplateHandler.cs b/HrynCo.NotificationService.Services/EmailTemplates/Update/UpdateEmailTemplateHandler.cs index a9d101a..1cd842d 100644 --- a/HrynCo.NotificationService.Services/EmailTemplates/Update/UpdateEmailTemplateHandler.cs +++ b/HrynCo.NotificationService.Services/EmailTemplates/Update/UpdateEmailTemplateHandler.cs @@ -13,9 +13,8 @@ internal sealed class UpdateEmailTemplateHandler public UpdateEmailTemplateHandler( IContextualSerilogLogger logger, - IUnitOfWork unitOfWork, IEmailTemplateRepository templates) - : base(logger, unitOfWork) + : base(logger) { _templates = templates; } diff --git a/HrynCo.NotificationService.Web/HrynCo.NotificationService.Web.http b/HrynCo.NotificationService.Web/HrynCo.NotificationService.Web.http index 51c1dc1..2e3119a 100644 --- a/HrynCo.NotificationService.Web/HrynCo.NotificationService.Web.http +++ b/HrynCo.NotificationService.Web/HrynCo.NotificationService.Web.http @@ -1,6 +1,47 @@ -@HrynCo.NotificationService.Api_HostAddress = http://localhost:5188 +@host = http://localhost:5188 -GET {{HrynCo.NotificationService.Api_HostAddress}}/weatherforecast/ -Accept: application/json +### Create a new email template +POST {{host}}/api/v1/email-templates +Content-Type: application/json -### +{ + "ServiceName": "StoreMate-Prod", + "Key": "ShareInvite", + "LanguageCode": "uk", + "Subject": "Вас запрошено", + "HtmlBody": "

Вітаємо, \u007b\u007bRecipientName\u007d\u007d.

Вас запрошено

\u007b\u007bInviterName\u007d\u007d запросив вас приєднатися до \u007b\u007bAppName\u007d\u007d, щоб ви могли безпечно співпрацювати.

Відкрити запрошення

Запрошення дійсне до \u007b\u007bValidUntil\u007d\u007d.

", + "TextBody": "Вітаємо, \u007b\u007bRecipientName\u007d\u007d.\n\n\u007b\u007bInviterName\u007d\u007d запросив вас приєднатися до \u007b\u007bAppName\u007d\u007d, щоб ви могли безпечно співпрацювати.\n\nВідкрийте запрошення: \u007b\u007bInviteLink\u007d\u007d\nДійсне до: \u007b\u007bValidUntil\u007d\u007d", + "Variables": [ + { "Name": "RecipientName", "Required": false }, + { "Name": "InviterName", "Required": false }, + { "Name": "AppName", "Required": false }, + { "Name": "InviteLink", "Required": false }, + { "Name": "ValidUntil", "Required": false } + ] +} + +### Get the created template +GET {{host}}/api/v1/email-templates/StoreMate-Prod/ShareInvite/uk + +### List all templates for the service +GET {{host}}/api/v1/email-templates?serviceName=StoreMate-Prod + +### Update the template +PUT {{host}}/api/v1/email-templates/StoreMate-Prod/ShareInvite/uk +Content-Type: application/json + +{ + "Subject": "Вас запрошено", + "HtmlBody": "

Вітаємо, \u007b\u007bRecipientName\u007d\u007d.

\u007b\u007bInviterName\u007d\u007d запросив вас приєднатися до \u007b\u007bAppName\u007d\u007d.

Відкрити запрошення

Дійсне до \u007b\u007bValidUntil\u007d\u007d.

", + "TextBody": "Вітаємо, \u007b\u007bRecipientName\u007d\u007d.\n\n\u007b\u007bInviterName\u007d\u007d запросив вас приєднатися до \u007b\u007bAppName\u007d\u007d.\n\nВідкрийте запрошення: \u007b\u007bInviteLink\u007d\u007d\nДійсне до: \u007b\u007bValidUntil\u007d\u007d", + "Variables": [ + { "Name": "RecipientName", "Required": false }, + { "Name": "InviterName", "Required": false }, + { "Name": "AppName", "Required": false }, + { "Name": "InviteLink", "Required": false }, + { "Name": "ValidUntil", "Required": false } + ] +} + +### Delete the template +DELETE {{host}}/api/v1/email-templates/StoreMate-Prod/ShareInvite/uk diff --git a/HrynCo.NotificationService.Web/Views/AdminTemplates/Edit.cshtml b/HrynCo.NotificationService.Web/Views/AdminTemplates/Edit.cshtml index 891e163..5171b6e 100644 --- a/HrynCo.NotificationService.Web/Views/AdminTemplates/Edit.cshtml +++ b/HrynCo.NotificationService.Web/Views/AdminTemplates/Edit.cshtml @@ -21,74 +21,93 @@ } -
-
- - - -
-
- - - -
-
- - - -
-
+ -
- - - -
- -
- - - -
- -
- - -
- -
- - - -
JSON array of {"name":"...", "required":true|false}
-
- -
-
-
-
Preview
-
Rendered with sample values from the variable list.
-
- Ready -
- -
-
-
Sample values
-
-
Change these values to see the rendered output update immediately.
+
+
+
+
+ + + +
+
+ + + +
+
+ + + +
-
-
Rendered subject
-
+
+ + + +
-
Rendered HTML
- +
+ + + +
-
Rendered text
-

+            
+ + +
+ +
+ + + +
JSON array of {"name":"...", "required":true|false}
+
+
+ +
+
+
+
+
Preview
+
Rendered with sample values from the variable list.
+
+ Ready +
+ +
+
+
Sample values
+
+
Change these values to see the rendered output update immediately.
+
+ +
+
Rendered subject
+
+ +
Rendered HTML
+ + +
Rendered text
+

+                    
+
@@ -114,6 +133,7 @@ const previewText = document.getElementById('previewText'); const previewFrame = document.getElementById('previewHtmlFrame'); const previewStatus = document.getElementById('previewStatus'); + const previewTab = document.getElementById('preview-tab'); if (!subjectField || !htmlField || !textField || !variablesField || !previewVariablesHost || !previewSubject || !previewText || !previewFrame || !previewStatus) { return; @@ -267,6 +287,9 @@ subjectField.addEventListener('input', updatePreview); htmlField.addEventListener('input', updatePreview); textField.addEventListener('input', updatePreview); + if (previewTab) { + previewTab.addEventListener('shown.bs.tab', updatePreview); + } renderVariableInputs(); })(); diff --git a/HrynCo.NotificationService.Web/wwwroot/css/admin.css b/HrynCo.NotificationService.Web/wwwroot/css/admin.css index 40bbb6d..eb8a068 100644 --- a/HrynCo.NotificationService.Web/wwwroot/css/admin.css +++ b/HrynCo.NotificationService.Web/wwwroot/css/admin.css @@ -118,9 +118,10 @@ body { .empty-state .bi { font-size: 2.5rem; opacity: .35; display: block; margin-bottom: .75rem; } .empty-state p { font-size: .9rem; margin-bottom: 0; } -/* ── Editor wrapper — constrains width ───────────────── */ +/* ── Editor wrapper ──────────────────────────────────── */ .editor-wrapper { - max-width: 860px; + width: 100%; + max-width: none; } /* ── Editor card ──────────────────────────────────────── */ @@ -154,6 +155,26 @@ body { border-radius: 0 0 .5rem .5rem !important; } +/* ── Editor tabs ─────────────────────────────────────── */ +.template-editor-tabs { + border-bottom-color: #dce3eb; +} + +.template-editor-tabs .nav-link { + color: #526072; + font-weight: 600; + border-radius: .5rem .5rem 0 0; +} + +.template-editor-tabs .nav-link.active { + color: #0d6efd; +} + +/* ── Tab content ──────────────────────────────────────── */ +.tab-content { + min-width: 0; +} + /* ── Email template preview ───────────────────────────── */ .template-preview-panel { border: 1px solid #dce3eb; @@ -185,9 +206,9 @@ body { margin-top: .15rem; } -.template-preview-grid { +.template-preview-body { display: grid; - grid-template-columns: minmax(240px, 300px) minmax(0, 1fr); + grid-template-columns: minmax(0, 1fr); gap: 1rem; padding: 1rem 1.25rem 1.25rem; } @@ -197,6 +218,13 @@ body { min-width: 0; } +.template-preview-section-block { + padding: 1rem; + border: 1px solid #e5ebf2; + border-radius: .65rem; + background: #fafcff; +} + .template-preview-section-title { font-size: .7rem; font-weight: 700; @@ -210,6 +238,7 @@ body { display: flex; flex-direction: column; gap: .75rem; + max-width: 420px; } .template-preview-variables .form-control-sm { @@ -248,7 +277,7 @@ body { } @media (max-width: 992px) { - .template-preview-grid { + .template-preview-body { grid-template-columns: 1fr; } From c18f0b7fb18676af0110f5b0163b6f0ddd09e74c Mon Sep 17 00:00:00 2001 From: Anatolii Grynchuk Date: Wed, 13 May 2026 02:11:59 +0300 Subject: [PATCH 2/2] refactor: update admin CSS for improved layout and readability - Switch `.template-preview-variables` to grid layout for better responsiveness. - Adjust form label font size and spacing for consistency. - Enhance styles of `.form-control-sm` for improved usability. --- .../wwwroot/css/admin.css | 20 +++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/HrynCo.NotificationService.Web/wwwroot/css/admin.css b/HrynCo.NotificationService.Web/wwwroot/css/admin.css index eb8a068..f02c088 100644 --- a/HrynCo.NotificationService.Web/wwwroot/css/admin.css +++ b/HrynCo.NotificationService.Web/wwwroot/css/admin.css @@ -235,14 +235,26 @@ body { } .template-preview-variables { - display: flex; - flex-direction: column; - gap: .75rem; - max-width: 420px; + display: grid; + grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); + gap: .75rem 1rem; + max-width: none; +} + +.template-preview-variables > div { + margin-bottom: 0 !important; +} + +.template-preview-variables .form-label { + font-size: .72rem; + margin-bottom: .2rem; } .template-preview-variables .form-control-sm { background: #fff; + min-height: calc(1.5em + .45rem + 2px); + padding: .2rem .45rem; + font-size: .82rem; } .template-preview-subject {