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.
This commit is contained in:
parent
9703517974
commit
51c267e48f
@ -67,6 +67,19 @@ namespace Surge365.MassEmailReact.API.Controllers
|
|||||||
return mailing is not null ? Ok(mailing) : NotFound($"Mailing statistics with id '{id}' not found.");
|
return mailing is not null ? Ok(mailing) : NotFound($"Mailing statistics with id '{id}' not found.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpGet("{id}/emails")]
|
||||||
|
public async Task<IActionResult> 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<IActionResult> 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]
|
[HttpPost]
|
||||||
public async Task<IActionResult> CreateMailing([FromBody] MailingUpdateDto mailingUpdateDto)
|
public async Task<IActionResult> CreateMailing([FromBody] MailingUpdateDto mailingUpdateDto)
|
||||||
{
|
{
|
||||||
|
|||||||
@ -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; } = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -15,5 +15,6 @@ namespace Surge365.MassEmailReact.Domain.Entities
|
|||||||
public Guid? SessionActivityId { get; set; }
|
public Guid? SessionActivityId { get; set; }
|
||||||
public string? RecurringTypeCode { get; set; }
|
public string? RecurringTypeCode { get; set; }
|
||||||
public DateTime? RecurringStartDate { get; set; }
|
public DateTime? RecurringStartDate { get; set; }
|
||||||
}
|
public MailingTemplateUpdateDto Template { get; set; } = new MailingTemplateUpdateDto();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -11,6 +11,9 @@ namespace Surge365.MassEmailReact.Application.Interfaces
|
|||||||
Task<List<Mailing>> GetByStatusAsync(string codes, string? startDate, string? endDate);
|
Task<List<Mailing>> GetByStatusAsync(string codes, string? startDate, string? endDate);
|
||||||
Task<List<MailingStatistic>> GetStatisticsByStatusAsync(string codes, string? startDate, string? endDate);
|
Task<List<MailingStatistic>> GetStatisticsByStatusAsync(string codes, string? startDate, string? endDate);
|
||||||
Task<MailingStatistic?> GetStatisticByIdAsync(int id);
|
Task<MailingStatistic?> GetStatisticByIdAsync(int id);
|
||||||
|
Task<List<MailingEmail>> GetEmailsByIdAsync(int id);
|
||||||
|
Task<MailingTemplate?> GetTemplateByIdAsync(int id);
|
||||||
|
Task<MailingTarget?> GetTargetByIdAsync(int id);
|
||||||
Task<bool> NameIsAvailableAsync(int? id, string name);
|
Task<bool> NameIsAvailableAsync(int? id, string name);
|
||||||
Task<string> GetNextAvailableNameAsync(int? id, string name);
|
Task<string> GetNextAvailableNameAsync(int? id, string name);
|
||||||
Task<int?> CreateAsync(Mailing mailing);
|
Task<int?> CreateAsync(Mailing mailing);
|
||||||
|
|||||||
@ -12,6 +12,9 @@ namespace Surge365.MassEmailReact.Application.Interfaces
|
|||||||
Task<List<Mailing>> GetByStatusAsync(string codes, string? startDate, string? endDate);
|
Task<List<Mailing>> GetByStatusAsync(string codes, string? startDate, string? endDate);
|
||||||
Task<List<MailingStatistic>> GetStatisticsByStatusAsync(string codes, string? startDate, string? endDate);
|
Task<List<MailingStatistic>> GetStatisticsByStatusAsync(string codes, string? startDate, string? endDate);
|
||||||
Task<MailingStatistic?> GetStatisticByIdAsync(int id);
|
Task<MailingStatistic?> GetStatisticByIdAsync(int id);
|
||||||
|
Task<List<MailingEmail>> GetEmailsByIdAsync(int id);
|
||||||
|
Task<MailingTemplate?> GetTemplateByIdAsync(int id);
|
||||||
|
Task<MailingTarget?> GetTargetByIdAsync(int id);
|
||||||
Task<bool> NameIsAvailableAsync(int? id, string name);
|
Task<bool> NameIsAvailableAsync(int? id, string name);
|
||||||
Task<string> GetNextAvailableNameAsync(int? id, string name);
|
Task<string> GetNextAvailableNameAsync(int? id, string name);
|
||||||
|
|
||||||
|
|||||||
@ -14,15 +14,15 @@ namespace Surge365.MassEmailReact.Domain.Entities
|
|||||||
public DateTime? SentDate { get; set; }
|
public DateTime? SentDate { get; set; }
|
||||||
public DateTime CreateDate { get; set; } = DateTime.Now;
|
public DateTime CreateDate { get; set; } = DateTime.Now;
|
||||||
public DateTime UpdateDate { get; set; } = DateTime.Now;
|
public DateTime UpdateDate { get; set; } = DateTime.Now;
|
||||||
public Guid? SessionActivityId { get; set; }
|
|
||||||
public string? RecurringTypeCode { get; set; }
|
public string? RecurringTypeCode { get; set; }
|
||||||
public DateTime? RecurringStartDate { get; set; }
|
public DateTime? RecurringStartDate { get; set; }
|
||||||
|
public MailingTemplate Template { get; set; } = new MailingTemplate();
|
||||||
|
|
||||||
public Mailing() { }
|
public Mailing() { }
|
||||||
|
|
||||||
private Mailing(int id, string name, string description, int templateId, int targetId,
|
private Mailing(int id, string name, string description, int templateId, int targetId,
|
||||||
string statusCode, DateTime? scheduleDate, DateTime? sentDate, DateTime createDate,
|
string statusCode, DateTime? scheduleDate, DateTime? sentDate, DateTime createDate,
|
||||||
DateTime updateDate, Guid? sessionActivityId, string? recurringTypeCode, DateTime? recurringStartDate)
|
DateTime updateDate, string? recurringTypeCode, DateTime? recurringStartDate)
|
||||||
{
|
{
|
||||||
Id = id;
|
Id = id;
|
||||||
Name = name;
|
Name = name;
|
||||||
@ -34,17 +34,16 @@ namespace Surge365.MassEmailReact.Domain.Entities
|
|||||||
SentDate = sentDate;
|
SentDate = sentDate;
|
||||||
CreateDate = createDate;
|
CreateDate = createDate;
|
||||||
UpdateDate = updateDate;
|
UpdateDate = updateDate;
|
||||||
SessionActivityId = sessionActivityId;
|
|
||||||
RecurringTypeCode = recurringTypeCode;
|
RecurringTypeCode = recurringTypeCode;
|
||||||
RecurringStartDate = recurringStartDate;
|
RecurringStartDate = recurringStartDate;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Mailing Create(int id, string name, string description, int templateId, int targetId,
|
public static Mailing Create(int id, string name, string description, int templateId, int targetId,
|
||||||
string statusCode, DateTime? scheduleDate, DateTime? sentDate, DateTime createDate,
|
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,
|
return new Mailing(id, name, description, templateId, targetId, statusCode, scheduleDate,
|
||||||
sentDate, createDate, updateDate, sessionActivityId, recurringTypeCode, recurringStartDate);
|
sentDate, createDate, updateDate, recurringTypeCode, recurringStartDate);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
40
Surge365.MassEmailReact.Domain/Entities/MailingEmail.cs
Normal file
40
Surge365.MassEmailReact.Domain/Entities/MailingEmail.cs
Normal file
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
39
Surge365.MassEmailReact.Domain/Entities/MailingTarget.cs
Normal file
39
Surge365.MassEmailReact.Domain/Entities/MailingTarget.cs
Normal file
@ -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<TargetColumn> Columns { get; set; } = new List<TargetColumn>();
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
54
Surge365.MassEmailReact.Domain/Entities/MailingTemplate.cs
Normal file
54
Surge365.MassEmailReact.Domain/Entities/MailingTemplate.cs
Normal file
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -26,6 +26,9 @@ namespace Surge365.MassEmailReact.Infrastructure.DapperMaps
|
|||||||
config.AddMap(new TemplateMap());
|
config.AddMap(new TemplateMap());
|
||||||
config.AddMap(new EmailDomainMap());
|
config.AddMap(new EmailDomainMap());
|
||||||
config.AddMap(new MailingMap());
|
config.AddMap(new MailingMap());
|
||||||
|
config.AddMap(new MailingEmailMap());
|
||||||
|
config.AddMap(new MailingTemplateMap());
|
||||||
|
config.AddMap(new MailingTargetMap());
|
||||||
config.AddMap(new MailingStatisticMap());
|
config.AddMap(new MailingStatisticMap());
|
||||||
config.AddMap(new UserMap());
|
config.AddMap(new UserMap());
|
||||||
});
|
});
|
||||||
|
|||||||
@ -0,0 +1,20 @@
|
|||||||
|
using Dapper.FluentMap.Mapping;
|
||||||
|
using Surge365.MassEmailReact.Domain.Entities;
|
||||||
|
|
||||||
|
namespace Surge365.MassEmailReact.Infrastructure.DapperMaps
|
||||||
|
{
|
||||||
|
public class MailingEmailMap : EntityMap<MailingEmail>
|
||||||
|
{
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -17,7 +17,6 @@ namespace Surge365.MassEmailReact.Infrastructure.DapperMaps
|
|||||||
Map(m => m.SentDate).ToColumn("sent_date");
|
Map(m => m.SentDate).ToColumn("sent_date");
|
||||||
Map(m => m.CreateDate).ToColumn("create_date");
|
Map(m => m.CreateDate).ToColumn("create_date");
|
||||||
Map(m => m.UpdateDate).ToColumn("update_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.RecurringTypeCode).ToColumn("blast_recurring_type_code");
|
||||||
Map(m => m.RecurringStartDate).ToColumn("recurring_start_date");
|
Map(m => m.RecurringStartDate).ToColumn("recurring_start_date");
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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<MailingTarget>
|
||||||
|
{
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,27 @@
|
|||||||
|
using Dapper.FluentMap.Mapping;
|
||||||
|
using Surge365.MassEmailReact.Domain.Entities;
|
||||||
|
|
||||||
|
namespace Surge365.MassEmailReact.Infrastructure.DapperMaps
|
||||||
|
{
|
||||||
|
public class MailingTemplateMap : EntityMap<MailingTemplate>
|
||||||
|
{
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -8,6 +8,7 @@ using System;
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Data;
|
using System.Data;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Text.Json;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace Surge365.MassEmailReact.Infrastructure.Repositories
|
namespace Surge365.MassEmailReact.Infrastructure.Repositories
|
||||||
@ -34,7 +35,22 @@ namespace Surge365.MassEmailReact.Infrastructure.Repositories
|
|||||||
ArgumentNullException.ThrowIfNull(ConnectionString);
|
ArgumentNullException.ThrowIfNull(ConnectionString);
|
||||||
|
|
||||||
using SqlConnection conn = new SqlConnection(ConnectionString);
|
using SqlConnection conn = new SqlConnection(ConnectionString);
|
||||||
return (await conn.QueryAsync<Mailing>("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<Mailing>();
|
||||||
|
if (mailing == null) return null;
|
||||||
|
|
||||||
|
var template = await multi.ReadSingleOrDefaultAsync<MailingTemplate>();
|
||||||
|
if (mailing != null)
|
||||||
|
mailing.Template = template;
|
||||||
|
|
||||||
|
return mailing;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<List<Mailing>> GetAllAsync(bool activeOnly = true)
|
public async Task<List<Mailing>> GetAllAsync(bool activeOnly = true)
|
||||||
@ -42,14 +58,57 @@ namespace Surge365.MassEmailReact.Infrastructure.Repositories
|
|||||||
ArgumentNullException.ThrowIfNull(ConnectionString);
|
ArgumentNullException.ThrowIfNull(ConnectionString);
|
||||||
|
|
||||||
using SqlConnection conn = new SqlConnection(ConnectionString);
|
using SqlConnection conn = new SqlConnection(ConnectionString);
|
||||||
return (await conn.QueryAsync<Mailing>("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<Mailing>()).ToList();
|
||||||
|
if (!mailings.Any()) return mailings;
|
||||||
|
|
||||||
|
var templates = (await multi.ReadAsync<MailingTemplate>()).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<List<Mailing>> GetByStatusAsync(string codes, string? startDate, string? endDate)
|
public async Task<List<Mailing>> GetByStatusAsync(string codes, string? startDate, string? endDate)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(ConnectionString);
|
ArgumentNullException.ThrowIfNull(ConnectionString);
|
||||||
|
|
||||||
using SqlConnection conn = new SqlConnection(ConnectionString);
|
using SqlConnection conn = new SqlConnection(ConnectionString);
|
||||||
return (await conn.QueryAsync<Mailing>("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<Mailing>()).ToList();
|
||||||
|
if (!mailings.Any()) return mailings;
|
||||||
|
|
||||||
|
var templates = (await multi.ReadAsync<MailingTemplate>()).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<List<MailingStatistic>> GetStatisticsByStatusAsync(string codes, string? startDate, string? endDate)
|
public async Task<List<MailingStatistic>> GetStatisticsByStatusAsync(string codes, string? startDate, string? endDate)
|
||||||
{
|
{
|
||||||
@ -65,6 +124,27 @@ namespace Surge365.MassEmailReact.Infrastructure.Repositories
|
|||||||
using SqlConnection conn = new SqlConnection(ConnectionString);
|
using SqlConnection conn = new SqlConnection(ConnectionString);
|
||||||
return (await conn.QueryAsync<MailingStatistic>("mem_get_blast_statistic_by_blast", new { blast_key = id }, commandType: CommandType.StoredProcedure)).FirstOrDefault();
|
return (await conn.QueryAsync<MailingStatistic>("mem_get_blast_statistic_by_blast", new { blast_key = id }, commandType: CommandType.StoredProcedure)).FirstOrDefault();
|
||||||
}
|
}
|
||||||
|
public async Task<List<MailingEmail>> GetEmailsByIdAsync(int id)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(ConnectionString);
|
||||||
|
|
||||||
|
using SqlConnection conn = new SqlConnection(ConnectionString);
|
||||||
|
return (await conn.QueryAsync<MailingEmail>("mem_get_blast_email_by_blast_id", new { blast_key = id }, commandType: CommandType.StoredProcedure)).ToList();
|
||||||
|
}
|
||||||
|
public async Task<MailingTemplate?> GetTemplateByIdAsync(int id)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(ConnectionString);
|
||||||
|
|
||||||
|
using SqlConnection conn = new SqlConnection(ConnectionString);
|
||||||
|
return (await conn.QueryAsync<MailingTemplate>("mem_get_blast_template_by_blast_id", new { blast_key = id }, commandType: CommandType.StoredProcedure)).FirstOrDefault();
|
||||||
|
}
|
||||||
|
public async Task<MailingTarget?> GetTargetByIdAsync(int id)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(ConnectionString);
|
||||||
|
|
||||||
|
using SqlConnection conn = new SqlConnection(ConnectionString);
|
||||||
|
return (await conn.QueryAsync<MailingTarget>("mem_get_blast_target_by_blast_id", new { blast_key = id }, commandType: CommandType.StoredProcedure)).FirstOrDefault();
|
||||||
|
}
|
||||||
public async Task<bool> NameIsAvailableAsync(int? id, string name)
|
public async Task<bool> NameIsAvailableAsync(int? id, string name)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(ConnectionString);
|
ArgumentNullException.ThrowIfNull(ConnectionString);
|
||||||
@ -111,9 +191,9 @@ namespace Surge365.MassEmailReact.Infrastructure.Repositories
|
|||||||
parameters.Add("@blast_status_code", mailing.StatusCode, DbType.String);
|
parameters.Add("@blast_status_code", mailing.StatusCode, DbType.String);
|
||||||
parameters.Add("@schedule_date", mailing.ScheduleDate, DbType.DateTime);
|
parameters.Add("@schedule_date", mailing.ScheduleDate, DbType.DateTime);
|
||||||
parameters.Add("@sent_date", mailing.SentDate, 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("@blast_recurring_type_code", mailing.RecurringTypeCode, DbType.String);
|
||||||
parameters.Add("@recurring_start_date", mailing.RecurringStartDate, DbType.DateTime);
|
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);
|
parameters.Add("@success", dbType: DbType.Boolean, direction: ParameterDirection.Output);
|
||||||
|
|
||||||
await conn.ExecuteAsync("mem_save_blast", parameters, commandType: CommandType.StoredProcedure);
|
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("@blast_status_code", mailing.StatusCode, DbType.String);
|
||||||
parameters.Add("@schedule_date", mailing.ScheduleDate, DbType.DateTime);
|
parameters.Add("@schedule_date", mailing.ScheduleDate, DbType.DateTime);
|
||||||
parameters.Add("@sent_date", mailing.SentDate, 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("@blast_recurring_type_code", mailing.RecurringTypeCode, DbType.String);
|
||||||
parameters.Add("@recurring_start_date", mailing.RecurringStartDate, DbType.DateTime);
|
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);
|
parameters.Add("@success", dbType: DbType.Boolean, direction: ParameterDirection.Output);
|
||||||
|
|
||||||
await conn.ExecuteAsync("mem_save_blast", parameters, commandType: CommandType.StoredProcedure);
|
await conn.ExecuteAsync("mem_save_blast", parameters, commandType: CommandType.StoredProcedure);
|
||||||
|
|||||||
@ -73,6 +73,18 @@ namespace Surge365.MassEmailReact.Infrastructure.Services
|
|||||||
{
|
{
|
||||||
return await _mailingRepository.GetStatisticByIdAsync(id);
|
return await _mailingRepository.GetStatisticByIdAsync(id);
|
||||||
}
|
}
|
||||||
|
public async Task<List<MailingEmail>> GetEmailsByIdAsync(int id)
|
||||||
|
{
|
||||||
|
return await _mailingRepository.GetEmailsByIdAsync(id);
|
||||||
|
}
|
||||||
|
public async Task<MailingTemplate?> GetTemplateByIdAsync(int id)
|
||||||
|
{
|
||||||
|
return await _mailingRepository.GetTemplateByIdAsync(id);
|
||||||
|
}
|
||||||
|
public async Task<MailingTarget?> GetTargetByIdAsync(int id)
|
||||||
|
{
|
||||||
|
return await _mailingRepository.GetTargetByIdAsync(id);
|
||||||
|
}
|
||||||
public async Task<bool> NameIsAvailableAsync(int? id, string name)
|
public async Task<bool> NameIsAvailableAsync(int? id, string name)
|
||||||
{
|
{
|
||||||
return await _mailingRepository.NameIsAvailableAsync(id, name);
|
return await _mailingRepository.NameIsAvailableAsync(id, name);
|
||||||
@ -96,9 +108,14 @@ namespace Surge365.MassEmailReact.Infrastructure.Services
|
|||||||
StatusCode = mailingDto.StatusCode,
|
StatusCode = mailingDto.StatusCode,
|
||||||
ScheduleDate = mailingDto.ScheduleDate,
|
ScheduleDate = mailingDto.ScheduleDate,
|
||||||
SentDate = mailingDto.SentDate,
|
SentDate = mailingDto.SentDate,
|
||||||
SessionActivityId = mailingDto.SessionActivityId,
|
|
||||||
RecurringTypeCode = mailingDto.RecurringTypeCode,
|
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);
|
return await _mailingRepository.CreateAsync(mailing);
|
||||||
@ -119,9 +136,14 @@ namespace Surge365.MassEmailReact.Infrastructure.Services
|
|||||||
mailing.StatusCode = mailingDto.StatusCode;
|
mailing.StatusCode = mailingDto.StatusCode;
|
||||||
mailing.ScheduleDate = mailingDto.ScheduleDate;
|
mailing.ScheduleDate = mailingDto.ScheduleDate;
|
||||||
mailing.SentDate = mailingDto.SentDate;
|
mailing.SentDate = mailingDto.SentDate;
|
||||||
mailing.SessionActivityId = mailingDto.SessionActivityId;
|
|
||||||
mailing.RecurringTypeCode = mailingDto.RecurringTypeCode;
|
mailing.RecurringTypeCode = mailingDto.RecurringTypeCode;
|
||||||
mailing.RecurringStartDate = mailingDto.RecurringStartDate;
|
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);
|
return await _mailingRepository.UpdateAsync(mailing);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -20,6 +20,8 @@ import VisibilityIcon from '@mui/icons-material/Visibility';
|
|||||||
import Template from "@/types/template";
|
import Template from "@/types/template";
|
||||||
import Mailing from "@/types/mailing";
|
import Mailing from "@/types/mailing";
|
||||||
import Target from "@/types/target";
|
import Target from "@/types/target";
|
||||||
|
//import MailingTemplate from "@/types/mailingTemplate";
|
||||||
|
//import MailingTarget from "@/types/mailingTarget";
|
||||||
import EmailList from "@/components/forms/EmailList";
|
import EmailList from "@/components/forms/EmailList";
|
||||||
import TestEmailList from "@/types/testEmailList";
|
import TestEmailList from "@/types/testEmailList";
|
||||||
import TemplateViewer from "@/components/modals/TemplateViewer"
|
import TemplateViewer from "@/components/modals/TemplateViewer"
|
||||||
@ -133,12 +135,39 @@ const schema = yup.object().shape({
|
|||||||
})
|
})
|
||||||
: schema.nullable();
|
: schema.nullable();
|
||||||
}),
|
}),
|
||||||
|
template: yup.object().shape({
|
||||||
//.when("recurringTypeCode", {
|
id: yup.number().nullable().default(0),
|
||||||
//is: (value: string) => value !== "" && value !== null, // String comparison for "None"
|
mailingId: yup.number().default(0),
|
||||||
//then: (schema) => schema.required("Recurring start date is required when recurring type is set"),
|
name: yup.string().default(""),
|
||||||
//otherwise: (schema) => schema.nullable(),
|
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) => {
|
const nameIsAvailable = async (id: number, name: string) => {
|
||||||
@ -164,6 +193,33 @@ const defaultMailing: Mailing = {
|
|||||||
sessionActivityId: null,
|
sessionActivityId: null,
|
||||||
recurringTypeCode: null,
|
recurringTypeCode: null,
|
||||||
recurringStartDate: 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) => {
|
const MailingEdit = ({ open, mailing, onClose, onSave }: MailingEditProps) => {
|
||||||
@ -179,7 +235,7 @@ const MailingEdit = ({ open, mailing, onClose, onSave }: MailingEditProps) => {
|
|||||||
const [TargetSampleModalOpen, setTargetSampleModalOpen] = useState<boolean>(false);
|
const [TargetSampleModalOpen, setTargetSampleModalOpen] = useState<boolean>(false);
|
||||||
const [currentTarget, setCurrentTarget] = useState<Target | null>(null);
|
const [currentTarget, setCurrentTarget] = useState<Target | null>(null);
|
||||||
|
|
||||||
const { register, trigger, control, handleSubmit, reset, formState: { errors } } = useForm<Mailing>({
|
const { register, trigger, control, handleSubmit, reset, setValue, formState: { errors } } = useForm<Mailing>({
|
||||||
mode: "onBlur",
|
mode: "onBlur",
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
...(mailing || defaultMailing),
|
...(mailing || defaultMailing),
|
||||||
@ -308,8 +364,8 @@ const MailingEdit = ({ open, mailing, onClose, onSave }: MailingEditProps) => {
|
|||||||
throw new Error("Failed to test mailing");
|
throw new Error("Failed to test mailing");
|
||||||
}
|
}
|
||||||
|
|
||||||
toast.success("Test mailing sent successfully");
|
toast.success("Test mailing(s) sent successfully");
|
||||||
console.log("Test mailing sent successfully");
|
console.log("Test mailing(s) sent successfully");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Test mailing error:", error);
|
console.error("Test mailing error:", error);
|
||||||
toast.error("Failed to send test mailing");
|
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 filteredTargets = setupData.targets.filter(t => t.isActive);
|
||||||
const filteredTemplates = setupData.templates.filter(t => t.isActive);
|
const filteredTemplates = setupData.templates.filter(t => t.isActive);
|
||||||
|
const filteredEmailDomains = setupData.emailDomains.filter((domain) => domain.isActive);
|
||||||
return (
|
return (
|
||||||
<LocalizationProvider dateAdapter={AdapterDayjs}> {/* Wrap with LocalizationProvider */}
|
<LocalizationProvider dateAdapter={AdapterDayjs}> {/* Wrap with LocalizationProvider */}
|
||||||
<Dialog open={open} onClose={(_, reason) => { onClose(reason); }} maxWidth="sm" fullWidth disableEscapeKeyDown >
|
<Dialog open={open} onClose={(_, reason) => { onClose(reason); }} maxWidth="sm" fullWidth disableEscapeKeyDown >
|
||||||
@ -377,6 +434,16 @@ const MailingEdit = ({ open, mailing, onClose, onSave }: MailingEditProps) => {
|
|||||||
field.onChange(newValue ? newValue.id : null);
|
field.onChange(newValue ? newValue.id : null);
|
||||||
trigger("templateId");
|
trigger("templateId");
|
||||||
setCurrentTemplate(newValue);
|
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) => (
|
renderInput={(params) => (
|
||||||
<TextField
|
<TextField
|
||||||
@ -403,6 +470,67 @@ const MailingEdit = ({ open, mailing, onClose, onSave }: MailingEditProps) => {
|
|||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
|
{/* Add domainId Autocomplete */}
|
||||||
|
<Controller
|
||||||
|
name="template.domainId"
|
||||||
|
control={control}
|
||||||
|
render={({ field }) => (
|
||||||
|
<Autocomplete
|
||||||
|
{...field}
|
||||||
|
options={filteredEmailDomains}
|
||||||
|
getOptionLabel={(option) => option.name}
|
||||||
|
value={filteredEmailDomains.find((d) => d.id === field.value) || null}
|
||||||
|
onChange={(_, newValue) => {
|
||||||
|
field.onChange(newValue ? newValue.id : null);
|
||||||
|
trigger("template.domainId");
|
||||||
|
}}
|
||||||
|
renderInput={(params) => (
|
||||||
|
<TextField
|
||||||
|
{...params}
|
||||||
|
label="Domain"
|
||||||
|
fullWidth
|
||||||
|
margin="dense"
|
||||||
|
error={!!errors.template?.domainId}
|
||||||
|
helperText={errors.template?.domainId?.message}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Controller
|
||||||
|
name="template.subject"
|
||||||
|
control={control}
|
||||||
|
render={({ field }) => (
|
||||||
|
<TextField
|
||||||
|
{...field}
|
||||||
|
label="Subject"
|
||||||
|
fullWidth
|
||||||
|
margin="dense"
|
||||||
|
error={!!errors.template?.subject}
|
||||||
|
helperText={errors.template?.subject?.message}
|
||||||
|
onChange={(e) => field.onChange(e.target.value)} // Ensure value updates
|
||||||
|
value={field.value ?? ""} // Ensure controlled value
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Controller
|
||||||
|
name="template.fromName"
|
||||||
|
control={control}
|
||||||
|
render={({ field }) => (
|
||||||
|
<TextField
|
||||||
|
{...field}
|
||||||
|
label="To Name"
|
||||||
|
fullWidth
|
||||||
|
margin="dense"
|
||||||
|
error={!!errors.template?.fromName}
|
||||||
|
helperText={errors.template?.fromName?.message}
|
||||||
|
onChange={(e) => field.onChange(e.target.value)} // Ensure value updates
|
||||||
|
value={field.value ?? ""} // Ensure controlled value
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||||
<Controller
|
<Controller
|
||||||
|
|||||||
@ -1,11 +1,33 @@
|
|||||||
import { useState } from "react";
|
import { useState, useEffect } from 'react';
|
||||||
import { Dialog, DialogTitle, DialogContent, Typography, IconButton } from '@mui/material';
|
import {
|
||||||
import { useSetupData } from '@/context/SetupDataContext';
|
Dialog,
|
||||||
import Mailing from '@/types/mailing';
|
DialogTitle,
|
||||||
import VisibilityIcon from '@mui/icons-material/Visibility';
|
DialogContent,
|
||||||
import CloseIcon from '@mui/icons-material/Close';
|
Typography,
|
||||||
import TemplateViewer from "@/components/modals/TemplateViewer"
|
IconButton,
|
||||||
import TargetSampleModal from "@/components/modals/TargetSampleModal"
|
Box,
|
||||||
|
MenuItem,
|
||||||
|
Select,
|
||||||
|
FormControl,
|
||||||
|
InputLabel,
|
||||||
|
} from "@mui/material";
|
||||||
|
import {
|
||||||
|
DataGrid,
|
||||||
|
GridColDef,
|
||||||
|
GridToolbarContainer,
|
||||||
|
GridToolbarQuickFilter,
|
||||||
|
GridToolbarExport,
|
||||||
|
GridToolbarDensitySelector,
|
||||||
|
GridToolbarColumnsButton,
|
||||||
|
} from "@mui/x-data-grid";
|
||||||
|
import { useSetupData } from "@/context/SetupDataContext";
|
||||||
|
import Mailing from "@/types/mailing";
|
||||||
|
import VisibilityIcon from "@mui/icons-material/Visibility";
|
||||||
|
import CloseIcon from "@mui/icons-material/Close";
|
||||||
|
import TemplateViewer from "@/components/modals/TemplateViewer";
|
||||||
|
import TargetSampleModal from "@/components/modals/TargetSampleModal";
|
||||||
|
import Target from "@/types/target";
|
||||||
|
import Template from "@/types/template";
|
||||||
|
|
||||||
interface MailingViewProps {
|
interface MailingViewProps {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
@ -13,49 +35,115 @@ interface MailingViewProps {
|
|||||||
onClose: () => void;
|
onClose: () => 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) {
|
function MailingView({ open, mailing, onClose }: MailingViewProps) {
|
||||||
const setupData = useSetupData();
|
const setupData = useSetupData();
|
||||||
const [templateViewerOpen, setTemplateViewerOpen] = useState<boolean>(false);
|
const [templateViewerOpen, setTemplateViewerOpen] = useState<boolean>(false);
|
||||||
const [TargetSampleModalOpen, setTargetSampleModalOpen] = useState<boolean>(false);
|
const [targetSampleModalOpen, setTargetSampleModalOpen] = useState<boolean>(false);
|
||||||
|
const [emails, setEmails] = useState<MailingEmail[]>([]);
|
||||||
|
const [loading, setLoading] = useState<boolean>(false);
|
||||||
|
const [statusFilter, setStatusFilter] = useState<string>("");
|
||||||
|
|
||||||
|
// 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;
|
if (!mailing) return null;
|
||||||
|
|
||||||
// Look up related data from setupData
|
// Look up related data from setupData
|
||||||
const template = setupData.templates.find(t => t.id === mailing.templateId);
|
const template = mailing.template ?? setupData.templates.find((t) => t.id === mailing.templateId); //TODO: Pull from mailing.Template if status is not "ED"
|
||||||
const target = setupData.targets.find(t => t.id === mailing.targetId);
|
const target = mailing.target ?? setupData.targets.find((t) => t.id === mailing.targetId);
|
||||||
const domain = template ? setupData.emailDomains.find(d => d.id === template.domainId) : null;
|
const domain = template ? setupData.emailDomains.find((d) => d.id === template.domainId) : null;
|
||||||
|
|
||||||
// Format status string
|
// Status mappings
|
||||||
const statusString = mailing.scheduleDate
|
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",
|
||||||
|
};
|
||||||
|
|
||||||
|
// 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()}`
|
? `Scheduled for ${new Date(mailing.scheduleDate).toLocaleString()}`
|
||||||
: mailing.sentDate
|
: "Waiting to Send",
|
||||||
? `Sent on ${new Date(mailing.sentDate).toLocaleString()}`
|
}[mailing.statusCode.trim().toUpperCase()] || "Unknown";
|
||||||
: 'N/A';
|
|
||||||
|
|
||||||
// Helper function to format recurring string (customize based on your recurringTypeCode logic)
|
// Helper function to format recurring string
|
||||||
const formatRecurringString = (typeCode: string, startDate: string): string => {
|
const formatRecurringString = (typeCode: string, startDate: string): string => {
|
||||||
const date = new Date(startDate);
|
const date = new Date(startDate);
|
||||||
switch (typeCode.toUpperCase()) {
|
switch (typeCode.toUpperCase()) {
|
||||||
case 'D':
|
case "D":
|
||||||
return `Daily at ${date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}`;
|
return `Daily at ${date.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })}`;
|
||||||
case 'W':
|
case "W":
|
||||||
return `Weekly on ${date.toLocaleDateString('en-US', { weekday: 'long' })} at ${date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}`;
|
return `Weekly on ${date.toLocaleDateString("en-US", { weekday: "long" })} at ${date.toLocaleTimeString([], {
|
||||||
case 'M':
|
hour: "2-digit",
|
||||||
return `Monthly on day ${date.getDate()} at ${date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}`;
|
minute: "2-digit",
|
||||||
|
})}`;
|
||||||
|
case "M":
|
||||||
|
return `Monthly on day ${date.getDate()} at ${date.toLocaleTimeString([], {
|
||||||
|
hour: "2-digit",
|
||||||
|
minute: "2-digit",
|
||||||
|
})}`;
|
||||||
default:
|
default:
|
||||||
return 'Custom recurring schedule';
|
return "Custom recurring schedule";
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Format recurring string
|
// Format recurring string
|
||||||
const recurringString = mailing.recurringTypeCode && mailing.recurringStartDate
|
const recurringString =
|
||||||
|
mailing.recurringTypeCode && mailing.recurringStartDate
|
||||||
? formatRecurringString(mailing.recurringTypeCode, mailing.recurringStartDate)
|
? formatRecurringString(mailing.recurringTypeCode, mailing.recurringStartDate)
|
||||||
: 'No';
|
: "No";
|
||||||
|
|
||||||
// Navigation handlers for viewing related entities
|
// Navigation handlers for viewing related entities
|
||||||
const handleViewTarget = () => {
|
const handleViewTarget = () => {
|
||||||
if (target) {
|
if (target) {
|
||||||
setTargetSampleModalOpen(!TargetSampleModalOpen);
|
setTargetSampleModalOpen(!targetSampleModalOpen);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -65,15 +153,62 @@ function MailingView({ open, mailing, onClose }: MailingViewProps) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Grid columns
|
||||||
|
const columns: GridColDef<MailingEmail>[] = [
|
||||||
|
{
|
||||||
|
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 = () => (
|
||||||
|
<GridToolbarContainer sx={{ display: "flex", alignItems: "center" }}>
|
||||||
|
<FormControl sx={{ minWidth: 150, mr: 2, mt: 1 }} size="small">
|
||||||
|
<InputLabel >Status</InputLabel>
|
||||||
|
<Select
|
||||||
|
value={statusFilter}
|
||||||
|
onChange={(e) => setStatusFilter(e.target.value)}
|
||||||
|
label="Status"
|
||||||
|
margin="dense"
|
||||||
|
>
|
||||||
|
<MenuItem value="">All</MenuItem>
|
||||||
|
{uniqueStatusCodes.map((code) => (
|
||||||
|
<MenuItem key={code} value={code}>
|
||||||
|
{statusMap[code]}
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
<GridToolbarColumnsButton />
|
||||||
|
<GridToolbarDensitySelector />
|
||||||
|
<GridToolbarExport />
|
||||||
|
<GridToolbarQuickFilter sx={{ ml: "auto" }} />
|
||||||
|
</GridToolbarContainer>
|
||||||
|
);
|
||||||
|
|
||||||
|
// Filter emails based on status
|
||||||
|
const filteredEmails = statusFilter
|
||||||
|
? emails.filter((email) => email.statusCode.trim() === statusFilter)
|
||||||
|
: emails;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onClose={onClose} maxWidth="sm" fullWidth>
|
<Dialog open={open} onClose={onClose} maxWidth="md" fullWidth>
|
||||||
<DialogTitle>
|
<DialogTitle>
|
||||||
{mailing.name}
|
{mailing.name}
|
||||||
<IconButton
|
<IconButton
|
||||||
aria-label="close"
|
aria-label="close"
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
sx={{
|
sx={{
|
||||||
position: 'absolute',
|
position: "absolute",
|
||||||
right: 8,
|
right: 8,
|
||||||
top: 8,
|
top: 8,
|
||||||
color: (theme) => theme.palette.grey[500],
|
color: (theme) => theme.palette.grey[500],
|
||||||
@ -84,7 +219,7 @@ function MailingView({ open, mailing, onClose }: MailingViewProps) {
|
|||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<Typography sx={{ mb: 1, height: "20px" }}>
|
<Typography sx={{ mb: 1, height: "20px" }}>
|
||||||
Target Name: {target?.name || 'Unknown'}
|
Target Name: {target?.name || "Unknown"}
|
||||||
{target && (
|
{target && (
|
||||||
<IconButton onClick={handleViewTarget} sx={{ ml: 1, height: "100%" }}>
|
<IconButton onClick={handleViewTarget} sx={{ ml: 1, height: "100%" }}>
|
||||||
<VisibilityIcon />
|
<VisibilityIcon />
|
||||||
@ -93,7 +228,7 @@ function MailingView({ open, mailing, onClose }: MailingViewProps) {
|
|||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
<Typography sx={{ mb: 1, height: "20px" }}>
|
<Typography sx={{ mb: 1, height: "20px" }}>
|
||||||
Template Name: {template?.name || 'Unknown'}
|
Template Name: {template?.name || "Unknown"}
|
||||||
{template && (
|
{template && (
|
||||||
<IconButton onClick={handleViewTemplate} sx={{ ml: 1, height: "100%" }}>
|
<IconButton onClick={handleViewTemplate} sx={{ ml: 1, height: "100%" }}>
|
||||||
<VisibilityIcon />
|
<VisibilityIcon />
|
||||||
@ -101,24 +236,49 @@ function MailingView({ open, mailing, onClose }: MailingViewProps) {
|
|||||||
)}
|
)}
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
<Typography sx={{ mb: 1, height: "20px" }}>From Name: {template?.fromName || 'N/A'}</Typography>
|
<Typography sx={{ mb: 1, height: "20px" }}>From Name: {template?.fromName || "N/A"}</Typography>
|
||||||
<Typography sx={{ mb: 1, height: "20px" }}>Domain: {domain?.name || 'N/A'}</Typography> {/* Assuming EmailDomain has a domainName field */}
|
<Typography sx={{ mb: 1, height: "20px" }}>Domain: {domain?.name || "N/A"}</Typography>
|
||||||
<Typography sx={{ mb: 1, height: "20px" }}>Subject: {template?.subject || 'N/A'}</Typography>
|
<Typography sx={{ mb: 1, height: "20px" }}>Subject: {template?.subject || "N/A"}</Typography>
|
||||||
<Typography sx={{ mb: 1, height: "20px" }}>Status: {statusString}</Typography>
|
<Typography sx={{ mb: 1, height: "20px" }}>Status: {statusString}</Typography>
|
||||||
<Typography sx={{ mb: 1, height: "20px" }}>Recurring: {recurringString}</Typography>
|
<Typography sx={{ mb: 2, height: "20px" }}>Recurring: {recurringString}</Typography>
|
||||||
|
|
||||||
|
<Box sx={{ height: 600, mt: 2 }}>
|
||||||
|
<DataGrid
|
||||||
|
rows={filteredEmails}
|
||||||
|
columns={columns}
|
||||||
|
getRowId={(row) => 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" }}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
|
||||||
{templateViewerOpen && (
|
{templateViewerOpen && (
|
||||||
<TemplateViewer
|
<TemplateViewer
|
||||||
open={templateViewerOpen}
|
open={templateViewerOpen}
|
||||||
template={template!}
|
template={template as Template}
|
||||||
onClose={() => { setTemplateViewerOpen(false) }}
|
onClose={() => setTemplateViewerOpen(false)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{TargetSampleModalOpen && (
|
{targetSampleModalOpen && (
|
||||||
<TargetSampleModal
|
<TargetSampleModal
|
||||||
open={TargetSampleModalOpen}
|
open={targetSampleModalOpen}
|
||||||
target={target!}
|
target={target as Target}
|
||||||
onClose={() => { setTargetSampleModalOpen(false) }}
|
onClose={() => setTargetSampleModalOpen(false)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
|
|||||||
@ -1,3 +1,5 @@
|
|||||||
|
import MailingTemplate from '@/types/mailingTemplate';
|
||||||
|
import MailingTarget from '@/types/mailingTarget';
|
||||||
export interface Mailing {
|
export interface Mailing {
|
||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
@ -10,6 +12,8 @@ export interface Mailing {
|
|||||||
sessionActivityId: string | null;
|
sessionActivityId: string | null;
|
||||||
recurringTypeCode: string | null;
|
recurringTypeCode: string | null;
|
||||||
recurringStartDate: string | null;
|
recurringStartDate: string | null;
|
||||||
|
template: MailingTemplate | null;
|
||||||
|
target: MailingTarget | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Mailing;
|
export default Mailing;
|
||||||
14
Surge365.MassEmailReact.Web/src/types/mailingTarget.ts
Normal file
14
Surge365.MassEmailReact.Web/src/types/mailingTarget.ts
Normal file
@ -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;
|
||||||
18
Surge365.MassEmailReact.Web/src/types/mailingTemplate.ts
Normal file
18
Surge365.MassEmailReact.Web/src/types/mailingTemplate.ts
Normal file
@ -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;
|
||||||
Loading…
x
Reference in New Issue
Block a user