From 51c267e48fbe0f144cda610eee1089eea893b27a Mon Sep 17 00:00:00 2001 From: David Headrick Date: Mon, 14 Apr 2025 16:09:07 -0500 Subject: [PATCH] Add API endpoints for emails and templates retrieval - Introduced new endpoints in `MailingsController.cs` for fetching emails and templates by mailing ID. - Updated `MailingUpdateDto` to include `MailingTemplateUpdateDto`. - Extended `IMailingRepository` and `IMailingService` with methods for emails, templates, and targets. - Modified `Mailing` class to include a `MailingTemplate` property; removed `SessionActivityId`. - Implemented new methods in `MailingRepository` with adjusted SQL queries. - Created new entity classes: `MailingEmail`, `MailingTarget`, and `MailingTemplate`. - Added mapping configurations for new entities in Dapper maps. - Updated `MailingEdit.tsx` and `MailingView.tsx` components for new structures and improved UI handling. - Adjusted `Mailing` interface and created TypeScript interfaces for new entities. --- .../Controllers/MailingsController.cs | 13 + .../DTOs/MailingTemplateUpdateDto.cs | 9 + .../DTOs/MailingUpdateDto.cs | 3 +- .../Interfaces/IMailingRepository.cs | 3 + .../Interfaces/IMailingService.cs | 3 + .../Entities/Mailing.cs | 9 +- .../Entities/MailingEmail.cs | 40 +++ .../Entities/MailingTarget.cs | 39 +++ .../Entities/MailingTemplate.cs | 54 ++++ .../DapperMaps/DapperConfiguration.cs | 3 + .../DapperMaps/MailingEmailMap.cs | 20 ++ .../DapperMaps/MailingMap.cs | 1 - .../DapperMaps/MailingTargetMap.cs | 25 ++ .../DapperMaps/MailingTemplateMap.cs | 27 ++ .../Repositories/MailingRepository.cs | 90 ++++++- .../Services/MailingService.cs | 28 +- .../src/components/modals/MailingEdit.tsx | 152 ++++++++++- .../src/components/modals/MailingView.tsx | 248 ++++++++++++++---- .../src/types/mailing.ts | 4 + .../src/types/mailingTarget.ts | 14 + .../src/types/mailingTemplate.ts | 18 ++ 21 files changed, 732 insertions(+), 71 deletions(-) create mode 100644 Surge365.MassEmailReact.Application/DTOs/MailingTemplateUpdateDto.cs create mode 100644 Surge365.MassEmailReact.Domain/Entities/MailingEmail.cs create mode 100644 Surge365.MassEmailReact.Domain/Entities/MailingTarget.cs create mode 100644 Surge365.MassEmailReact.Domain/Entities/MailingTemplate.cs create mode 100644 Surge365.MassEmailReact.Infrastructure/DapperMaps/MailingEmailMap.cs create mode 100644 Surge365.MassEmailReact.Infrastructure/DapperMaps/MailingTargetMap.cs create mode 100644 Surge365.MassEmailReact.Infrastructure/DapperMaps/MailingTemplateMap.cs create mode 100644 Surge365.MassEmailReact.Web/src/types/mailingTarget.ts create mode 100644 Surge365.MassEmailReact.Web/src/types/mailingTemplate.ts diff --git a/Surge365.MassEmailReact.API/Controllers/MailingsController.cs b/Surge365.MassEmailReact.API/Controllers/MailingsController.cs index 9e76a09..26eafdf 100644 --- a/Surge365.MassEmailReact.API/Controllers/MailingsController.cs +++ b/Surge365.MassEmailReact.API/Controllers/MailingsController.cs @@ -67,6 +67,19 @@ namespace Surge365.MassEmailReact.API.Controllers return mailing is not null ? Ok(mailing) : NotFound($"Mailing statistics with id '{id}' not found."); } + [HttpGet("{id}/emails")] + public async Task GetEmailsById(int id) + { + var emails = await _mailingService.GetEmailsByIdAsync(id); + return emails is not null ? Ok(emails) : NotFound($"Mailing emails with id '{id}' not found."); + } + [HttpGet("{id}/template")] + public async Task GetTemplateById(int id) + { + var template = await _mailingService.GetTemplateByIdAsync(id); + return template is not null ? Ok(template) : NotFound($"Mailing template with id '{id}' not found."); + } + [HttpPost] public async Task CreateMailing([FromBody] MailingUpdateDto mailingUpdateDto) { diff --git a/Surge365.MassEmailReact.Application/DTOs/MailingTemplateUpdateDto.cs b/Surge365.MassEmailReact.Application/DTOs/MailingTemplateUpdateDto.cs new file mode 100644 index 0000000..d656922 --- /dev/null +++ b/Surge365.MassEmailReact.Application/DTOs/MailingTemplateUpdateDto.cs @@ -0,0 +1,9 @@ +namespace Surge365.MassEmailReact.Domain.Entities +{ + public class MailingTemplateUpdateDto + { + public int DomainId { get; set; } + public string Subject { get; set; } = ""; + public string FromName { get; set; } = ""; + } +} \ No newline at end of file diff --git a/Surge365.MassEmailReact.Application/DTOs/MailingUpdateDto.cs b/Surge365.MassEmailReact.Application/DTOs/MailingUpdateDto.cs index 6ab7804..c8b8139 100644 --- a/Surge365.MassEmailReact.Application/DTOs/MailingUpdateDto.cs +++ b/Surge365.MassEmailReact.Application/DTOs/MailingUpdateDto.cs @@ -15,5 +15,6 @@ namespace Surge365.MassEmailReact.Domain.Entities public Guid? SessionActivityId { get; set; } public string? RecurringTypeCode { get; set; } public DateTime? RecurringStartDate { get; set; } - } + public MailingTemplateUpdateDto Template { get; set; } = new MailingTemplateUpdateDto(); +} } \ No newline at end of file diff --git a/Surge365.MassEmailReact.Application/Interfaces/IMailingRepository.cs b/Surge365.MassEmailReact.Application/Interfaces/IMailingRepository.cs index ca6ccc0..9285f91 100644 --- a/Surge365.MassEmailReact.Application/Interfaces/IMailingRepository.cs +++ b/Surge365.MassEmailReact.Application/Interfaces/IMailingRepository.cs @@ -11,6 +11,9 @@ namespace Surge365.MassEmailReact.Application.Interfaces Task> GetByStatusAsync(string codes, string? startDate, string? endDate); Task> GetStatisticsByStatusAsync(string codes, string? startDate, string? endDate); Task GetStatisticByIdAsync(int id); + Task> GetEmailsByIdAsync(int id); + Task GetTemplateByIdAsync(int id); + Task GetTargetByIdAsync(int id); Task NameIsAvailableAsync(int? id, string name); Task GetNextAvailableNameAsync(int? id, string name); Task CreateAsync(Mailing mailing); diff --git a/Surge365.MassEmailReact.Application/Interfaces/IMailingService.cs b/Surge365.MassEmailReact.Application/Interfaces/IMailingService.cs index 8b60e40..8466d3f 100644 --- a/Surge365.MassEmailReact.Application/Interfaces/IMailingService.cs +++ b/Surge365.MassEmailReact.Application/Interfaces/IMailingService.cs @@ -12,6 +12,9 @@ namespace Surge365.MassEmailReact.Application.Interfaces Task> GetByStatusAsync(string codes, string? startDate, string? endDate); Task> GetStatisticsByStatusAsync(string codes, string? startDate, string? endDate); Task GetStatisticByIdAsync(int id); + Task> GetEmailsByIdAsync(int id); + Task GetTemplateByIdAsync(int id); + Task GetTargetByIdAsync(int id); Task NameIsAvailableAsync(int? id, string name); Task GetNextAvailableNameAsync(int? id, string name); diff --git a/Surge365.MassEmailReact.Domain/Entities/Mailing.cs b/Surge365.MassEmailReact.Domain/Entities/Mailing.cs index a92bb2a..dd480d3 100644 --- a/Surge365.MassEmailReact.Domain/Entities/Mailing.cs +++ b/Surge365.MassEmailReact.Domain/Entities/Mailing.cs @@ -14,15 +14,15 @@ namespace Surge365.MassEmailReact.Domain.Entities public DateTime? SentDate { get; set; } public DateTime CreateDate { get; set; } = DateTime.Now; public DateTime UpdateDate { get; set; } = DateTime.Now; - public Guid? SessionActivityId { get; set; } public string? RecurringTypeCode { get; set; } public DateTime? RecurringStartDate { 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, Guid? sessionActivityId, string? recurringTypeCode, DateTime? recurringStartDate) + DateTime updateDate, string? recurringTypeCode, DateTime? recurringStartDate) { Id = id; Name = name; @@ -34,17 +34,16 @@ namespace Surge365.MassEmailReact.Domain.Entities SentDate = sentDate; CreateDate = createDate; UpdateDate = updateDate; - SessionActivityId = sessionActivityId; RecurringTypeCode = recurringTypeCode; RecurringStartDate = recurringStartDate; } public static Mailing Create(int id, string name, string description, int templateId, int targetId, string statusCode, DateTime? scheduleDate, DateTime? sentDate, DateTime createDate, - DateTime updateDate, Guid? sessionActivityId, string? recurringTypeCode, DateTime? recurringStartDate) + DateTime updateDate, string? recurringTypeCode, DateTime? recurringStartDate) { return new Mailing(id, name, description, templateId, targetId, statusCode, scheduleDate, - sentDate, createDate, updateDate, sessionActivityId, recurringTypeCode, recurringStartDate); + sentDate, createDate, updateDate, recurringTypeCode, recurringStartDate); } } } \ No newline at end of file diff --git a/Surge365.MassEmailReact.Domain/Entities/MailingEmail.cs b/Surge365.MassEmailReact.Domain/Entities/MailingEmail.cs new file mode 100644 index 0000000..eb9c064 --- /dev/null +++ b/Surge365.MassEmailReact.Domain/Entities/MailingEmail.cs @@ -0,0 +1,40 @@ +using System; + +namespace Surge365.MassEmailReact.Domain.Entities +{ + public class MailingEmail + { + public int? Id { get; private set; } + public int MailingId { get; set; } + public string StatusCode { get; set; } = ""; + public string EmailAddress { get; set; } = ""; + public int ClickCount { get; set; } + public int OpenCount { get; set; } + public DateTime CreateDate { get; set; } + public DateTime UpdateDate { get; set; } + + public MailingEmail() { } + + private MailingEmail(int id, int mailingId, string statusCode, string emailAddress, + int clickCount, int openCount, DateTime createDate, + DateTime updateDate) + { + Id = id; + MailingId = mailingId; + StatusCode = statusCode; + EmailAddress = emailAddress; + ClickCount = clickCount; + OpenCount = openCount; + CreateDate = createDate; + UpdateDate = updateDate; + } + + public static MailingEmail Create(int id, int mailingId, string statusCode, string emailAddress, + int clickCount, int openCount, DateTime createDate, + DateTime updateDate) + { + return new MailingEmail(id, mailingId, statusCode, emailAddress, clickCount, + openCount, createDate, updateDate); + } + } +} \ No newline at end of file diff --git a/Surge365.MassEmailReact.Domain/Entities/MailingTarget.cs b/Surge365.MassEmailReact.Domain/Entities/MailingTarget.cs new file mode 100644 index 0000000..3b2928b --- /dev/null +++ b/Surge365.MassEmailReact.Domain/Entities/MailingTarget.cs @@ -0,0 +1,39 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Surge365.MassEmailReact.Domain.Entities +{ + public class MailingTarget + { + public int? Id { get; private set; } + public int MailingId { get; set; } = 0; + public int ServerId { get; set; } + public string Name { get; set; } = ""; + public string DatabaseName { get; set; } = ""; + public string ViewName { get; set; } = ""; + public string FilterQuery { get; set; } = ""; + public bool AllowWriteBack { get; set; } + + //public List Columns { get; set; } = new List(); + + public MailingTarget() { } + private MailingTarget(int id, int mailingId, int serverId, string name, string databaseName, string viewName, string filterQuery, bool allowWriteBack) + { + Id = id; + MailingId = mailingId; + ServerId = ServerId; + Name = name; + DatabaseName = databaseName; + ViewName = viewName; + FilterQuery = filterQuery; + AllowWriteBack = allowWriteBack; + } + public static MailingTarget Create(int id, int mailingId, int serverId, string name, string databaseName, string viewName, string filterQuery, bool allowWriteBack) + { + return new MailingTarget(id, mailingId, serverId, name, databaseName, viewName, filterQuery, allowWriteBack); + } + } +} diff --git a/Surge365.MassEmailReact.Domain/Entities/MailingTemplate.cs b/Surge365.MassEmailReact.Domain/Entities/MailingTemplate.cs new file mode 100644 index 0000000..9069e46 --- /dev/null +++ b/Surge365.MassEmailReact.Domain/Entities/MailingTemplate.cs @@ -0,0 +1,54 @@ +using System; + +namespace Surge365.MassEmailReact.Domain.Entities +{ + public class MailingTemplate + { + public int? Id { get; private set; } + public int MailingId { get; set; } = 0; + public string Name { get; set; } = ""; + public int DomainId { get; set; } + public string Description { get; set; } = ""; + public string HtmlBody { get; set; } = ""; + public string Subject { get; set; } = ""; + public string ToName { get; set; } = ""; + public string FromName { get; set; } = ""; + public string FromEmail { get; set; } = ""; + public string ReplyToEmail { get; set; } = ""; + public bool ClickTracking { get; set; } + public bool OpenTracking { get; set; } + public string CategoryXml { get; set; } = ""; + public bool IsActive { get; set; } + + public MailingTemplate() { } + + private MailingTemplate(int id, int mailingId, string name, int domainId, string description, string htmlBody, string subject, + string toName, string fromName, string fromEmail, string replyToEmail, + bool clickTracking, bool openTracking, string categoryXml, bool isActive) + { + MailingId = mailingId; + Name = name; + DomainId = domainId; + Description = description; + HtmlBody = htmlBody; + Subject = subject; + ToName = toName; + FromName = fromName; + FromEmail = fromEmail; + ReplyToEmail = replyToEmail; + ClickTracking = clickTracking; + OpenTracking = openTracking; + CategoryXml = categoryXml; + IsActive = isActive; + } + + public static MailingTemplate Create(int id, int mailingId, string name, int domainId, string description, string htmlBody, + string subject, string toName, string fromName, string fromEmail, + string replyToEmail, bool clickTracking, bool openTracking, + string categoryXml, bool isActive) + { + return new MailingTemplate(id, mailingId, name, domainId, description, htmlBody, subject, toName, fromName, + fromEmail, replyToEmail, clickTracking, openTracking, categoryXml, isActive); + } + } +} \ No newline at end of file diff --git a/Surge365.MassEmailReact.Infrastructure/DapperMaps/DapperConfiguration.cs b/Surge365.MassEmailReact.Infrastructure/DapperMaps/DapperConfiguration.cs index e9308cf..0eaa19b 100644 --- a/Surge365.MassEmailReact.Infrastructure/DapperMaps/DapperConfiguration.cs +++ b/Surge365.MassEmailReact.Infrastructure/DapperMaps/DapperConfiguration.cs @@ -26,6 +26,9 @@ namespace Surge365.MassEmailReact.Infrastructure.DapperMaps config.AddMap(new TemplateMap()); config.AddMap(new EmailDomainMap()); config.AddMap(new MailingMap()); + config.AddMap(new MailingEmailMap()); + config.AddMap(new MailingTemplateMap()); + config.AddMap(new MailingTargetMap()); config.AddMap(new MailingStatisticMap()); config.AddMap(new UserMap()); }); diff --git a/Surge365.MassEmailReact.Infrastructure/DapperMaps/MailingEmailMap.cs b/Surge365.MassEmailReact.Infrastructure/DapperMaps/MailingEmailMap.cs new file mode 100644 index 0000000..4eff038 --- /dev/null +++ b/Surge365.MassEmailReact.Infrastructure/DapperMaps/MailingEmailMap.cs @@ -0,0 +1,20 @@ +using Dapper.FluentMap.Mapping; +using Surge365.MassEmailReact.Domain.Entities; + +namespace Surge365.MassEmailReact.Infrastructure.DapperMaps +{ + public class MailingEmailMap : EntityMap + { + public MailingEmailMap() + { + Map(e => e.Id).ToColumn("blast_email_key"); + Map(e => e.MailingId).ToColumn("blast_key"); + Map(e => e.StatusCode).ToColumn("blast_email_status_code"); + Map(e => e.EmailAddress).ToColumn("email_address"); + Map(e => e.ClickCount).ToColumn("click_count"); + Map(e => e.OpenCount).ToColumn("open_count"); + Map(e => e.CreateDate).ToColumn("create_date"); + Map(e => e.UpdateDate).ToColumn("update_date"); + } + } +} \ No newline at end of file diff --git a/Surge365.MassEmailReact.Infrastructure/DapperMaps/MailingMap.cs b/Surge365.MassEmailReact.Infrastructure/DapperMaps/MailingMap.cs index 0a9a26c..f9fc6f2 100644 --- a/Surge365.MassEmailReact.Infrastructure/DapperMaps/MailingMap.cs +++ b/Surge365.MassEmailReact.Infrastructure/DapperMaps/MailingMap.cs @@ -17,7 +17,6 @@ namespace Surge365.MassEmailReact.Infrastructure.DapperMaps Map(m => m.SentDate).ToColumn("sent_date"); Map(m => m.CreateDate).ToColumn("create_date"); Map(m => m.UpdateDate).ToColumn("update_date"); - Map(m => m.SessionActivityId).ToColumn("session_activity_id"); Map(m => m.RecurringTypeCode).ToColumn("blast_recurring_type_code"); Map(m => m.RecurringStartDate).ToColumn("recurring_start_date"); } diff --git a/Surge365.MassEmailReact.Infrastructure/DapperMaps/MailingTargetMap.cs b/Surge365.MassEmailReact.Infrastructure/DapperMaps/MailingTargetMap.cs new file mode 100644 index 0000000..09cb2a5 --- /dev/null +++ b/Surge365.MassEmailReact.Infrastructure/DapperMaps/MailingTargetMap.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Dapper.FluentMap.Mapping; +using Surge365.MassEmailReact.Domain.Entities; + +namespace Surge365.MassEmailReact.Infrastructure.DapperMaps +{ + public class MailingTargetMap : EntityMap + { + public MailingTargetMap() + { + Map(t => t.Id).ToColumn("blast_target_key"); + Map(t => t.MailingId).ToColumn("blast_key"); + Map(t => t.ServerId).ToColumn("blast_server_key"); + Map(t => t.Name).ToColumn("name"); + Map(t => t.DatabaseName).ToColumn("database_name"); + Map(t => t.ViewName).ToColumn("view_name"); + Map(t => t.FilterQuery).ToColumn("filter_query"); + Map(t => t.AllowWriteBack).ToColumn("allow_write_back"); + } + } +} diff --git a/Surge365.MassEmailReact.Infrastructure/DapperMaps/MailingTemplateMap.cs b/Surge365.MassEmailReact.Infrastructure/DapperMaps/MailingTemplateMap.cs new file mode 100644 index 0000000..4537baa --- /dev/null +++ b/Surge365.MassEmailReact.Infrastructure/DapperMaps/MailingTemplateMap.cs @@ -0,0 +1,27 @@ +using Dapper.FluentMap.Mapping; +using Surge365.MassEmailReact.Domain.Entities; + +namespace Surge365.MassEmailReact.Infrastructure.DapperMaps +{ + public class MailingTemplateMap : EntityMap + { + public MailingTemplateMap() + { + Map(t => t.Id).ToColumn("blast_template_key"); + Map(t => t.MailingId).ToColumn("blast_key"); + Map(t => t.Name).ToColumn("name"); + Map(t => t.DomainId).ToColumn("domain_key"); + Map(t => t.Description).ToColumn("description"); + Map(t => t.HtmlBody).ToColumn("html_body"); + Map(t => t.Subject).ToColumn("subject"); + Map(t => t.ToName).ToColumn("to_name"); + Map(t => t.FromName).ToColumn("from_name"); + Map(t => t.FromEmail).ToColumn("from_email"); + Map(t => t.ReplyToEmail).ToColumn("reply_to_email"); + Map(t => t.ClickTracking).ToColumn("click_tracking"); + Map(t => t.OpenTracking).ToColumn("open_tracking"); + Map(t => t.CategoryXml).ToColumn("category_xml"); + Map(t => t.IsActive).ToColumn("is_active"); + } + } +} \ No newline at end of file diff --git a/Surge365.MassEmailReact.Infrastructure/Repositories/MailingRepository.cs b/Surge365.MassEmailReact.Infrastructure/Repositories/MailingRepository.cs index 0bfad72..d5da265 100644 --- a/Surge365.MassEmailReact.Infrastructure/Repositories/MailingRepository.cs +++ b/Surge365.MassEmailReact.Infrastructure/Repositories/MailingRepository.cs @@ -8,6 +8,7 @@ using System; using System.Collections.Generic; using System.Data; using System.Linq; +using System.Text.Json; using System.Threading.Tasks; namespace Surge365.MassEmailReact.Infrastructure.Repositories @@ -34,7 +35,22 @@ namespace Surge365.MassEmailReact.Infrastructure.Repositories ArgumentNullException.ThrowIfNull(ConnectionString); using SqlConnection conn = new SqlConnection(ConnectionString); - return (await conn.QueryAsync("mem_get_blast_by_id", new { blast_key = id }, commandType: CommandType.StoredProcedure)).FirstOrDefault(); + + await conn.OpenAsync(); + + using var multi = await conn.QueryMultipleAsync( + "mem_get_blast_by_id", + new { blast_key = id }, + commandType: CommandType.StoredProcedure); + + var mailing = await multi.ReadSingleOrDefaultAsync(); + if (mailing == null) return null; + + var template = await multi.ReadSingleOrDefaultAsync(); + if (mailing != null) + mailing.Template = template; + + return mailing; } public async Task> GetAllAsync(bool activeOnly = true) @@ -42,14 +58,57 @@ namespace Surge365.MassEmailReact.Infrastructure.Repositories ArgumentNullException.ThrowIfNull(ConnectionString); using SqlConnection conn = new SqlConnection(ConnectionString); - return (await conn.QueryAsync("mem_get_blast_all", new { active_only = activeOnly }, commandType: CommandType.StoredProcedure)).ToList(); + + await conn.OpenAsync(); + + using var multi = await conn.QueryMultipleAsync( + "mem_get_blast_all", + new { active_only = activeOnly }, + commandType: CommandType.StoredProcedure); + + var mailings = (await multi.ReadAsync()).ToList(); + if (!mailings.Any()) return mailings; + + var templates = (await multi.ReadAsync()).ToList(); + + var mailingDictionary = mailings.ToDictionary(t => t.Id!.Value); + foreach (var template in templates) + { + if (mailingDictionary.TryGetValue(template.MailingId, out var mailing)) + { + mailing.Template = template; + } + } + + return mailings; } public async Task> GetByStatusAsync(string codes, string? startDate, string? endDate) { ArgumentNullException.ThrowIfNull(ConnectionString); using SqlConnection conn = new SqlConnection(ConnectionString); - return (await conn.QueryAsync("mem_get_blast_by_status", new { blast_status_codes = codes, start_date = startDate, end_date = endDate }, commandType: CommandType.StoredProcedure)).ToList(); + await conn.OpenAsync(); + + using var multi = await conn.QueryMultipleAsync( + "mem_get_blast_by_status", + new { blast_status_codes = codes, start_date = startDate, end_date = endDate }, + commandType: CommandType.StoredProcedure); + + var mailings = (await multi.ReadAsync()).ToList(); + if (!mailings.Any()) return mailings; + + var templates = (await multi.ReadAsync()).ToList(); + + var mailingDictionary = mailings.ToDictionary(t => t.Id!.Value); + foreach (var template in templates) + { + if (mailingDictionary.TryGetValue(template.MailingId, out var mailing)) + { + mailing.Template = template; + } + } + + return mailings; } public async Task> GetStatisticsByStatusAsync(string codes, string? startDate, string? endDate) { @@ -65,6 +124,27 @@ namespace Surge365.MassEmailReact.Infrastructure.Repositories using SqlConnection conn = new SqlConnection(ConnectionString); return (await conn.QueryAsync("mem_get_blast_statistic_by_blast", new { blast_key = id }, commandType: CommandType.StoredProcedure)).FirstOrDefault(); } + public async Task> GetEmailsByIdAsync(int id) + { + ArgumentNullException.ThrowIfNull(ConnectionString); + + using SqlConnection conn = new SqlConnection(ConnectionString); + return (await conn.QueryAsync("mem_get_blast_email_by_blast_id", new { blast_key = id }, commandType: CommandType.StoredProcedure)).ToList(); + } + public async Task GetTemplateByIdAsync(int id) + { + ArgumentNullException.ThrowIfNull(ConnectionString); + + using SqlConnection conn = new SqlConnection(ConnectionString); + return (await conn.QueryAsync("mem_get_blast_template_by_blast_id", new { blast_key = id }, commandType: CommandType.StoredProcedure)).FirstOrDefault(); + } + public async Task GetTargetByIdAsync(int id) + { + ArgumentNullException.ThrowIfNull(ConnectionString); + + using SqlConnection conn = new SqlConnection(ConnectionString); + return (await conn.QueryAsync("mem_get_blast_target_by_blast_id", new { blast_key = id }, commandType: CommandType.StoredProcedure)).FirstOrDefault(); + } public async Task NameIsAvailableAsync(int? id, string name) { ArgumentNullException.ThrowIfNull(ConnectionString); @@ -111,9 +191,9 @@ namespace Surge365.MassEmailReact.Infrastructure.Repositories parameters.Add("@blast_status_code", mailing.StatusCode, DbType.String); parameters.Add("@schedule_date", mailing.ScheduleDate, DbType.DateTime); parameters.Add("@sent_date", mailing.SentDate, DbType.DateTime); - parameters.Add("@session_activity_id", mailing.SessionActivityId, DbType.Guid); parameters.Add("@blast_recurring_type_code", mailing.RecurringTypeCode, DbType.String); parameters.Add("@recurring_start_date", mailing.RecurringStartDate, DbType.DateTime); + parameters.Add("@template_json", JsonSerializer.Serialize(mailing.Template, new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower }), DbType.String); parameters.Add("@success", dbType: DbType.Boolean, direction: ParameterDirection.Output); await conn.ExecuteAsync("mem_save_blast", parameters, commandType: CommandType.StoredProcedure); @@ -140,9 +220,9 @@ namespace Surge365.MassEmailReact.Infrastructure.Repositories parameters.Add("@blast_status_code", mailing.StatusCode, DbType.String); parameters.Add("@schedule_date", mailing.ScheduleDate, DbType.DateTime); parameters.Add("@sent_date", mailing.SentDate, DbType.DateTime); - parameters.Add("@session_activity_id", mailing.SessionActivityId, DbType.Guid); parameters.Add("@blast_recurring_type_code", mailing.RecurringTypeCode, DbType.String); parameters.Add("@recurring_start_date", mailing.RecurringStartDate, DbType.DateTime); + parameters.Add("@template_json", JsonSerializer.Serialize(mailing.Template, new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower }), DbType.String); parameters.Add("@success", dbType: DbType.Boolean, direction: ParameterDirection.Output); await conn.ExecuteAsync("mem_save_blast", parameters, commandType: CommandType.StoredProcedure); diff --git a/Surge365.MassEmailReact.Infrastructure/Services/MailingService.cs b/Surge365.MassEmailReact.Infrastructure/Services/MailingService.cs index b6ecf14..5f4e65a 100644 --- a/Surge365.MassEmailReact.Infrastructure/Services/MailingService.cs +++ b/Surge365.MassEmailReact.Infrastructure/Services/MailingService.cs @@ -73,6 +73,18 @@ namespace Surge365.MassEmailReact.Infrastructure.Services { return await _mailingRepository.GetStatisticByIdAsync(id); } + public async Task> GetEmailsByIdAsync(int id) + { + return await _mailingRepository.GetEmailsByIdAsync(id); + } + public async Task GetTemplateByIdAsync(int id) + { + return await _mailingRepository.GetTemplateByIdAsync(id); + } + public async Task GetTargetByIdAsync(int id) + { + return await _mailingRepository.GetTargetByIdAsync(id); + } public async Task NameIsAvailableAsync(int? id, string name) { return await _mailingRepository.NameIsAvailableAsync(id, name); @@ -96,9 +108,14 @@ namespace Surge365.MassEmailReact.Infrastructure.Services StatusCode = mailingDto.StatusCode, ScheduleDate = mailingDto.ScheduleDate, SentDate = mailingDto.SentDate, - SessionActivityId = mailingDto.SessionActivityId, RecurringTypeCode = mailingDto.RecurringTypeCode, - RecurringStartDate = mailingDto.RecurringStartDate + RecurringStartDate = mailingDto.RecurringStartDate, + Template = new MailingTemplate + { + DomainId = mailingDto.Template.DomainId, + Subject = mailingDto.Template.Subject, + FromName = mailingDto.Template.FromName + } }; return await _mailingRepository.CreateAsync(mailing); @@ -119,9 +136,14 @@ namespace Surge365.MassEmailReact.Infrastructure.Services mailing.StatusCode = mailingDto.StatusCode; mailing.ScheduleDate = mailingDto.ScheduleDate; mailing.SentDate = mailingDto.SentDate; - mailing.SessionActivityId = mailingDto.SessionActivityId; mailing.RecurringTypeCode = mailingDto.RecurringTypeCode; mailing.RecurringStartDate = mailingDto.RecurringStartDate; + mailing.Template = new MailingTemplate + { + DomainId = mailingDto.Template.DomainId, + Subject = mailingDto.Template.Subject, + FromName = mailingDto.Template.FromName + }; return await _mailingRepository.UpdateAsync(mailing); } diff --git a/Surge365.MassEmailReact.Web/src/components/modals/MailingEdit.tsx b/Surge365.MassEmailReact.Web/src/components/modals/MailingEdit.tsx index 824f334..18d6d2f 100644 --- a/Surge365.MassEmailReact.Web/src/components/modals/MailingEdit.tsx +++ b/Surge365.MassEmailReact.Web/src/components/modals/MailingEdit.tsx @@ -20,6 +20,8 @@ import VisibilityIcon from '@mui/icons-material/Visibility'; import Template from "@/types/template"; import Mailing from "@/types/mailing"; import Target from "@/types/target"; +//import MailingTemplate from "@/types/mailingTemplate"; +//import MailingTarget from "@/types/mailingTarget"; import EmailList from "@/components/forms/EmailList"; import TestEmailList from "@/types/testEmailList"; import TemplateViewer from "@/components/modals/TemplateViewer" @@ -68,7 +70,7 @@ const schema = yup.object().shape({ if (value.length === 0) return true; return await nameIsAvailable(this.parent.id, value); - }), + }), description: yup.string().default(""), templateId: yup.number().typeError("Template is required").required("Template is required").test("valid-template", "Invalid template", function (value) { const setupData = this.options.context?.setupData as SetupData; @@ -110,12 +112,12 @@ const schema = yup.object().shape({ .string() .nullable() .when("$recurring", (recurring, schema) => { // Use context variable - const isRecurring = recurring[0] ?? false; + const isRecurring = recurring[0] ?? false; return isRecurring ? schema.oneOf(recurringTypeOptions.map((r) => r.code), "Invalid recurring type") : schema.nullable(); }), - + recurringStartDate: yup.string() .nullable() .when("$recurring", (recurring, schema) => { @@ -133,12 +135,39 @@ const schema = yup.object().shape({ }) : schema.nullable(); }), - - //.when("recurringTypeCode", { - //is: (value: string) => value !== "" && value !== null, // String comparison for "None" - //then: (schema) => schema.required("Recurring start date is required when recurring type is set"), - //otherwise: (schema) => schema.nullable(), - //}), + template: yup.object().shape({ + id: yup.number().nullable().default(0), + mailingId: yup.number().default(0), + name: yup.string().default(""), + domainId: yup + .number() + .typeError("Domain is required") + .required("Domain is required") + .test("valid-domain", "Invalid domain", function (value) { + const setupData = this.options.context?.setupData as SetupData; + return setupData.emailDomains.some((d) => d.id === value); + }), + description: yup.string().default(""), + htmlBody: yup.string().default(""), + subject: yup.string().required("Subject is required").default(""), + toName: yup.string().default(""), + fromName: yup.string().required("From Name is required").default(""), + fromEmail: yup.string().default(""), + replyToEmail: yup.string().default(""), + clickTracking: yup.boolean().default(false), + openTracking: yup.boolean().default(false), + categoryXml: yup.string().default(""), + }), + target: yup.object().shape({ + id: yup.number().nullable().default(0), + mailingId: yup.number().default(0), + serverId: yup.number().default(0), + name: yup.string().default(""), + databaseName: yup.string().default(""), + viewName: yup.string().default(""), + filterQuery: yup.string().default(""), + allowWriteBack: yup.boolean().default(false), + }).nullable(), }); const nameIsAvailable = async (id: number, name: string) => { @@ -164,6 +193,33 @@ const defaultMailing: Mailing = { sessionActivityId: null, recurringTypeCode: null, recurringStartDate: null, + template: { + id: 0, + mailingId: 0, + name: "", + domainId: 0, + description: "", + htmlBody: "", + subject: "", + toName: "", + fromName: "", + fromEmail: "", + replyToEmail: "", + clickTracking: false, + openTracking: false, + categoryXml: "" + }, + target: { + id: 0, + mailingId: 0, + serverId: 0, + name: "", + databaseName: "", + viewName: "", + filterQuery: "", + allowWriteBack: false, + } +, }; const MailingEdit = ({ open, mailing, onClose, onSave }: MailingEditProps) => { @@ -179,7 +235,7 @@ const MailingEdit = ({ open, mailing, onClose, onSave }: MailingEditProps) => { const [TargetSampleModalOpen, setTargetSampleModalOpen] = useState(false); const [currentTarget, setCurrentTarget] = useState(null); - const { register, trigger, control, handleSubmit, reset, formState: { errors } } = useForm({ + const { register, trigger, control, handleSubmit, reset, setValue, formState: { errors } } = useForm({ mode: "onBlur", defaultValues: { ...(mailing || defaultMailing), @@ -308,8 +364,8 @@ const MailingEdit = ({ open, mailing, onClose, onSave }: MailingEditProps) => { throw new Error("Failed to test mailing"); } - toast.success("Test mailing sent successfully"); - console.log("Test mailing sent successfully"); + toast.success("Test mailing(s) sent successfully"); + console.log("Test mailing(s) sent successfully"); } catch (error) { console.error("Test mailing error:", error); toast.error("Failed to send test mailing"); @@ -341,6 +397,7 @@ const MailingEdit = ({ open, mailing, onClose, onSave }: MailingEditProps) => { }; const filteredTargets = setupData.targets.filter(t => t.isActive); const filteredTemplates = setupData.templates.filter(t => t.isActive); + const filteredEmailDomains = setupData.emailDomains.filter((domain) => domain.isActive); return ( {/* Wrap with LocalizationProvider */} { onClose(reason); }} maxWidth="sm" fullWidth disableEscapeKeyDown > @@ -377,6 +434,16 @@ const MailingEdit = ({ open, mailing, onClose, onSave }: MailingEditProps) => { field.onChange(newValue ? newValue.id : null); trigger("templateId"); setCurrentTemplate(newValue); + // Update the template object in the form data + if (newValue) { + setValue("template.domainId", newValue.domainId, { shouldValidate: true }); + setValue("template.subject", newValue.subject ?? "", { shouldValidate: true }); + setValue("template.fromName", newValue.fromName ?? "", { shouldValidate: true }); + } else { + setValue("template.domainId", 0, { shouldValidate: true }); + setValue("template.subject", "", { shouldValidate: true }); + setValue("template.fromName", "", { shouldValidate: true }); + } }} renderInput={(params) => ( { )} + {/* Add domainId Autocomplete */} + ( + option.name} + value={filteredEmailDomains.find((d) => d.id === field.value) || null} + onChange={(_, newValue) => { + field.onChange(newValue ? newValue.id : null); + trigger("template.domainId"); + }} + renderInput={(params) => ( + + )} + /> + )} + /> + + ( + field.onChange(e.target.value)} // Ensure value updates + value={field.value ?? ""} // Ensure controlled value + /> + )} + /> + + ( + field.onChange(e.target.value)} // Ensure value updates + value={field.value ?? ""} // Ensure controlled value + /> + )} + /> void; } +interface MailingEmail { + id: number; + mailingId: number; + statusCode: string; + emailAddress: string; + clickCount: number; + openCount: number; + createDate: string; + updateDate: string; + sessionActivityId?: string; +} + function MailingView({ open, mailing, onClose }: MailingViewProps) { const setupData = useSetupData(); const [templateViewerOpen, setTemplateViewerOpen] = useState(false); - const [TargetSampleModalOpen, setTargetSampleModalOpen] = useState(false); + const [targetSampleModalOpen, setTargetSampleModalOpen] = useState(false); + const [emails, setEmails] = useState([]); + const [loading, setLoading] = useState(false); + const [statusFilter, setStatusFilter] = useState(""); + + // Load emails from API + useEffect(() => { + const fetchEmails = async () => { + if (!mailing?.id) { + setEmails([]); + setLoading(false); + return; + } + setLoading(true); + + const emailsResponse = await fetch(`/api/mailings/${mailing.id}/emails`); + const emailsData = await emailsResponse.json(); + + if (emailsData) { + setEmails(emailsData); + } else { + console.error("Failed to fetch emails"); + } + setLoading(false); + }; + fetchEmails(); + }, [mailing?.id]); if (!mailing) return null; // Look up related data from setupData - const template = setupData.templates.find(t => t.id === mailing.templateId); - const target = setupData.targets.find(t => t.id === mailing.targetId); - const domain = template ? setupData.emailDomains.find(d => d.id === template.domainId) : null; + const template = mailing.template ?? setupData.templates.find((t) => t.id === mailing.templateId); //TODO: Pull from mailing.Template if status is not "ED" + const target = mailing.target ?? setupData.targets.find((t) => t.id === mailing.targetId); + const domain = template ? setupData.emailDomains.find((d) => d.id === template.domainId) : null; - // Format status string - const statusString = mailing.scheduleDate - ? `Scheduled for ${new Date(mailing.scheduleDate).toLocaleString()}` - : mailing.sentDate - ? `Sent on ${new Date(mailing.sentDate).toLocaleString()}` - : 'N/A'; + // Status mappings + const statusMap: { [key: string]: string } = { + BL: "Blocked", + C: "Complaint Received", + D: "Delivered", + DR: "Dropped", + F: "Failed", + HB: "Hard Bounced", + I: "Invalid", + P: "Pending", + S: "Sent", + SB: "Soft Bounced", + U: "Unsubscribed", + }; - // Helper function to format recurring string (customize based on your recurringTypeCode logic) + // Format status string for mailing + const statusString = { + C: "Cancelled", + ED: "Editing", + ER: "Error", + QE: "Queueing Error", + S: "Sent", + SD: "Sending", + SC: mailing.scheduleDate + ? `Scheduled for ${new Date(mailing.scheduleDate).toLocaleString()}` + : "Waiting to Send", + }[mailing.statusCode.trim().toUpperCase()] || "Unknown"; + + // Helper function to format recurring string const formatRecurringString = (typeCode: string, startDate: string): string => { const date = new Date(startDate); switch (typeCode.toUpperCase()) { - case 'D': - return `Daily at ${date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}`; - case 'W': - return `Weekly on ${date.toLocaleDateString('en-US', { weekday: 'long' })} at ${date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}`; - case 'M': - return `Monthly on day ${date.getDate()} at ${date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}`; + case "D": + return `Daily at ${date.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })}`; + case "W": + return `Weekly on ${date.toLocaleDateString("en-US", { weekday: "long" })} at ${date.toLocaleTimeString([], { + hour: "2-digit", + minute: "2-digit", + })}`; + case "M": + return `Monthly on day ${date.getDate()} at ${date.toLocaleTimeString([], { + hour: "2-digit", + minute: "2-digit", + })}`; default: - return 'Custom recurring schedule'; + return "Custom recurring schedule"; } }; // Format recurring string - const recurringString = mailing.recurringTypeCode && mailing.recurringStartDate - ? formatRecurringString(mailing.recurringTypeCode, mailing.recurringStartDate) - : 'No'; + const recurringString = + mailing.recurringTypeCode && mailing.recurringStartDate + ? formatRecurringString(mailing.recurringTypeCode, mailing.recurringStartDate) + : "No"; // Navigation handlers for viewing related entities const handleViewTarget = () => { if (target) { - setTargetSampleModalOpen(!TargetSampleModalOpen); + setTargetSampleModalOpen(!targetSampleModalOpen); } }; @@ -65,15 +153,62 @@ function MailingView({ open, mailing, onClose }: MailingViewProps) { } }; + // Grid columns + const columns: GridColDef[] = [ + { + field: "statusCode", + headerName: "Status", + width: 150, + valueGetter: (_: number, row: MailingEmail) => statusMap[row.statusCode.trim()] || row.statusCode, + }, + { field: "emailAddress", headerName: "Email Address", flex: 1, minWidth: 200 }, + { field: "clickCount", headerName: "Clicks", width: 100 }, + { field: "openCount", headerName: "Opens", width: 100 }, + ]; + + // Unique status codes for filter + const uniqueStatusCodes = Object.keys(statusMap); + + // Custom toolbar with status filter + const CustomToolbar = () => ( + + + Status + + + + + + + + ); + + // Filter emails based on status + const filteredEmails = statusFilter + ? emails.filter((email) => email.statusCode.trim() === statusFilter) + : emails; + return ( - + {mailing.name} theme.palette.grey[500], @@ -84,7 +219,7 @@ function MailingView({ open, mailing, onClose }: MailingViewProps) { - Target Name: {target?.name || 'Unknown'} + Target Name: {target?.name || "Unknown"} {target && ( @@ -93,7 +228,7 @@ function MailingView({ open, mailing, onClose }: MailingViewProps) { - Template Name: {template?.name || 'Unknown'} + Template Name: {template?.name || "Unknown"} {template && ( @@ -101,24 +236,49 @@ function MailingView({ open, mailing, onClose }: MailingViewProps) { )} - From Name: {template?.fromName || 'N/A'} - Domain: {domain?.name || 'N/A'} {/* Assuming EmailDomain has a domainName field */} - Subject: {template?.subject || 'N/A'} + From Name: {template?.fromName || "N/A"} + Domain: {domain?.name || "N/A"} + Subject: {template?.subject || "N/A"} Status: {statusString} - Recurring: {recurringString} + Recurring: {recurringString} + + + row.id} + loading={loading} + autoPageSize + slots={{ toolbar: CustomToolbar }} + slotProps={{ + toolbar: { + showQuickFilter: true, + }, + }} + initialState={{ + pagination: { + paginationModel: { + pageSize: 20, + }, + }, + }} + pageSizeOptions={[10, 20, 50, 100]} + sx={{ minWidth: "600px" }} + /> + {templateViewerOpen && ( { setTemplateViewerOpen(false) }} + template={template as Template} + onClose={() => setTemplateViewerOpen(false)} /> )} - {TargetSampleModalOpen && ( + {targetSampleModalOpen && ( { setTargetSampleModalOpen(false) }} + open={targetSampleModalOpen} + target={target as Target} + onClose={() => setTargetSampleModalOpen(false)} /> )} diff --git a/Surge365.MassEmailReact.Web/src/types/mailing.ts b/Surge365.MassEmailReact.Web/src/types/mailing.ts index 85adce2..2282aef 100644 --- a/Surge365.MassEmailReact.Web/src/types/mailing.ts +++ b/Surge365.MassEmailReact.Web/src/types/mailing.ts @@ -1,3 +1,5 @@ +import MailingTemplate from '@/types/mailingTemplate'; +import MailingTarget from '@/types/mailingTarget'; export interface Mailing { id: number; name: string; @@ -10,6 +12,8 @@ export interface Mailing { sessionActivityId: string | null; recurringTypeCode: string | null; recurringStartDate: string | null; + template: MailingTemplate | null; + target: MailingTarget | null; } export default Mailing; \ No newline at end of file diff --git a/Surge365.MassEmailReact.Web/src/types/mailingTarget.ts b/Surge365.MassEmailReact.Web/src/types/mailingTarget.ts new file mode 100644 index 0000000..063a5af --- /dev/null +++ b/Surge365.MassEmailReact.Web/src/types/mailingTarget.ts @@ -0,0 +1,14 @@ +//import MailingTargetColumn from './mailingTargetColumn'; +export interface MailingTarget { + id: number; + mailingId: number; + serverId: number; + name: string; + databaseName: string; + viewName: string; + filterQuery: string; + allowWriteBack: boolean; + //columns: MailingTargetColumn[]; +} + +export default MailingTarget; \ No newline at end of file diff --git a/Surge365.MassEmailReact.Web/src/types/mailingTemplate.ts b/Surge365.MassEmailReact.Web/src/types/mailingTemplate.ts new file mode 100644 index 0000000..4216d63 --- /dev/null +++ b/Surge365.MassEmailReact.Web/src/types/mailingTemplate.ts @@ -0,0 +1,18 @@ +export interface MailingTemplate { + id: number; + mailingId: number; + name: string; + domainId: number; + description: string; + htmlBody: string; + subject: string; + toName: string; + fromName: string; + fromEmail: string; + replyToEmail: string; + clickTracking: boolean; + openTracking: boolean; + categoryXml: string; +} + +export default MailingTemplate; \ No newline at end of file