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:
David Headrick 2025-04-14 16:09:07 -05:00
parent 9703517974
commit 51c267e48f
21 changed files with 732 additions and 71 deletions

View File

@ -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)
{ {

View File

@ -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; } = "";
}
}

View File

@ -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();
}
} }

View File

@ -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);

View File

@ -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);

View File

@ -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);
} }
} }
} }

View 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);
}
}
}

View 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);
}
}
}

View 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);
}
}
}

View File

@ -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());
}); });

View File

@ -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");
}
}
}

View File

@ -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");
} }

View File

@ -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");
}
}
}

View File

@ -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");
}
}
}

View File

@ -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);

View File

@ -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);
} }

View File

@ -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

View File

@ -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>

View File

@ -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;

View 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;

View 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;