From 5a6c57baded5ce645a4090d1ddeffecfd14ed97d Mon Sep 17 00:00:00 2001 From: David Headrick Date: Sun, 24 Aug 2025 08:04:53 -0500 Subject: [PATCH] Add unsubscribe list management functionality This commit introduces comprehensive functionality for managing unsubscribe lists within the application. Key changes include: - Creation of new DTOs, services, repositories, and controllers for unsubscribe list operations. - Updates to `MailingsController.cs` and `TestEmailListsController.cs` to include necessary using directives. - Registration of `IUnsubscribeListService` and `IUnsubscribeListRepository` in `Program.cs`. - Implementation of the `UnsubscribeListsController` with API endpoints for CRUD operations. - Introduction of the `UnsubscribeList` entity and its mapping to database columns. - Modifications to existing classes, including `Mailing` and `Target`, to reference unsubscribe lists. - Frontend updates to TypeScript interfaces and components for displaying and selecting unsubscribe lists. These enhancements provide a more robust email management system. --- .../Controllers/MailingsController.cs | 1 + .../Controllers/TestEmailListsController.cs | 1 + .../Controllers/UnsubscribeListsController.cs | 93 ++++++++++ Surge365.MassEmailReact.API/Program.cs | 2 + .../DTOs/MailingTemplateUpdateDto.cs | 2 +- .../DTOs/MailingUpdateDto.cs | 5 +- .../DTOs/TargetColumnUpdateDto.cs | 2 +- .../DTOs/TargetUpdateDto.cs | 3 +- .../DTOs/TestEmailListUpdateDto.cs | 4 +- .../DTOs/TestMailingDto.cs | 3 +- .../DTOs/TestTargetDto.cs | 2 +- .../DTOs/UnsubscribeListUpdateDto.cs | 13 ++ .../Interfaces/IMailingService.cs | 3 +- .../Interfaces/ITestEmailListService.cs | 3 +- .../Interfaces/IUnsubscribeListRepository.cs | 14 ++ .../Interfaces/IUnsubscribeListService.cs | 15 ++ .../Entities/Mailing.cs | 11 +- .../Entities/Target.cs | 8 +- .../Entities/UnsubscribeList.cs | 36 ++++ .../EntityMaps/EntityMapperConfiguration.cs | 1 + .../EntityMaps/MailingMap.cs | 1 + .../EntityMaps/TargetMap.cs | 1 + .../EntityMaps/UnsubscribeListMap.cs | 19 ++ .../Repositories/MailingRepository.cs | 2 + .../Repositories/TargetRepository.cs | 2 + .../Repositories/UnsubscribeListRepository.cs | 122 +++++++++++++ .../Services/MailingService.cs | 2 + .../Services/TargetService.cs | 4 +- .../Services/TestEmailListService.cs | 3 +- .../Services/UnsubscribeListService.cs | 66 +++++++ .../Surge365.MassEmailReact.Web.esproj | 1 + .../src/components/modals/MailingEdit.tsx | 162 +++++++++++++----- .../src/components/modals/TargetEdit.tsx | 28 +++ .../src/components/pages/NewMailings.tsx | 19 ++ .../src/components/pages/Targets.tsx | 19 ++ .../src/context/SetupDataContext.tsx | 60 ++++++- .../src/types/mailing.ts | 1 + .../src/types/target.ts | 1 + .../src/types/unsubscribeList.ts | 11 ++ Surge365.MassEmailReact.Web/vite.config.ts | 3 +- 40 files changed, 690 insertions(+), 59 deletions(-) create mode 100644 Surge365.MassEmailReact.API/Controllers/UnsubscribeListsController.cs create mode 100644 Surge365.MassEmailReact.Application/DTOs/UnsubscribeListUpdateDto.cs create mode 100644 Surge365.MassEmailReact.Application/Interfaces/IUnsubscribeListRepository.cs create mode 100644 Surge365.MassEmailReact.Application/Interfaces/IUnsubscribeListService.cs create mode 100644 Surge365.MassEmailReact.Domain/Entities/UnsubscribeList.cs create mode 100644 Surge365.MassEmailReact.Infrastructure/EntityMaps/UnsubscribeListMap.cs create mode 100644 Surge365.MassEmailReact.Infrastructure/Repositories/UnsubscribeListRepository.cs create mode 100644 Surge365.MassEmailReact.Infrastructure/Services/UnsubscribeListService.cs create mode 100644 Surge365.MassEmailReact.Web/src/types/unsubscribeList.ts diff --git a/Surge365.MassEmailReact.API/Controllers/MailingsController.cs b/Surge365.MassEmailReact.API/Controllers/MailingsController.cs index fe6c61e..4da2d21 100644 --- a/Surge365.MassEmailReact.API/Controllers/MailingsController.cs +++ b/Surge365.MassEmailReact.API/Controllers/MailingsController.cs @@ -2,6 +2,7 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.Data.SqlClient; using Surge365.Core.Controllers; +using Surge365.MassEmailReact.Application.DTOs; using Surge365.MassEmailReact.Application.Interfaces; using Surge365.MassEmailReact.Domain.Entities; using Surge365.MassEmailReact.Domain.Enums; diff --git a/Surge365.MassEmailReact.API/Controllers/TestEmailListsController.cs b/Surge365.MassEmailReact.API/Controllers/TestEmailListsController.cs index 4fdfcf3..b35df09 100644 --- a/Surge365.MassEmailReact.API/Controllers/TestEmailListsController.cs +++ b/Surge365.MassEmailReact.API/Controllers/TestEmailListsController.cs @@ -1,6 +1,7 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Surge365.Core.Controllers; +using Surge365.MassEmailReact.Application.DTOs; using Surge365.MassEmailReact.Application.Interfaces; using Surge365.MassEmailReact.Domain.Entities; diff --git a/Surge365.MassEmailReact.API/Controllers/UnsubscribeListsController.cs b/Surge365.MassEmailReact.API/Controllers/UnsubscribeListsController.cs new file mode 100644 index 0000000..114a00b --- /dev/null +++ b/Surge365.MassEmailReact.API/Controllers/UnsubscribeListsController.cs @@ -0,0 +1,93 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Surge365.MassEmailReact.Application.DTOs; +using Surge365.MassEmailReact.Application.Interfaces; +using System; +using System.Threading.Tasks; + +namespace Surge365.MassEmailReact.API.Controllers +{ + [ApiController] + [Route("api/[controller]")] + [Authorize] + public class UnsubscribeListsController : ControllerBase + { + private readonly IUnsubscribeListService _unsubscribeListService; + + public UnsubscribeListsController(IUnsubscribeListService unsubscribeListService) + { + _unsubscribeListService = unsubscribeListService; + } + + [HttpGet("GetByCode/{unsubscribeListCode}")] + public async Task GetByCode(string unsubscribeListCode) + { + try + { + var unsubscribeList = await _unsubscribeListService.GetByCodeAsync(unsubscribeListCode); + if (unsubscribeList == null) + return NotFound(); + + return Ok(unsubscribeList); + } + catch (Exception ex) + { + return StatusCode(500, $"An error occurred: {ex.Message}"); + } + } + + [HttpGet("GetAll")] + public async Task GetAll([FromQuery] bool activeOnly = true) + { + try + { + var unsubscribeLists = await _unsubscribeListService.GetAllAsync(activeOnly); + return Ok(unsubscribeLists); + } + catch (Exception ex) + { + return StatusCode(500, $"An error occurred: {ex.Message}"); + } + } + + [HttpPost("Create")] + public async Task Create([FromBody] UnsubscribeListUpdateDto unsubscribeListDto) + { + try + { + if (!ModelState.IsValid) + return BadRequest(ModelState); + + var success = await _unsubscribeListService.CreateAsync(unsubscribeListDto); + if (success) + return Ok(new { success = true }); + else + return BadRequest("Failed to create unsubscribe list"); + } + catch (Exception ex) + { + return StatusCode(500, $"An error occurred: {ex.Message}"); + } + } + + [HttpPut("Update")] + public async Task Update([FromBody] UnsubscribeListUpdateDto unsubscribeListDto) + { + try + { + if (!ModelState.IsValid) + return BadRequest(ModelState); + + var success = await _unsubscribeListService.UpdateAsync(unsubscribeListDto); + if (success) + return Ok(new { success = true }); + else + return BadRequest("Failed to update unsubscribe list"); + } + catch (Exception ex) + { + return StatusCode(500, $"An error occurred: {ex.Message}"); + } + } + } +} \ No newline at end of file diff --git a/Surge365.MassEmailReact.API/Program.cs b/Surge365.MassEmailReact.API/Program.cs index 05ea68f..42834b1 100644 --- a/Surge365.MassEmailReact.API/Program.cs +++ b/Surge365.MassEmailReact.API/Program.cs @@ -69,6 +69,8 @@ try builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); + builder.Services.AddScoped(); + builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); diff --git a/Surge365.MassEmailReact.Application/DTOs/MailingTemplateUpdateDto.cs b/Surge365.MassEmailReact.Application/DTOs/MailingTemplateUpdateDto.cs index d656922..0f2ad68 100644 --- a/Surge365.MassEmailReact.Application/DTOs/MailingTemplateUpdateDto.cs +++ b/Surge365.MassEmailReact.Application/DTOs/MailingTemplateUpdateDto.cs @@ -1,4 +1,4 @@ -namespace Surge365.MassEmailReact.Domain.Entities +namespace Surge365.MassEmailReact.Application.DTOs { public class MailingTemplateUpdateDto { diff --git a/Surge365.MassEmailReact.Application/DTOs/MailingUpdateDto.cs b/Surge365.MassEmailReact.Application/DTOs/MailingUpdateDto.cs index c8b8139..25ed76d 100644 --- a/Surge365.MassEmailReact.Application/DTOs/MailingUpdateDto.cs +++ b/Surge365.MassEmailReact.Application/DTOs/MailingUpdateDto.cs @@ -1,6 +1,6 @@ using System; -namespace Surge365.MassEmailReact.Domain.Entities +namespace Surge365.MassEmailReact.Application.DTOs { public class MailingUpdateDto { @@ -15,6 +15,7 @@ namespace Surge365.MassEmailReact.Domain.Entities public Guid? SessionActivityId { get; set; } public string? RecurringTypeCode { get; set; } public DateTime? RecurringStartDate { get; set; } + public string? UnsubscribeListCode { get; set; } public MailingTemplateUpdateDto Template { get; set; } = new MailingTemplateUpdateDto(); -} + } } \ No newline at end of file diff --git a/Surge365.MassEmailReact.Application/DTOs/TargetColumnUpdateDto.cs b/Surge365.MassEmailReact.Application/DTOs/TargetColumnUpdateDto.cs index a5cc210..d5133fb 100644 --- a/Surge365.MassEmailReact.Application/DTOs/TargetColumnUpdateDto.cs +++ b/Surge365.MassEmailReact.Application/DTOs/TargetColumnUpdateDto.cs @@ -4,7 +4,7 @@ using System.Linq; using System.Text; using System.Threading.Tasks; -namespace Surge365.MassEmailReact.Domain.Entities +namespace Surge365.MassEmailReact.Application.DTOs { public class TargetColumnUpdateDto { diff --git a/Surge365.MassEmailReact.Application/DTOs/TargetUpdateDto.cs b/Surge365.MassEmailReact.Application/DTOs/TargetUpdateDto.cs index 53c67bc..1ddac7f 100644 --- a/Surge365.MassEmailReact.Application/DTOs/TargetUpdateDto.cs +++ b/Surge365.MassEmailReact.Application/DTOs/TargetUpdateDto.cs @@ -4,7 +4,7 @@ using System.Linq; using System.Text; using System.Threading.Tasks; -namespace Surge365.MassEmailReact.Domain.Entities +namespace Surge365.MassEmailReact.Application.DTOs { public class TargetUpdateDto { @@ -16,6 +16,7 @@ namespace Surge365.MassEmailReact.Domain.Entities public string FilterQuery { get; set; } = ""; public bool AllowWriteBack { get; set; } = false; public bool IsActive { get; set; } = true; + public string? DefaultUnsubscribeListCode { get; set; } public List Columns { get; set; } = new List(); } } \ No newline at end of file diff --git a/Surge365.MassEmailReact.Application/DTOs/TestEmailListUpdateDto.cs b/Surge365.MassEmailReact.Application/DTOs/TestEmailListUpdateDto.cs index 4672fea..7aab0ee 100644 --- a/Surge365.MassEmailReact.Application/DTOs/TestEmailListUpdateDto.cs +++ b/Surge365.MassEmailReact.Application/DTOs/TestEmailListUpdateDto.cs @@ -1,4 +1,6 @@ -namespace Surge365.MassEmailReact.Domain.Entities +using System.Collections.Generic; + +namespace Surge365.MassEmailReact.Application.DTOs { public class TestEmailListUpdateDto { diff --git a/Surge365.MassEmailReact.Application/DTOs/TestMailingDto.cs b/Surge365.MassEmailReact.Application/DTOs/TestMailingDto.cs index ef8f56e..4844073 100644 --- a/Surge365.MassEmailReact.Application/DTOs/TestMailingDto.cs +++ b/Surge365.MassEmailReact.Application/DTOs/TestMailingDto.cs @@ -1,6 +1,7 @@ using System; +using System.Collections.Generic; -namespace Surge365.MassEmailReact.Domain.Entities +namespace Surge365.MassEmailReact.Application.DTOs { public class TestMailingDto { diff --git a/Surge365.MassEmailReact.Application/DTOs/TestTargetDto.cs b/Surge365.MassEmailReact.Application/DTOs/TestTargetDto.cs index c69d770..101d934 100644 --- a/Surge365.MassEmailReact.Application/DTOs/TestTargetDto.cs +++ b/Surge365.MassEmailReact.Application/DTOs/TestTargetDto.cs @@ -4,7 +4,7 @@ using System.Linq; using System.Text; using System.Threading.Tasks; -namespace Surge365.MassEmailReact.Domain.Entities +namespace Surge365.MassEmailReact.Application.DTOs { public class TestTargetDto { diff --git a/Surge365.MassEmailReact.Application/DTOs/UnsubscribeListUpdateDto.cs b/Surge365.MassEmailReact.Application/DTOs/UnsubscribeListUpdateDto.cs new file mode 100644 index 0000000..eb6b793 --- /dev/null +++ b/Surge365.MassEmailReact.Application/DTOs/UnsubscribeListUpdateDto.cs @@ -0,0 +1,13 @@ +using System; + +namespace Surge365.MassEmailReact.Application.DTOs +{ + public class UnsubscribeListUpdateDto + { + public string UnsubscribeListCode { get; set; } = ""; + public string FriendlyName { get; set; } = ""; + public string? FriendlyDescription { get; set; } + public bool IsActive { get; set; } = true; + public short DisplayOrder { get; set; } = 0; + } +} \ No newline at end of file diff --git a/Surge365.MassEmailReact.Application/Interfaces/IMailingService.cs b/Surge365.MassEmailReact.Application/Interfaces/IMailingService.cs index 8466d3f..02a92e3 100644 --- a/Surge365.MassEmailReact.Application/Interfaces/IMailingService.cs +++ b/Surge365.MassEmailReact.Application/Interfaces/IMailingService.cs @@ -1,4 +1,5 @@ -using Surge365.MassEmailReact.Domain.Entities; +using Surge365.MassEmailReact.Application.DTOs; +using Surge365.MassEmailReact.Domain.Entities; using System.Collections.Generic; using System.Threading.Tasks; diff --git a/Surge365.MassEmailReact.Application/Interfaces/ITestEmailListService.cs b/Surge365.MassEmailReact.Application/Interfaces/ITestEmailListService.cs index 449e169..074b56a 100644 --- a/Surge365.MassEmailReact.Application/Interfaces/ITestEmailListService.cs +++ b/Surge365.MassEmailReact.Application/Interfaces/ITestEmailListService.cs @@ -1,4 +1,5 @@ -using Surge365.MassEmailReact.Domain.Entities; +using Surge365.MassEmailReact.Application.DTOs; +using Surge365.MassEmailReact.Domain.Entities; using System.Collections.Generic; using System.Threading.Tasks; diff --git a/Surge365.MassEmailReact.Application/Interfaces/IUnsubscribeListRepository.cs b/Surge365.MassEmailReact.Application/Interfaces/IUnsubscribeListRepository.cs new file mode 100644 index 0000000..6908186 --- /dev/null +++ b/Surge365.MassEmailReact.Application/Interfaces/IUnsubscribeListRepository.cs @@ -0,0 +1,14 @@ +using Surge365.MassEmailReact.Domain.Entities; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Surge365.MassEmailReact.Application.Interfaces +{ + public interface IUnsubscribeListRepository + { + Task GetByCodeAsync(string unsubscribeListCode); + Task> GetAllAsync(bool activeOnly = true); + Task CreateAsync(UnsubscribeList unsubscribeList); + Task UpdateAsync(UnsubscribeList unsubscribeList); + } +} \ No newline at end of file diff --git a/Surge365.MassEmailReact.Application/Interfaces/IUnsubscribeListService.cs b/Surge365.MassEmailReact.Application/Interfaces/IUnsubscribeListService.cs new file mode 100644 index 0000000..717f459 --- /dev/null +++ b/Surge365.MassEmailReact.Application/Interfaces/IUnsubscribeListService.cs @@ -0,0 +1,15 @@ +using Surge365.MassEmailReact.Application.DTOs; +using Surge365.MassEmailReact.Domain.Entities; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Surge365.MassEmailReact.Application.Interfaces +{ + public interface IUnsubscribeListService + { + Task GetByCodeAsync(string unsubscribeListCode); + Task> GetAllAsync(bool activeOnly = true); + Task CreateAsync(UnsubscribeListUpdateDto unsubscribeListDto); + Task UpdateAsync(UnsubscribeListUpdateDto unsubscribeListDto); + } +} \ No newline at end of file diff --git a/Surge365.MassEmailReact.Domain/Entities/Mailing.cs b/Surge365.MassEmailReact.Domain/Entities/Mailing.cs index dd480d3..0e954c2 100644 --- a/Surge365.MassEmailReact.Domain/Entities/Mailing.cs +++ b/Surge365.MassEmailReact.Domain/Entities/Mailing.cs @@ -16,13 +16,15 @@ namespace Surge365.MassEmailReact.Domain.Entities public DateTime UpdateDate { get; set; } = DateTime.Now; public string? RecurringTypeCode { get; set; } public DateTime? RecurringStartDate { get; set; } + public string? UnsubscribeListCode { get; set; } public MailingTemplate Template { get; set; } = new MailingTemplate(); public Mailing() { } private Mailing(int id, string name, string description, int templateId, int targetId, string statusCode, DateTime? scheduleDate, DateTime? sentDate, DateTime createDate, - DateTime updateDate, string? recurringTypeCode, DateTime? recurringStartDate) + DateTime updateDate, string? recurringTypeCode, DateTime? recurringStartDate, + string? unsubscribeListCode) { Id = id; Name = name; @@ -36,14 +38,17 @@ namespace Surge365.MassEmailReact.Domain.Entities UpdateDate = updateDate; RecurringTypeCode = recurringTypeCode; RecurringStartDate = recurringStartDate; + UnsubscribeListCode = unsubscribeListCode; } public static Mailing Create(int id, string name, string description, int templateId, int targetId, string statusCode, DateTime? scheduleDate, DateTime? sentDate, DateTime createDate, - DateTime updateDate, string? recurringTypeCode, DateTime? recurringStartDate) + DateTime updateDate, string? recurringTypeCode, DateTime? recurringStartDate, + string? unsubscribeListCode) { return new Mailing(id, name, description, templateId, targetId, statusCode, scheduleDate, - sentDate, createDate, updateDate, recurringTypeCode, recurringStartDate); + sentDate, createDate, updateDate, recurringTypeCode, recurringStartDate, + unsubscribeListCode); } } } \ No newline at end of file diff --git a/Surge365.MassEmailReact.Domain/Entities/Target.cs b/Surge365.MassEmailReact.Domain/Entities/Target.cs index df14df2..c1c234d 100644 --- a/Surge365.MassEmailReact.Domain/Entities/Target.cs +++ b/Surge365.MassEmailReact.Domain/Entities/Target.cs @@ -16,10 +16,11 @@ namespace Surge365.MassEmailReact.Domain.Entities public string FilterQuery { get; set; } = ""; public bool AllowWriteBack { get; set; } public bool IsActive { get; set; } + public string? DefaultUnsubscribeListCode { get; set; } public List Columns { get; set; } = new List(); public Target() { } - private Target(int id, int serverId, string name, string databaseName, string viewName, string filterQuery, bool allowWriteBack, bool isActive) + private Target(int id, int serverId, string name, string databaseName, string viewName, string filterQuery, bool allowWriteBack, bool isActive, string? defaultUnsubscribeListCode) { Id = id; ServerId = ServerId; @@ -29,10 +30,11 @@ namespace Surge365.MassEmailReact.Domain.Entities FilterQuery = filterQuery; AllowWriteBack = allowWriteBack; IsActive = isActive; + DefaultUnsubscribeListCode = defaultUnsubscribeListCode; } - public static Target Create(int id, int serverId, string name, string databaseName, string viewName, string filterQuery, bool allowWriteBack, bool isActive) + public static Target Create(int id, int serverId, string name, string databaseName, string viewName, string filterQuery, bool allowWriteBack, bool isActive, string? defaultUnsubscribeListCode) { - return new Target(id, serverId, name, databaseName, viewName, filterQuery, allowWriteBack, isActive); + return new Target(id, serverId, name, databaseName, viewName, filterQuery, allowWriteBack, isActive, defaultUnsubscribeListCode); } } } diff --git a/Surge365.MassEmailReact.Domain/Entities/UnsubscribeList.cs b/Surge365.MassEmailReact.Domain/Entities/UnsubscribeList.cs new file mode 100644 index 0000000..802ccab --- /dev/null +++ b/Surge365.MassEmailReact.Domain/Entities/UnsubscribeList.cs @@ -0,0 +1,36 @@ +using System; + +namespace Surge365.MassEmailReact.Domain.Entities +{ + public class UnsubscribeList + { + public string UnsubscribeListCode { get; private set; } = ""; + public string FriendlyName { get; set; } = ""; + public string? FriendlyDescription { get; set; } + public bool IsActive { get; set; } = true; + public short DisplayOrder { get; set; } = 0; + public DateTime CreateDate { get; set; } = DateTime.Now; + public DateTime UpdateDate { get; set; } = DateTime.Now; + + public UnsubscribeList() { } + + private UnsubscribeList(string unsubscribeListCode, string friendlyName, string? friendlyDescription, + bool isActive, short displayOrder, DateTime createDate, DateTime updateDate) + { + UnsubscribeListCode = unsubscribeListCode; + FriendlyName = friendlyName; + FriendlyDescription = friendlyDescription; + IsActive = isActive; + DisplayOrder = displayOrder; + CreateDate = createDate; + UpdateDate = updateDate; + } + + public static UnsubscribeList Create(string unsubscribeListCode, string friendlyName, string? friendlyDescription, + bool isActive, short displayOrder, DateTime createDate, DateTime updateDate) + { + return new UnsubscribeList(unsubscribeListCode, friendlyName, friendlyDescription, isActive, + displayOrder, createDate, updateDate); + } + } +} \ No newline at end of file diff --git a/Surge365.MassEmailReact.Infrastructure/EntityMaps/EntityMapperConfiguration.cs b/Surge365.MassEmailReact.Infrastructure/EntityMaps/EntityMapperConfiguration.cs index 4026416..b2c5724 100644 --- a/Surge365.MassEmailReact.Infrastructure/EntityMaps/EntityMapperConfiguration.cs +++ b/Surge365.MassEmailReact.Infrastructure/EntityMaps/EntityMapperConfiguration.cs @@ -12,6 +12,7 @@ namespace Surge365.MassEmailReact.Infrastructure.EntityMaps QueryMapper.AddMap(new TestEmailListMap()); QueryMapper.AddMap(new BouncedEmailMap()); QueryMapper .AddMap(new UnsubscribeUrlMap()); + QueryMapper.AddMap(new UnsubscribeListMap()); QueryMapper.AddMap(new TemplateMap()); QueryMapper.AddMap(new EmailDomainMap()); QueryMapper .AddMap(new MailingMap()); diff --git a/Surge365.MassEmailReact.Infrastructure/EntityMaps/MailingMap.cs b/Surge365.MassEmailReact.Infrastructure/EntityMaps/MailingMap.cs index a1915f1..d684156 100644 --- a/Surge365.MassEmailReact.Infrastructure/EntityMaps/MailingMap.cs +++ b/Surge365.MassEmailReact.Infrastructure/EntityMaps/MailingMap.cs @@ -19,6 +19,7 @@ namespace Surge365.MassEmailReact.Infrastructure.EntityMaps Map(m => m.UpdateDate).ToColumn("update_date"); Map(m => m.RecurringTypeCode).ToColumn("blast_recurring_type_code"); Map(m => m.RecurringStartDate).ToColumn("recurring_start_date"); + Map(m => m.UnsubscribeListCode).ToColumn("unsubscribe_list_code"); } } } \ No newline at end of file diff --git a/Surge365.MassEmailReact.Infrastructure/EntityMaps/TargetMap.cs b/Surge365.MassEmailReact.Infrastructure/EntityMaps/TargetMap.cs index 5907bf2..db39374 100644 --- a/Surge365.MassEmailReact.Infrastructure/EntityMaps/TargetMap.cs +++ b/Surge365.MassEmailReact.Infrastructure/EntityMaps/TargetMap.cs @@ -20,6 +20,7 @@ namespace Surge365.MassEmailReact.Infrastructure.EntityMaps Map(t => t.FilterQuery).ToColumn("filter_query"); Map(t => t.AllowWriteBack).ToColumn("allow_write_back"); Map(t => t.IsActive).ToColumn("is_active"); + Map(t => t.DefaultUnsubscribeListCode).ToColumn("default_unsubscribe_list_code"); } } } diff --git a/Surge365.MassEmailReact.Infrastructure/EntityMaps/UnsubscribeListMap.cs b/Surge365.MassEmailReact.Infrastructure/EntityMaps/UnsubscribeListMap.cs new file mode 100644 index 0000000..7d88c95 --- /dev/null +++ b/Surge365.MassEmailReact.Infrastructure/EntityMaps/UnsubscribeListMap.cs @@ -0,0 +1,19 @@ +using Surge365.Core.Mapping; +using Surge365.MassEmailReact.Domain.Entities; + +namespace Surge365.MassEmailReact.Infrastructure.EntityMaps +{ + public class UnsubscribeListMap : EntityMap + { + public UnsubscribeListMap() + { + Map(u => u.UnsubscribeListCode).ToColumn("unsubscribe_list_code"); + Map(u => u.FriendlyName).ToColumn("friendly_name"); + Map(u => u.FriendlyDescription).ToColumn("friendly_description"); + Map(u => u.IsActive).ToColumn("is_active"); + Map(u => u.DisplayOrder).ToColumn("display_order"); + Map(u => u.CreateDate).ToColumn("create_date"); + Map(u => u.UpdateDate).ToColumn("update_date"); + } + } +} \ No newline at end of file diff --git a/Surge365.MassEmailReact.Infrastructure/Repositories/MailingRepository.cs b/Surge365.MassEmailReact.Infrastructure/Repositories/MailingRepository.cs index 98f3d59..f8e51dd 100644 --- a/Surge365.MassEmailReact.Infrastructure/Repositories/MailingRepository.cs +++ b/Surge365.MassEmailReact.Infrastructure/Repositories/MailingRepository.cs @@ -259,6 +259,7 @@ namespace Surge365.MassEmailReact.Infrastructure.Repositories new SqlParameter("@sent_date", mailing.SentDate ?? (object)DBNull.Value), new SqlParameter("@blast_recurring_type_code", mailing.RecurringTypeCode ?? (object)DBNull.Value), new SqlParameter("@recurring_start_date", mailing.RecurringStartDate ?? (object)DBNull.Value), + new SqlParameter("@unsubscribe_list_code", mailing.UnsubscribeListCode ?? (object)DBNull.Value), new SqlParameter("@template_json", JsonSerializer.Serialize(mailing.Template, new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower })), pmSuccess }; @@ -294,6 +295,7 @@ namespace Surge365.MassEmailReact.Infrastructure.Repositories new SqlParameter("@sent_date", mailing.SentDate ?? (object)DBNull.Value), new SqlParameter("@blast_recurring_type_code", mailing.RecurringTypeCode ?? (object)DBNull.Value), new SqlParameter("@recurring_start_date", mailing.RecurringStartDate ?? (object)DBNull.Value), + new SqlParameter("@unsubscribe_list_code", mailing.UnsubscribeListCode ?? (object)DBNull.Value), new SqlParameter("@template_json", JsonSerializer.Serialize(mailing.Template, new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower })), pmSuccess }; diff --git a/Surge365.MassEmailReact.Infrastructure/Repositories/TargetRepository.cs b/Surge365.MassEmailReact.Infrastructure/Repositories/TargetRepository.cs index f98b235..b94b276 100644 --- a/Surge365.MassEmailReact.Infrastructure/Repositories/TargetRepository.cs +++ b/Surge365.MassEmailReact.Infrastructure/Repositories/TargetRepository.cs @@ -118,6 +118,7 @@ namespace Surge365.MassEmailReact.Infrastructure.Repositories new SqlParameter("@filter_query", target.FilterQuery), new SqlParameter("@allow_write_back", target.AllowWriteBack), new SqlParameter("@is_active", target.IsActive), + new SqlParameter("@default_unsubscribe_list_code", target.DefaultUnsubscribeListCode ?? (object)DBNull.Value), new SqlParameter("@column_json", target.Columns != null ? JsonSerializer.Serialize(target.Columns) : (object)DBNull.Value), pmSuccess }; @@ -152,6 +153,7 @@ namespace Surge365.MassEmailReact.Infrastructure.Repositories new SqlParameter("@filter_query", target.FilterQuery), new SqlParameter("@allow_write_back", target.AllowWriteBack), new SqlParameter("@is_active", target.IsActive), + new SqlParameter("@default_unsubscribe_list_code", target.DefaultUnsubscribeListCode ?? (object)DBNull.Value), new SqlParameter("@column_json", target.Columns != null ? JsonSerializer.Serialize(target.Columns) : (object)DBNull.Value), pmSuccess }; diff --git a/Surge365.MassEmailReact.Infrastructure/Repositories/UnsubscribeListRepository.cs b/Surge365.MassEmailReact.Infrastructure/Repositories/UnsubscribeListRepository.cs new file mode 100644 index 0000000..4c9e88b --- /dev/null +++ b/Surge365.MassEmailReact.Infrastructure/Repositories/UnsubscribeListRepository.cs @@ -0,0 +1,122 @@ +using Microsoft.Data.SqlClient; +using Microsoft.Extensions.Configuration; +using Surge365.Core.Interfaces; +using Surge365.Core.Mapping; +using Surge365.Core.Services; +using Surge365.MassEmailReact.Application.Interfaces; +using Surge365.MassEmailReact.Domain.Entities; +using System; +using System.Collections.Generic; +using System.Data; +using System.Linq; +using System.Threading.Tasks; + +namespace Surge365.MassEmailReact.Infrastructure.Repositories +{ + public class UnsubscribeListRepository : IUnsubscribeListRepository + { + private IConfiguration _config; + private DataAccessFactory _dataAccessFactory; + private IQueryMapper _queryMapper; + public DataAccess GetDataAccess(string connectionStringName = "MassEmail") => _dataAccessFactory.Get(connectionStringName) ?? throw new ArgumentNullException(nameof(_dataAccessFactory), $"DataAccess context for '{connectionStringName}' not found."); + + public UnsubscribeListRepository(IConfiguration config, DataAccessFactory dataAccessFactory, IQueryMapper queryMapper) + { + _config = config; + _dataAccessFactory = dataAccessFactory ?? throw new ArgumentNullException(nameof(dataAccessFactory), "DataAccessFactory cannot be null."); + _queryMapper = queryMapper; +#if DEBUG + if (!_queryMapper.EntityMaps.ContainsKey(typeof(UnsubscribeList))) + { + throw new InvalidOperationException("UnsubscribeList query mapping is missing. Make sure ConfigureCustomMaps() is called inside program.cs (program startup)."); + } +#endif + } + + public async Task GetByCodeAsync(string unsubscribeListCode) + { + ArgumentNullException.ThrowIfNull(_config); + + var parameters = new List + { + new SqlParameter("@unsubscribe_list_code", unsubscribeListCode) + }; + + DataAccess dataAccess = GetDataAccess(); + var dataSet = await dataAccess.CallRetrievalProcedureAsync(parameters, "mem_get_unsubscribe_list_by_code"); + + var results = await _queryMapper.MapAsync(dataSet); + return results.FirstOrDefault(); + } + + public async Task> GetAllAsync(bool activeOnly = true) + { + var parameters = new List + { + new SqlParameter("@active_only", activeOnly) + }; + + DataAccess dataAccess = GetDataAccess(); + var dataSet = await dataAccess.CallRetrievalProcedureAsync(parameters, "mem_get_unsubscribe_list_all"); + + var results = await _queryMapper.MapAsync(dataSet); + return results.ToList(); + } + + public async Task CreateAsync(UnsubscribeList unsubscribeList) + { + ArgumentNullException.ThrowIfNull(unsubscribeList); + if (string.IsNullOrWhiteSpace(unsubscribeList.UnsubscribeListCode)) + throw new Exception("UnsubscribeListCode cannot be null or empty"); + + SqlParameter pmSuccess = new SqlParameter("@success", SqlDbType.Bit) + { + Direction = ParameterDirection.Output + }; + + List parameters = new List + { + new SqlParameter("@unsubscribe_list_code", unsubscribeList.UnsubscribeListCode), + new SqlParameter("@friendly_name", unsubscribeList.FriendlyName), + new SqlParameter("@friendly_description", unsubscribeList.FriendlyDescription ?? (object)DBNull.Value), + new SqlParameter("@is_active", unsubscribeList.IsActive), + new SqlParameter("@display_order", unsubscribeList.DisplayOrder), + pmSuccess + }; + + DataAccess dataAccess = GetDataAccess(); + await dataAccess.CallActionProcedureAsync(parameters, "mem_save_unsubscribe_list"); + bool success = pmSuccess.Value != null && (bool)pmSuccess.Value; + + return success; + } + + public async Task UpdateAsync(UnsubscribeList unsubscribeList) + { + ArgumentNullException.ThrowIfNull(unsubscribeList); + if (string.IsNullOrWhiteSpace(unsubscribeList.UnsubscribeListCode)) + throw new Exception("UnsubscribeListCode cannot be null or empty"); + + SqlParameter pmSuccess = new SqlParameter("@success", SqlDbType.Bit) + { + Direction = ParameterDirection.Output + }; + + List parameters = new List + { + new SqlParameter("@unsubscribe_list_code", unsubscribeList.UnsubscribeListCode), + new SqlParameter("@friendly_name", unsubscribeList.FriendlyName), + new SqlParameter("@friendly_description", unsubscribeList.FriendlyDescription ?? (object)DBNull.Value), + new SqlParameter("@is_active", unsubscribeList.IsActive), + new SqlParameter("@display_order", unsubscribeList.DisplayOrder), + pmSuccess + }; + + DataAccess dataAccess = GetDataAccess(); + await dataAccess.CallActionProcedureAsync(parameters, "mem_save_unsubscribe_list"); + bool success = pmSuccess.Value != null && (bool)pmSuccess.Value; + + return success; + } + } +} \ No newline at end of file diff --git a/Surge365.MassEmailReact.Infrastructure/Services/MailingService.cs b/Surge365.MassEmailReact.Infrastructure/Services/MailingService.cs index 9448f6e..8fecb32 100644 --- a/Surge365.MassEmailReact.Infrastructure/Services/MailingService.cs +++ b/Surge365.MassEmailReact.Infrastructure/Services/MailingService.cs @@ -111,6 +111,7 @@ namespace Surge365.MassEmailReact.Infrastructure.Services SentDate = mailingDto.SentDate, RecurringTypeCode = mailingDto.RecurringTypeCode, RecurringStartDate = mailingDto.RecurringStartDate, + UnsubscribeListCode = mailingDto.UnsubscribeListCode, Template = new MailingTemplate { DomainId = mailingDto.Template.DomainId, @@ -139,6 +140,7 @@ namespace Surge365.MassEmailReact.Infrastructure.Services mailing.SentDate = mailingDto.SentDate; mailing.RecurringTypeCode = mailingDto.RecurringTypeCode; mailing.RecurringStartDate = mailingDto.RecurringStartDate; + mailing.UnsubscribeListCode = mailingDto.UnsubscribeListCode; mailing.Template = new MailingTemplate { DomainId = mailingDto.Template.DomainId, diff --git a/Surge365.MassEmailReact.Infrastructure/Services/TargetService.cs b/Surge365.MassEmailReact.Infrastructure/Services/TargetService.cs index 87f5625..0daadd4 100644 --- a/Surge365.MassEmailReact.Infrastructure/Services/TargetService.cs +++ b/Surge365.MassEmailReact.Infrastructure/Services/TargetService.cs @@ -9,8 +9,8 @@ using System.IdentityModel.Tokens.Jwt; using Microsoft.IdentityModel.Tokens; using Microsoft.Extensions.Configuration; using Surge365.MassEmailReact.Domain.Entities; -using System.Security.Cryptography; using Surge365.MassEmailReact.Application.DTOs; +using System.Security.Cryptography; using System.Text.RegularExpressions; using Microsoft.Data.SqlClient; @@ -53,6 +53,7 @@ namespace Surge365.MassEmailReact.Infrastructure.Services target.FilterQuery = targetDto.FilterQuery; target.AllowWriteBack = targetDto.AllowWriteBack; target.IsActive = targetDto.IsActive; + target.DefaultUnsubscribeListCode = targetDto.DefaultUnsubscribeListCode; target.Columns = new List(); foreach (var columnDto in targetDto.Columns) @@ -77,6 +78,7 @@ namespace Surge365.MassEmailReact.Infrastructure.Services target.FilterQuery = targetDto.FilterQuery; target.AllowWriteBack = targetDto.AllowWriteBack; target.IsActive = targetDto.IsActive; + target.DefaultUnsubscribeListCode = targetDto.DefaultUnsubscribeListCode; target.Columns = new List(); foreach (var columnDto in targetDto.Columns) diff --git a/Surge365.MassEmailReact.Infrastructure/Services/TestEmailListService.cs b/Surge365.MassEmailReact.Infrastructure/Services/TestEmailListService.cs index eef9fb7..8843942 100644 --- a/Surge365.MassEmailReact.Infrastructure/Services/TestEmailListService.cs +++ b/Surge365.MassEmailReact.Infrastructure/Services/TestEmailListService.cs @@ -1,4 +1,5 @@ -using Surge365.MassEmailReact.Application.Interfaces; +using Surge365.MassEmailReact.Application.DTOs; +using Surge365.MassEmailReact.Application.Interfaces; using Surge365.MassEmailReact.Domain.Entities; using System; using System.Collections.Generic; diff --git a/Surge365.MassEmailReact.Infrastructure/Services/UnsubscribeListService.cs b/Surge365.MassEmailReact.Infrastructure/Services/UnsubscribeListService.cs new file mode 100644 index 0000000..d4e03dc --- /dev/null +++ b/Surge365.MassEmailReact.Infrastructure/Services/UnsubscribeListService.cs @@ -0,0 +1,66 @@ +using Surge365.MassEmailReact.Application.Interfaces; +using Surge365.MassEmailReact.Application.DTOs; +using Surge365.MassEmailReact.Domain.Entities; +using Microsoft.Extensions.Configuration; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Surge365.MassEmailReact.Infrastructure.Services +{ + public class UnsubscribeListService : IUnsubscribeListService + { + private readonly IUnsubscribeListRepository _unsubscribeListRepository; + private readonly IConfiguration _config; + + public UnsubscribeListService(IUnsubscribeListRepository unsubscribeListRepository, IConfiguration config) + { + _unsubscribeListRepository = unsubscribeListRepository; + _config = config; + } + + public async Task GetByCodeAsync(string unsubscribeListCode) + { + return await _unsubscribeListRepository.GetByCodeAsync(unsubscribeListCode); + } + + public async Task> GetAllAsync(bool activeOnly = true) + { + return await _unsubscribeListRepository.GetAllAsync(activeOnly); + } + + public async Task CreateAsync(UnsubscribeListUpdateDto unsubscribeListDto) + { + ArgumentNullException.ThrowIfNull(unsubscribeListDto, nameof(unsubscribeListDto)); + if (string.IsNullOrWhiteSpace(unsubscribeListDto.UnsubscribeListCode)) + throw new Exception("UnsubscribeListCode cannot be null or empty"); + + var unsubscribeList = new UnsubscribeList + { + FriendlyName = unsubscribeListDto.FriendlyName, + FriendlyDescription = unsubscribeListDto.FriendlyDescription, + IsActive = unsubscribeListDto.IsActive, + DisplayOrder = unsubscribeListDto.DisplayOrder + }; + + return await _unsubscribeListRepository.CreateAsync(unsubscribeList); + } + + public async Task UpdateAsync(UnsubscribeListUpdateDto unsubscribeListDto) + { + ArgumentNullException.ThrowIfNull(unsubscribeListDto, nameof(unsubscribeListDto)); + if (string.IsNullOrWhiteSpace(unsubscribeListDto.UnsubscribeListCode)) + throw new Exception("UnsubscribeListCode cannot be null or empty"); + + var unsubscribeList = await _unsubscribeListRepository.GetByCodeAsync(unsubscribeListDto.UnsubscribeListCode); + if (unsubscribeList == null) return false; + + unsubscribeList.FriendlyName = unsubscribeListDto.FriendlyName; + unsubscribeList.FriendlyDescription = unsubscribeListDto.FriendlyDescription; + unsubscribeList.IsActive = unsubscribeListDto.IsActive; + unsubscribeList.DisplayOrder = unsubscribeListDto.DisplayOrder; + + return await _unsubscribeListRepository.UpdateAsync(unsubscribeList); + } + } +} \ No newline at end of file diff --git a/Surge365.MassEmailReact.Web/Surge365.MassEmailReact.Web.esproj b/Surge365.MassEmailReact.Web/Surge365.MassEmailReact.Web.esproj index 80232b1..b440abf 100644 --- a/Surge365.MassEmailReact.Web/Surge365.MassEmailReact.Web.esproj +++ b/Surge365.MassEmailReact.Web/Surge365.MassEmailReact.Web.esproj @@ -19,6 +19,7 @@ + \ No newline at end of file diff --git a/Surge365.MassEmailReact.Web/src/components/modals/MailingEdit.tsx b/Surge365.MassEmailReact.Web/src/components/modals/MailingEdit.tsx index a7f7e40..8b53f0e 100644 --- a/Surge365.MassEmailReact.Web/src/components/modals/MailingEdit.tsx +++ b/Surge365.MassEmailReact.Web/src/components/modals/MailingEdit.tsx @@ -23,6 +23,7 @@ import Template from "@/types/template"; import Mailing from "@/types/mailing"; import TargetSample from "@/types/targetSample"; import Target from "@/types/target"; +import UnsubscribeList from "@/types/unsubscribeList"; //import MailingTemplate from "@/types/mailingTemplate"; //import MailingTarget from "@/types/mailingTarget"; import EmailList from "@/components/forms/EmailList"; @@ -82,6 +83,7 @@ const MailingEdit = ({ open, mailing, onClose, onSave }: MailingEditProps) => { const [currentTarget, setCurrentTarget] = useState(null); const [targetSample, setTargetSample] = useState(null); const [targetSampleLoading, setTargetSampleLoading] = useState(false); + const [availableUnsubscribeLists, setAvailableUnsubscribeLists] = useState([]); const defaultMailing: Mailing = { id: 0, @@ -95,6 +97,7 @@ const MailingEdit = ({ open, mailing, onClose, onSave }: MailingEditProps) => { sessionActivityId: null, recurringTypeCode: null, recurringStartDate: null, + unsubscribeListCode: null, template: { id: 0, mailingId: 0, @@ -208,6 +211,7 @@ const MailingEdit = ({ open, mailing, onClose, onSave }: MailingEditProps) => { }) : schema.nullable(); }), + unsubscribeListCode: yup.string().nullable().optional(), template: yup.object().shape({ id: yup.number().nullable().default(0), mailingId: yup.number().default(0), @@ -254,6 +258,39 @@ const MailingEdit = ({ open, mailing, onClose, onSave }: MailingEditProps) => { const [loading, setLoading] = useState(false); + // Function to fetch unsubscribe list by code if not in active list + const fetchUnsubscribeListByCode = async (code: string): Promise => { + try { + const response = await customFetch(`/api/unsubscribeLists/GetByCode/${code}`); + if (!response.ok) return null; + return await response.json(); + } catch (error) { + console.error("Error fetching unsubscribe list by code:", error); + return null; + } + }; + + // Function to update available unsubscribe lists based on target selection + const updateAvailableUnsubscribeLists = async (selectedTarget: Target | null) => { + let lists = [...setupData.unsubscribeLists]; + + if (selectedTarget?.defaultUnsubscribeListCode) { + // Check if the target's default unsubscribe list is already in the active list + const isDefaultInActiveList = lists.some( + list => list.unsubscribeListCode === selectedTarget.defaultUnsubscribeListCode + ); + + // If not in active list, fetch it and add it + if (!isDefaultInActiveList) { + const defaultList = await fetchUnsubscribeListByCode(selectedTarget.defaultUnsubscribeListCode); + if (defaultList) { + lists = [...lists, defaultList]; + } + } + } + + setAvailableUnsubscribeLists(lists); + }; useEffect(() => { const initializeMailingEdit = async () => { @@ -270,6 +307,10 @@ const MailingEdit = ({ open, mailing, onClose, onSave }: MailingEditProps) => { setRecurring(false); setScheduleForLater(false); reset(mailing || defaultMailing, { keepDefaultValues: true }); + + // Initialize available unsubscribe lists + setAvailableUnsubscribeLists(setupData.unsubscribeLists); + if (setupData.testEmailLists.length > 0) { setTestEmailListId(setupData.testEmailLists[0].id); setEmails(setupData.testEmailLists[0].emails); @@ -286,6 +327,13 @@ const MailingEdit = ({ open, mailing, onClose, onSave }: MailingEditProps) => { if (mailing?.targetId) { const target = setupData.targets.find(t => t.id === mailing.targetId) || null; setCurrentTarget(target); + // Update available unsubscribe lists and set default if needed + await updateAvailableUnsubscribeLists(target); + + // If this is a new mailing or no unsubscribe list is set, default to target's default + if (target && (!mailing?.unsubscribeListCode || mailing.id === 0) && target.defaultUnsubscribeListCode) { + setValue("unsubscribeListCode", target.defaultUnsubscribeListCode); + } } else { setCurrentTarget(null); } @@ -293,7 +341,7 @@ const MailingEdit = ({ open, mailing, onClose, onSave }: MailingEditProps) => { }; initializeMailingEdit(); - }, [open, mailing, reset, setupData.testEmailLists, setupData.targets, setupData.templates]); + }, [open, mailing, reset, setupData.testEmailLists, setupData.targets, setupData.templates, setupData.unsubscribeLists]); const handleSave = async (formData: Mailing) => { const apiUrl = isNew ? "/api/mailings" : `/api/mailings/${formData.id}`; @@ -565,33 +613,43 @@ const MailingEdit = ({ open, mailing, onClose, onSave }: MailingEditProps) => { /> - ( - option.name} - value={filteredTargets.find(t => t.id === field.value) || null} - onChange={(_, newValue) => { - field.onChange(newValue ? newValue.id : null); - trigger("targetId"); - setCurrentTarget(newValue); - }} - renderInput={(params) => ( - - )} - sx={{ flexGrow: 1 }} - /> - )} + ( + option.name} + value={filteredTargets.find(t => t.id === field.value) || null} + onChange={async (_, newValue) => { + field.onChange(newValue ? newValue.id : null); + trigger("targetId"); + setCurrentTarget(newValue); + + // Update available unsubscribe lists when target changes + await updateAvailableUnsubscribeLists(newValue); + + // Set default unsubscribe list from target if available + if (newValue?.defaultUnsubscribeListCode) { + setValue("unsubscribeListCode", newValue.defaultUnsubscribeListCode); + } else { + setValue("unsubscribeListCode", null); + } + }} + renderInput={(params) => ( + + )} + sx={{ flexGrow: 1 }} + /> + )} /> {currentTarget && ( @@ -611,6 +669,32 @@ const MailingEdit = ({ open, mailing, onClose, onSave }: MailingEditProps) => { )} + ( + option.friendlyName} + value={availableUnsubscribeLists.find((list) => list.unsubscribeListCode === field.value) || null} + onChange={(_, newValue) => { + field.onChange(newValue ? newValue.unsubscribeListCode : null); + trigger("unsubscribeListCode"); + }} + renderInput={(params) => ( + + )} + /> + )} + /> option.name} @@ -724,24 +808,24 @@ const MailingEdit = ({ open, mailing, onClose, onSave }: MailingEditProps) => { )} - {templateViewerOpen && ( - { setTemplateViewerOpen(false) }} - /> - )} + {templateViewerOpen && ( + { setTemplateViewerOpen(false) }} + /> + )} {TargetSampleModalOpen && currentTarget && ( { setTargetSampleModalOpen(false) }} - /> - )} + onClose={() => { setTargetSampleModalOpen(false) }} + /> + )} - + diff --git a/Surge365.MassEmailReact.Web/src/components/modals/TargetEdit.tsx b/Surge365.MassEmailReact.Web/src/components/modals/TargetEdit.tsx index 3cb8046..b2c0c2b 100644 --- a/Surge365.MassEmailReact.Web/src/components/modals/TargetEdit.tsx +++ b/Surge365.MassEmailReact.Web/src/components/modals/TargetEdit.tsx @@ -51,6 +51,7 @@ const schema = yup.object().shape({ filterQuery: yup.string().nullable(), allowWriteBack: yup.boolean().default(false), isActive: yup.boolean().default(true), + defaultUnsubscribeListCode: yup.string().nullable().optional(), columns: yup .array().of( yup.object().shape({ @@ -84,6 +85,7 @@ const defaultTarget: Target = { filterQuery: "", allowWriteBack: false, isActive: true, + defaultUnsubscribeListCode: null, columns: [], }; @@ -315,6 +317,32 @@ const TargetEdit = ({ open, target, onClose, onSave }: TargetEditProps) => { error={!!errors.filterQuery} helperText={errors.filterQuery?.message} /> + ( + option.friendlyName} + value={setupData.unsubscribeLists.find((list) => list.unsubscribeListCode === field.value) || null} + onChange={(_, newValue) => { + field.onChange(newValue ? newValue.unsubscribeListCode : null); + trigger("defaultUnsubscribeListCode"); + }} + renderInput={(params) => ( + + )} + /> + )} + /> } label="Allow Write Back" diff --git a/Surge365.MassEmailReact.Web/src/components/pages/NewMailings.tsx b/Surge365.MassEmailReact.Web/src/components/pages/NewMailings.tsx index 2d17633..b91215e 100644 --- a/Surge365.MassEmailReact.Web/src/components/pages/NewMailings.tsx +++ b/Surge365.MassEmailReact.Web/src/components/pages/NewMailings.tsx @@ -53,6 +53,18 @@ function NewMailings() { minWidth: 160, valueGetter: (_: number, row: Mailing) => setupData.templates.find(t => t.id === row.templateId)?.subject || 'Unknown', }, + { + field: "unsubscribeListCode", + headerName: "Unsubscribe List", + flex: 1, + minWidth: 160, + renderCell: (params: GridRenderCellParams) => { + const unsubscribeList = setupData.unsubscribeLists.find( + list => list.unsubscribeListCode === params.row.unsubscribeListCode + ); + return unsubscribeList ? unsubscribeList.friendlyName : (params.row.unsubscribeListCode || "None"); + } + }, ]; const reloadMailings = async () => { @@ -153,6 +165,13 @@ function NewMailings() { ID: {row.id} Description: {row.description} Subject: {setupData.templates.find(t => t.id === row.templateId)?.subject || 'Unknown'} + + Unsubscribe List: { + setupData.unsubscribeLists.find( + list => list.unsubscribeListCode === row.unsubscribeListCode + )?.friendlyName || row.unsubscribeListCode || "None" + } + { e.stopPropagation(); handleEdit(row); }}> diff --git a/Surge365.MassEmailReact.Web/src/components/pages/Targets.tsx b/Surge365.MassEmailReact.Web/src/components/pages/Targets.tsx index 1602af1..4ab52e9 100644 --- a/Surge365.MassEmailReact.Web/src/components/pages/Targets.tsx +++ b/Surge365.MassEmailReact.Web/src/components/pages/Targets.tsx @@ -38,6 +38,18 @@ function Targets() { { field: "databaseName", headerName: "Database", flex: 1, minWidth: 100 }, { field: "viewName", headerName: "View", flex: 1, minWidth: 300 }, { field: "filterQuery", headerName: "Filter Query", flex: 1, minWidth: 100 }, + { + field: "defaultUnsubscribeListCode", + headerName: "Default Unsubscribe List", + flex: 1, + minWidth: 180, + renderCell: (params: GridRenderCellParams) => { + const unsubscribeList = setupData.unsubscribeLists.find( + list => list.unsubscribeListCode === params.row.defaultUnsubscribeListCode + ); + return unsubscribeList ? unsubscribeList.friendlyName : (params.row.defaultUnsubscribeListCode || "None"); + } + }, { field: "allowWriteBack", headerName: "Write Back", width: 150 }, { field: "isActive", headerName: "Active", width: 115 }, ]; @@ -102,6 +114,13 @@ function Targets() { Database: {row.databaseName} View: {row.viewName} Filter: {row.filterQuery} + + Default Unsubscribe List: { + setupData.unsubscribeLists.find( + list => list.unsubscribeListCode === row.defaultUnsubscribeListCode + )?.friendlyName || row.defaultUnsubscribeListCode || "None" + } + Writeback: {row.allowWriteBack ? "Yes" : "No"} Active: {row.isActive ? "Yes" : "No"} diff --git a/Surge365.MassEmailReact.Web/src/context/SetupDataContext.tsx b/Surge365.MassEmailReact.Web/src/context/SetupDataContext.tsx index 76964b0..edc331c 100644 --- a/Surge365.MassEmailReact.Web/src/context/SetupDataContext.tsx +++ b/Surge365.MassEmailReact.Web/src/context/SetupDataContext.tsx @@ -4,6 +4,7 @@ import Server from "@/types/server"; import TestEmailList from '@/types/testEmailList'; import BouncedEmail from '@/types/bouncedEmail'; import UnsubscribeUrl from '@/types/unsubscribeUrl'; +import UnsubscribeList from '@/types/unsubscribeList'; import Template from '@/types/template'; import EmailDomain from '@/types/emailDomain'; import { useCustomFetchNoNavigate } from "@/utils/customFetch"; @@ -36,6 +37,11 @@ export type SetupData = { setUnsubscribeUrls: (updatedUnsubscribeUrl: UnsubscribeUrl) => void; unsubscribeUrlsLoading: boolean; + unsubscribeLists: UnsubscribeList[]; + reloadUnsubscribeLists: () => void; + setUnsubscribeLists: (updatedUnsubscribeList: UnsubscribeList) => void; + unsubscribeListsLoading: boolean; + templates: Template[]; reloadTemplates: () => void; setTemplates: (updatedTemplate: Template) => void; @@ -71,6 +77,9 @@ export const SetupDataProvider = ({ children }: { children: React.ReactNode }) = const [unsubscribeUrls, setUnsubscribeUrls] = useState([]); const [unsubscribeUrlsLoading, setUnsubscribeUrlsLoading] = useState(false); + const [unsubscribeLists, setUnsubscribeLists] = useState([]); + const [unsubscribeListsLoading, setUnsubscribeListsLoading] = useState(false); + const [templates, setTemplates] = useState([]); const [templatesLoading, setTemplatesLoading] = useState(false); @@ -109,6 +118,10 @@ export const SetupDataProvider = ({ children }: { children: React.ReactNode }) = let unsubscribeUrlsData: UnsubscribeUrl[] | null = null; let loadUnsubscribeUrls = true; + setUnsubscribeListsLoading(true); + let unsubscribeListsData: UnsubscribeList[] | null = null; + let loadUnsubscribeLists = true; + setTemplatesLoading(true); let templatesData: Template[] | null = null; let loadTemplates = true; @@ -144,6 +157,11 @@ export const SetupDataProvider = ({ children }: { children: React.ReactNode }) = setUnsubscribeUrls(parsedData.unsubscribeUrls); setUnsubscribeUrlsLoading(false); } + if (parsedData.unsubscribeLists) { + loadUnsubscribeLists = false; + setUnsubscribeLists(parsedData.unsubscribeLists); + setUnsubscribeListsLoading(false); + } if (parsedData.templates) { loadTemplates = false; setTemplates(parsedData.templates); @@ -220,6 +238,19 @@ export const SetupDataProvider = ({ children }: { children: React.ReactNode }) = } } + if (loadUnsubscribeLists) { + const unsubscribeListsResponse = await customFetch("/api/unsubscribeLists/GetAll?activeOnly=true"); + unsubscribeListsData = await unsubscribeListsResponse.json(); + if (unsubscribeListsData) { + setUnsubscribeLists(unsubscribeListsData); + setUnsubscribeListsLoading(false); + } + else { + console.error("Failed to fetch unsubscribeLists"); + setUnsubscribeListsLoading(false); + } + } + if (loadTemplates) { const templatesResponse = await customFetch("/api/templates/GetAll?activeOnly=false"); templatesData = await templatesResponse.json(); @@ -247,13 +278,24 @@ export const SetupDataProvider = ({ children }: { children: React.ReactNode }) = } setDataLoading(false); - sessionStorage.setItem("setupData", JSON.stringify({ targets: targetsData, servers: serversData, testEmailLists: testEmailListsData, bouncedEmails: bouncedEmailsData, unsubscribeUrls: unsubscribeUrlsData, templates: templatesData, emailDomains: emailDomainsData })); + sessionStorage.setItem("setupData", JSON.stringify({ + targets: targetsData, + servers: serversData, + testEmailLists: testEmailListsData, + bouncedEmails: bouncedEmailsData, + unsubscribeUrls: unsubscribeUrlsData, + unsubscribeLists: unsubscribeListsData, + templates: templatesData, + emailDomains: emailDomainsData + })); } catch (error) { setDataLoading(false); setTargetsLoading(false); setServersLoading(false); setTestEmailListsLoading(false); setBouncedEmailsLoading(false); + setUnsubscribeUrlsLoading(false); + setUnsubscribeListsLoading(false); setTemplatesLoading(false); setEmailDomainsLoading(false); console.error("Failed to fetch setup data:", error); @@ -322,6 +364,17 @@ export const SetupDataProvider = ({ children }: { children: React.ReactNode }) = sessionStorage.setItem("setupData", JSON.stringify({ unsubscribeUrls, targets, testEmailLists, bouncedEmails })); }; + const updateUnsubscribeListCache = (updatedUnsubscribeList: UnsubscribeList) => { + setUnsubscribeLists((prevUnsubscribeLists) => { + const unsubscribeListExists = prevUnsubscribeLists.some((unsubscribeList) => unsubscribeList.unsubscribeListCode === updatedUnsubscribeList.unsubscribeListCode); + return unsubscribeListExists + ? prevUnsubscribeLists.map((unsubscribeList) => (unsubscribeList.unsubscribeListCode === updatedUnsubscribeList.unsubscribeListCode ? updatedUnsubscribeList : unsubscribeList)) + : [...prevUnsubscribeLists, updatedUnsubscribeList]; // Push new unsubscribeList if not found + }); + + sessionStorage.setItem("setupData", JSON.stringify({ unsubscribeLists, targets, testEmailLists, bouncedEmails })); + }; + const updateTemplateCache = (updatedTemplate: Template) => { setTemplates((prevTemplates) => { const templateExists = prevTemplates.some((template) => template.id === updatedTemplate.id); @@ -376,6 +429,11 @@ export const SetupDataProvider = ({ children }: { children: React.ReactNode }) = setUnsubscribeUrls: updateUnsubscribeUrlCache, unsubscribeUrlsLoading, + unsubscribeLists, + reloadUnsubscribeLists: reloadSetupData, + setUnsubscribeLists: updateUnsubscribeListCache, + unsubscribeListsLoading, + templates, reloadTemplates: reloadSetupData, setTemplates: updateTemplateCache, diff --git a/Surge365.MassEmailReact.Web/src/types/mailing.ts b/Surge365.MassEmailReact.Web/src/types/mailing.ts index 2282aef..503b527 100644 --- a/Surge365.MassEmailReact.Web/src/types/mailing.ts +++ b/Surge365.MassEmailReact.Web/src/types/mailing.ts @@ -12,6 +12,7 @@ export interface Mailing { sessionActivityId: string | null; recurringTypeCode: string | null; recurringStartDate: string | null; + unsubscribeListCode: string | null; template: MailingTemplate | null; target: MailingTarget | null; } diff --git a/Surge365.MassEmailReact.Web/src/types/target.ts b/Surge365.MassEmailReact.Web/src/types/target.ts index de1e852..cd23fc4 100644 --- a/Surge365.MassEmailReact.Web/src/types/target.ts +++ b/Surge365.MassEmailReact.Web/src/types/target.ts @@ -8,6 +8,7 @@ export interface Target { filterQuery: string; allowWriteBack: boolean; isActive: boolean; + defaultUnsubscribeListCode: string | null; columns: TargetColumn[]; } diff --git a/Surge365.MassEmailReact.Web/src/types/unsubscribeList.ts b/Surge365.MassEmailReact.Web/src/types/unsubscribeList.ts new file mode 100644 index 0000000..2105771 --- /dev/null +++ b/Surge365.MassEmailReact.Web/src/types/unsubscribeList.ts @@ -0,0 +1,11 @@ +export interface UnsubscribeList { + unsubscribeListCode: string; + friendlyName: string; + friendlyDescription: string | null; + isActive: boolean; + displayOrder: number; + createDate: string; + updateDate: string; +} + +export default UnsubscribeList; \ No newline at end of file diff --git a/Surge365.MassEmailReact.Web/vite.config.ts b/Surge365.MassEmailReact.Web/vite.config.ts index 5e16108..88a98a6 100644 --- a/Surge365.MassEmailReact.Web/vite.config.ts +++ b/Surge365.MassEmailReact.Web/vite.config.ts @@ -56,6 +56,7 @@ export default defineConfig({ https: { key: fs.readFileSync(keyFilePath), cert: fs.readFileSync(certFilePath), - } + }, + open: 'chrome' // This will try to open Chrome specifically } })