Add mailing features and update notification system
- Updated `MailingsController` with new endpoints for name availability and next available name. - Modified existing methods to accept optional date parameters. - Introduced `TestMailingDto` for testing mailings with email addresses. - Updated `IMailingRepository` and `IMailingService` interfaces for new methods. - Enhanced `MailingRepository` and `MailingService` to support new logic for testing and sending emails. - Updated `appsettings.json` with new SendGrid settings and default unsubscribe URL. - Modified `MailingEdit` component to fetch next available name and handle test mailings. - Added search filters for start and end dates in `CompletedMailings` component. - Included `react-toastify` for notifications in `package.json` and `package-lock.json`. - Updated `App` component to include `ToastContainer` for displaying notifications.
This commit is contained in:
parent
035a2e1dae
commit
26abe9e028
@ -29,20 +29,27 @@ namespace Surge365.MassEmailReact.API.Controllers
|
||||
public async Task<IActionResult> CheckNameAvailable([FromQuery] int? id, [FromQuery][Required] string name)
|
||||
{
|
||||
var available = await _mailingService.NameIsAvailableAsync(id, name);
|
||||
return Ok(available);
|
||||
return Ok(new { available });
|
||||
}
|
||||
|
||||
[HttpGet("nextavailablename")]
|
||||
public async Task<IActionResult> GetNextAvailableName([FromQuery] int? id, [FromQuery][Required] string name)
|
||||
{
|
||||
var nextAvailableName = await _mailingService.GetNextAvailableNameAsync(id, name);
|
||||
return Ok(new { name = nextAvailableName });
|
||||
}
|
||||
|
||||
[HttpGet("status/{statusCode}")]
|
||||
public async Task<IActionResult> GetByStatus(string statusCode)
|
||||
public async Task<IActionResult> GetByStatus(string statusCode, [FromQuery] string? startDate, [FromQuery] string? endDate)
|
||||
{
|
||||
var mailings = await _mailingService.GetByStatusAsync(statusCode);
|
||||
var mailings = await _mailingService.GetByStatusAsync(statusCode, startDate, endDate);
|
||||
return Ok(mailings);
|
||||
}
|
||||
|
||||
[HttpGet("status/{statusCode}/stats")]
|
||||
public async Task<IActionResult> GetStatisticsByStatus(string statusCode)
|
||||
public async Task<IActionResult> GetStatisticsByStatus(string statusCode, [FromQuery] string? startDate, [FromQuery] string? endDate)
|
||||
{
|
||||
var mailings = await _mailingService.GetStatisticsByStatusAsync(statusCode);
|
||||
var mailings = await _mailingService.GetStatisticsByStatusAsync(statusCode, startDate, endDate);
|
||||
return Ok(mailings);
|
||||
}
|
||||
|
||||
@ -67,6 +74,12 @@ namespace Surge365.MassEmailReact.API.Controllers
|
||||
if (mailingUpdateDto.Id != null && mailingUpdateDto.Id > 0)
|
||||
return BadRequest("Id must be null or 0");
|
||||
|
||||
if (mailingUpdateDto.ScheduleDate.HasValue && mailingUpdateDto.ScheduleDate.Value.Kind == DateTimeKind.Utc)
|
||||
mailingUpdateDto.ScheduleDate = TimeZoneInfo.ConvertTimeFromUtc(mailingUpdateDto.ScheduleDate.Value, TimeZoneInfo.Local);
|
||||
|
||||
if (mailingUpdateDto.RecurringStartDate.HasValue && mailingUpdateDto.RecurringStartDate.Value.Kind == DateTimeKind.Utc)
|
||||
mailingUpdateDto.RecurringStartDate = TimeZoneInfo.ConvertTimeFromUtc(mailingUpdateDto.RecurringStartDate.Value, TimeZoneInfo.Local);
|
||||
|
||||
var mailingId = await _mailingService.CreateAsync(mailingUpdateDto);
|
||||
if (mailingId == null)
|
||||
return StatusCode(StatusCodes.Status500InternalServerError, "Failed to create mailing.");
|
||||
@ -109,5 +122,18 @@ namespace Surge365.MassEmailReact.API.Controllers
|
||||
var updatedMailing = await _mailingService.GetByIdAsync(id);
|
||||
return Ok(updatedMailing);
|
||||
}
|
||||
|
||||
[HttpPost("test")]
|
||||
public async Task<IActionResult> TestMailing([FromBody] TestMailingDto testMailingDto)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(testMailingDto);
|
||||
ArgumentNullException.ThrowIfNull(testMailingDto.EmailAddresses);
|
||||
ArgumentNullException.ThrowIfNull(testMailingDto.Mailing);
|
||||
|
||||
if (!await _mailingService.TestMailing(testMailingDto.Mailing, testMailingDto.EmailAddresses))
|
||||
return StatusCode(StatusCodes.Status500InternalServerError, "Failed to test mailing.");
|
||||
|
||||
return Ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -17,5 +17,10 @@
|
||||
"MassEmail.ConnectionString": "data source=uat.surge365.com;initial catalog=MassEmail;User ID=ytb;Password=YTB()nl!n3;Encrypt=yes;TrustServerCertificate=yes;Connection Timeout=3;Application Name=##application_name##;" //TODO: Move this to development.json, on server should go somewhere secure. GET IT OUT OF GIT
|
||||
},
|
||||
"TestTargetSql": "CREATE TABLE #columns\r\n(\r\n primary_key INT NOT NULL IDENTITY(1,1) PRIMARY KEY,\r\n name VARCHAR(255),\r\n data_type CHAR(1)\r\n)\r\nSELECT TOP 10 *\r\nINTO #list\r\nFROM ##database_name##..##view_name##\r\n##filter##\r\n\r\nDECLARE @row_count INT\r\nSELECT @row_count = COUNT(*)\r\nFROM ##database_name##..##view_name##\r\n##filter##\r\n\r\nDECLARE c_curs CURSOR FOR \r\nSELECT c.name AS column_name, t.name AS data_type\r\nFROM tempdb.sys.columns c\r\nINNER JOIN tempdb.sys.types t ON c.user_type_id = t.user_type_id\r\n AND t.name NOT IN ('text','ntext','image','binary','varbinary','image','cursor','timestamp','hierarchyid','sql_variant','xml','table')\r\nWHERE object_id = object_id('tempdb..#list') \r\n AND ((t.name IN ('char','varchar') AND c.max_length <= 255)\r\n OR (t.name IN ('nchar','nvarchar') AND c.max_length <= 510)\r\n OR (t.name NOT IN ('char','varchar','nchar','nvarchar')))\r\n \r\nOPEN c_curs\r\nDECLARE @column_name VARCHAR(255), @column_type VARCHAR(255)\r\n\r\nFETCH NEXT FROM c_curs INTO @column_name, @column_type\r\nWHILE(@@FETCH_STATUS = 0)\r\nBEGIN \r\n DECLARE @data_type CHAR(1) = 'S'\r\n IF(@column_type IN ('date','datetime','datetime2','datetimeoffset','smalldatetime','time'))\r\n BEGIN\r\n SET @data_type = 'D'\r\n END\r\n ELSE IF(@column_type IN ('bit'))\r\n BEGIN\r\n SET @data_type = 'B'\r\n END\r\n ELSE IF(@column_type IN ('bigint','numeric','smallint','decimal','smallmoney','int','tinyint','money','float','real'))\r\n BEGIN\r\n SET @data_type = 'N'\r\n END\r\n INSERT INTO #columns(name, data_type) VALUES(@column_name, @data_type)\r\n FETCH NEXT FROM c_curs INTO @column_name, @column_type\r\nEND\r\nCLOSE c_curs\r\nDEALLOCATE c_curs\r\nSELECT * FROM #columns ORDER BY primary_key\r\nSELECT * FROM #list\r\nSELECT @row_count AS row_count\r\nDROP TABLE #columns\r\nDROP TABLE #list",
|
||||
"ConnectionStringTemplate": "data source=##server_name##,##port##;initial catalog=##database_name##;User ID=##username##;Password=##password##;persist security info=False;packet size=4096;"
|
||||
"ConnectionStringTemplate": "data source=##server_name##,##port##;initial catalog=##database_name##;User ID=##username##;Password=##password##;persist security info=False;packet size=4096;",
|
||||
"DefaultUnsubscribeUrl": "http://emailopentracking.surge365.com/unsubscribe.htm",
|
||||
"SendGrid_TestMode": false,
|
||||
"RegularExpression_Email": "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$",
|
||||
"SendGrid_Url": "smtp.sendgrid.net",
|
||||
"SendGrid_Port": "587"
|
||||
}
|
||||
10
Surge365.MassEmailReact.Application/DTOs/TestMailingDto.cs
Normal file
10
Surge365.MassEmailReact.Application/DTOs/TestMailingDto.cs
Normal file
@ -0,0 +1,10 @@
|
||||
using System;
|
||||
|
||||
namespace Surge365.MassEmailReact.Domain.Entities
|
||||
{
|
||||
public class TestMailingDto
|
||||
{
|
||||
public MailingUpdateDto? Mailing { get; set; }
|
||||
public List<string> EmailAddresses { get; set; } = new List<string>();
|
||||
}
|
||||
}
|
||||
@ -8,11 +8,11 @@ namespace Surge365.MassEmailReact.Application.Interfaces
|
||||
{
|
||||
Task<Mailing?> GetByIdAsync(int id);
|
||||
Task<List<Mailing>> GetAllAsync(bool activeOnly = true);
|
||||
Task<List<Mailing>> GetByStatusAsync(string code);
|
||||
Task<List<MailingStatistic>> GetStatisticsByStatusAsync(string code);
|
||||
Task<List<Mailing>> GetByStatusAsync(string code, string? startDate, string? endDate);
|
||||
Task<List<MailingStatistic>> GetStatisticsByStatusAsync(string code, string? startDate, string? endDate);
|
||||
Task<MailingStatistic?> GetStatisticByIdAsync(int id);
|
||||
Task<bool> NameIsAvailableAsync(int? id, string name);
|
||||
|
||||
Task<string> GetNextAvailableNameAsync(int? id, string name);
|
||||
Task<int?> CreateAsync(Mailing mailing);
|
||||
Task<bool> UpdateAsync(Mailing mailing);
|
||||
Task<bool> CancelMailingAsync(int id);
|
||||
|
||||
@ -9,13 +9,15 @@ namespace Surge365.MassEmailReact.Application.Interfaces
|
||||
Task<Mailing?> GetByIdAsync(int id);
|
||||
Task<List<Mailing>> GetAllAsync(bool activeOnly = true);
|
||||
|
||||
Task<List<Mailing>> GetByStatusAsync(string code);
|
||||
Task<List<MailingStatistic>> GetStatisticsByStatusAsync(string code);
|
||||
Task<List<Mailing>> GetByStatusAsync(string code, string? startDate, string? endDate);
|
||||
Task<List<MailingStatistic>> GetStatisticsByStatusAsync(string code, string? startDate, string? endDate);
|
||||
Task<MailingStatistic?> GetStatisticByIdAsync(int id);
|
||||
Task<bool> NameIsAvailableAsync(int? id, string name);
|
||||
Task<string> GetNextAvailableNameAsync(int? id, string name);
|
||||
|
||||
Task<int?> CreateAsync(MailingUpdateDto mailingDto);
|
||||
Task<bool> UpdateAsync(MailingUpdateDto mailingDto);
|
||||
Task<bool> CancelMailingAsync(int id);
|
||||
Task<bool> TestMailing(MailingUpdateDto mailingUpdateDto, List<string> emails);
|
||||
}
|
||||
}
|
||||
@ -44,19 +44,19 @@ namespace Surge365.MassEmailReact.Infrastructure.Repositories
|
||||
using SqlConnection conn = new SqlConnection(ConnectionString);
|
||||
return (await conn.QueryAsync<Mailing>("mem_get_blast_all", new { active_only = activeOnly }, commandType: CommandType.StoredProcedure)).ToList();
|
||||
}
|
||||
public async Task<List<Mailing>> GetByStatusAsync(string code)
|
||||
public async Task<List<Mailing>> GetByStatusAsync(string code, string? startDate, string? endDate)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(ConnectionString);
|
||||
|
||||
using SqlConnection conn = new SqlConnection(ConnectionString);
|
||||
return (await conn.QueryAsync<Mailing>("mem_get_blast_by_status", new { blast_status_code = code }, commandType: CommandType.StoredProcedure)).ToList();
|
||||
return (await conn.QueryAsync<Mailing>("mem_get_blast_by_status", new { blast_status_code = code, start_date = startDate, end_date = endDate }, commandType: CommandType.StoredProcedure)).ToList();
|
||||
}
|
||||
public async Task<List<MailingStatistic>> GetStatisticsByStatusAsync(string code)
|
||||
public async Task<List<MailingStatistic>> GetStatisticsByStatusAsync(string code, string? startDate, string? endDate)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(ConnectionString);
|
||||
|
||||
using SqlConnection conn = new SqlConnection(ConnectionString);
|
||||
return (await conn.QueryAsync<MailingStatistic>("mem_get_blast_statistic_by_status", new { blast_status_code = code }, commandType: CommandType.StoredProcedure)).ToList();
|
||||
return (await conn.QueryAsync<MailingStatistic>("mem_get_blast_statistic_by_status", new { blast_status_code = code, start_date = startDate, end_date = endDate }, commandType: CommandType.StoredProcedure)).ToList();
|
||||
}
|
||||
public async Task<MailingStatistic?> GetStatisticByIdAsync(int id)
|
||||
{
|
||||
@ -75,11 +75,24 @@ namespace Surge365.MassEmailReact.Infrastructure.Repositories
|
||||
parameters.Add("@blast_name", name, DbType.String);
|
||||
parameters.Add("@available", dbType: DbType.Boolean, direction: ParameterDirection.Output);
|
||||
|
||||
await conn.ExecuteAsync("mem_is_blast_name_available", parameters, commandType: CommandType.StoredProcedure);
|
||||
await conn.ExecuteAsync("mem_is_blast_name_available2", parameters, commandType: CommandType.StoredProcedure);
|
||||
|
||||
return parameters.Get<bool>("@available");
|
||||
}
|
||||
public async Task<string> GetNextAvailableNameAsync(int? id, string name)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(ConnectionString);
|
||||
using var conn = new SqlConnection(ConnectionString);
|
||||
|
||||
var parameters = new DynamicParameters();
|
||||
parameters.Add("@blast_key", id, DbType.Int32);
|
||||
parameters.Add("@blast_name", name, DbType.String);
|
||||
parameters.Add("@next_blast_name", dbType: DbType.String, size:-1, direction: ParameterDirection.Output);
|
||||
|
||||
await conn.ExecuteAsync("mem_get_next_available_blast_name", parameters, commandType: CommandType.StoredProcedure);
|
||||
|
||||
return parameters.Get<string>("@next_blast_name");
|
||||
}
|
||||
|
||||
public async Task<int?> CreateAsync(Mailing mailing)
|
||||
{
|
||||
|
||||
@ -1,20 +1,66 @@
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Surge365.MassEmailReact.Application.DTOs;
|
||||
using Surge365.MassEmailReact.Application.Interfaces;
|
||||
using Surge365.MassEmailReact.Domain.Entities;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Data;
|
||||
using System.Net.Mail;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using SendGrid;
|
||||
using SendGrid.Helpers.Mail;
|
||||
|
||||
namespace Surge365.MassEmailReact.Infrastructure.Services
|
||||
{
|
||||
public class MailingService : IMailingService
|
||||
{
|
||||
private readonly ITargetService _targetService;
|
||||
private readonly ITemplateService _templateService;
|
||||
private readonly IEmailDomainService _emailDomainService;
|
||||
private readonly IMailingRepository _mailingRepository;
|
||||
private readonly IConfiguration _config;
|
||||
|
||||
public MailingService(IMailingRepository mailingRepository, IConfiguration config)
|
||||
private string DefaultUnsubscribeUrl
|
||||
{
|
||||
get
|
||||
{
|
||||
return _config["DefaultUnsubscribeUrl"] ?? "";
|
||||
}
|
||||
}
|
||||
private bool SendGridTestMode
|
||||
{
|
||||
get
|
||||
{
|
||||
return _config.GetValue<bool>("SendGrid_TestMode");
|
||||
}
|
||||
}
|
||||
private string EmailRegex
|
||||
{
|
||||
get
|
||||
{
|
||||
return _config["RegularExpression_Email"] ?? "";
|
||||
}
|
||||
}
|
||||
private string SendGridUrl
|
||||
{
|
||||
get
|
||||
{
|
||||
return _config["SendGrid_Url"] ?? "";
|
||||
}
|
||||
}
|
||||
private int SendGridPort
|
||||
{
|
||||
get
|
||||
{
|
||||
return _config.GetValue<int>("SendGrid_Port");
|
||||
}
|
||||
}
|
||||
public MailingService(IMailingRepository mailingRepository, ITargetService targetService, ITemplateService templateService, IEmailDomainService emailDomainService, IConfiguration config)
|
||||
{
|
||||
_mailingRepository = mailingRepository;
|
||||
_targetService = targetService;
|
||||
_templateService = templateService;
|
||||
_emailDomainService = emailDomainService;
|
||||
_config = config;
|
||||
}
|
||||
|
||||
@ -27,13 +73,13 @@ namespace Surge365.MassEmailReact.Infrastructure.Services
|
||||
{
|
||||
return await _mailingRepository.GetAllAsync(activeOnly);
|
||||
}
|
||||
public async Task<List<Mailing>> GetByStatusAsync(string statusCode)
|
||||
public async Task<List<Mailing>> GetByStatusAsync(string statusCode, string? startDate, string? endDate)
|
||||
{
|
||||
return await _mailingRepository.GetByStatusAsync(statusCode);
|
||||
return await _mailingRepository.GetByStatusAsync(statusCode, startDate, endDate);
|
||||
}
|
||||
public async Task<List<MailingStatistic>> GetStatisticsByStatusAsync(string code)
|
||||
public async Task<List<MailingStatistic>> GetStatisticsByStatusAsync(string code, string? startDate, string? endDate)
|
||||
{
|
||||
return await _mailingRepository.GetStatisticsByStatusAsync(code);
|
||||
return await _mailingRepository.GetStatisticsByStatusAsync(code, startDate, endDate);
|
||||
}
|
||||
public async Task<MailingStatistic?> GetStatisticByIdAsync(int id)
|
||||
{
|
||||
@ -43,7 +89,10 @@ namespace Surge365.MassEmailReact.Infrastructure.Services
|
||||
{
|
||||
return await _mailingRepository.NameIsAvailableAsync(id, name);
|
||||
}
|
||||
|
||||
public async Task<string> GetNextAvailableNameAsync(int? id, string name)
|
||||
{
|
||||
return await _mailingRepository.GetNextAvailableNameAsync(id, name);
|
||||
}
|
||||
public async Task<int?> CreateAsync(MailingUpdateDto mailingDto)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(mailingDto, nameof(mailingDto));
|
||||
@ -92,5 +141,164 @@ namespace Surge365.MassEmailReact.Infrastructure.Services
|
||||
{
|
||||
return await _mailingRepository.CancelMailingAsync(id);
|
||||
}
|
||||
|
||||
public async Task<bool> TestMailing(MailingUpdateDto mailing, List<string> emails)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(mailing, nameof(mailing));
|
||||
ArgumentNullException.ThrowIfNull(emails, nameof(emails));
|
||||
if(emails.Count == 0) throw new Exception("No emails provided");
|
||||
|
||||
if (mailing.TargetId <= 0 || mailing.TemplateId <= 0)
|
||||
return false;
|
||||
|
||||
var targetSample = await _targetService.TestTargetAsync(mailing.TargetId);
|
||||
if (targetSample == null || targetSample.Rows.Count == 0)
|
||||
return false;
|
||||
|
||||
var template = await _templateService.GetByIdAsync(mailing.TemplateId);
|
||||
if (template == null)
|
||||
return false;
|
||||
|
||||
var domain = await _emailDomainService.GetByIdAsync(template.DomainId, true);
|
||||
if (domain == null)
|
||||
return false;
|
||||
|
||||
foreach (string email in emails)
|
||||
await SendTestEmailLocal(template, domain, targetSample, email);
|
||||
|
||||
return true;
|
||||
}
|
||||
private async Task<bool> SendTestEmailLocal(Template template, EmailDomain emailDomain, TargetSample targetSample, string emailAddress)
|
||||
{
|
||||
string html = template.HtmlBody;
|
||||
string tokenTemplate = "(?i)##{0}##";
|
||||
Regex r = new Regex(String.Format(tokenTemplate, "email_address"), RegexOptions.IgnoreCase);
|
||||
html = r.Replace(html, emailAddress);
|
||||
string subject = template.Subject;
|
||||
string toName = "";
|
||||
foreach (string columnName in targetSample.Columns.Keys)
|
||||
{
|
||||
r = new Regex(String.Format(tokenTemplate, columnName), RegexOptions.IgnoreCase);
|
||||
html = r.Replace(html, targetSample.Rows[0][columnName]);
|
||||
subject = r.Replace(subject, targetSample.Rows[0][columnName]);
|
||||
toName = r.Replace(toName, targetSample.Rows[0][columnName]);
|
||||
}
|
||||
html = RemoveOpenTag(html);
|
||||
html = MergeWithUnsubscribe(html, template);
|
||||
return await SendEmailLocal(emailDomain, emailAddress, toName, html, subject, template.FromName, template.FromEmail, template.ReplyToEmail, new List<string>(), null);
|
||||
}
|
||||
|
||||
private string RemoveOpenTag(string html)
|
||||
{
|
||||
Regex r = new Regex("(?i)##opentag##", RegexOptions.IgnoreCase);
|
||||
html = r.Replace(html, "");
|
||||
return html;
|
||||
}
|
||||
private string MergeWithUnsubscribe(string html, Template template)
|
||||
{
|
||||
string url = DefaultUnsubscribeUrl;
|
||||
//if (template.UnsubscribeUrl != null)
|
||||
//{
|
||||
// url = template.UnsubscribeUrl.Url;
|
||||
//}
|
||||
//else if (blast.BlastTemplate != null && blast.BlastTemplate.BlastUnsubscribeUrl != null)
|
||||
//{
|
||||
// url = blast.BlastTemplate.BlastUnsubscribeUrl.Url;
|
||||
//}
|
||||
//else if (blast.BlastTemplate != null && blast.BlastTemplate.UnsubscribeUrl != null)
|
||||
//{
|
||||
// url = blast.BlastTemplate.UnsubscribeUrl.Url;
|
||||
//}
|
||||
//else
|
||||
//{
|
||||
// url = Utilities.DefaultUnsubscribeUrl;
|
||||
//}
|
||||
Regex r = new Regex("(?i)##unsubscribeurl##", RegexOptions.IgnoreCase);
|
||||
html = r.Replace(html, url);
|
||||
return html;
|
||||
}
|
||||
private async Task<bool> SendEmailLocal(EmailDomain domain, string toAddress, string toName, string htmlBody, string subject, string fromName, string fromAddress, string replyToAddress, List<string> categories, int? blastEmailID)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(EmailRegex))
|
||||
{
|
||||
Regex r = new Regex(EmailRegex);
|
||||
if (!r.IsMatch(toAddress))
|
||||
return false;
|
||||
}
|
||||
|
||||
MailMessage m = new MailMessage();
|
||||
m.From = new MailAddress(fromAddress, fromName);
|
||||
m.Subject = subject;
|
||||
m.IsBodyHtml = true;
|
||||
m.Body = htmlBody;
|
||||
m.ReplyToList.Add(new MailAddress(replyToAddress));
|
||||
if (SendGridTestMode)
|
||||
{
|
||||
m.To.Add(new MailAddress("testemailtarget1@ytb.com", toName));
|
||||
}
|
||||
else
|
||||
{
|
||||
m.To.Add(new MailAddress(toAddress, toName));
|
||||
}
|
||||
Dictionary<string, string> uniqueIdentifiers = new Dictionary<string, string>();
|
||||
if (blastEmailID != null)
|
||||
uniqueIdentifiers.Add("emailid", blastEmailID.Value.ToString());
|
||||
await SendEmailToSendGrid(domain, m, categories, uniqueIdentifiers);
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine(ex.Message);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
private async Task SendEmailToSendGrid(EmailDomain domain, MailMessage msg, List<string> categories, Dictionary<string, string> uniqueIdentifiers)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (msg.From == null)
|
||||
return;
|
||||
if (msg.To == null || msg.To.Count == 0)
|
||||
return;
|
||||
if(msg.ReplyToList == null || msg.ReplyToList.Count == 0)
|
||||
return;
|
||||
//string username = domain.Username;
|
||||
string password = domain.Password;
|
||||
//string url = SendGridUrl;
|
||||
//int port = SendGridPort;
|
||||
|
||||
var client = new SendGridClient(password);
|
||||
var message = new SendGridMessage() {
|
||||
From = new EmailAddress(msg.From.Address, msg.From.DisplayName),
|
||||
Subject = msg.Subject,
|
||||
HtmlContent = msg.Body,
|
||||
ReplyTos = msg.ReplyToList.Select(x => new EmailAddress(x.Address, x.DisplayName)).ToList(),
|
||||
};
|
||||
|
||||
message.AddTos(msg.To.Select(x => new EmailAddress(x.Address, x.DisplayName)).ToList());
|
||||
if (uniqueIdentifiers != null && uniqueIdentifiers.Keys.Count > 0)
|
||||
message.AddCustomArgs(uniqueIdentifiers);
|
||||
if (categories != null && categories.Count > 0)
|
||||
message.AddCategories(categories);
|
||||
|
||||
if (SendGridTestMode)
|
||||
{
|
||||
message.AddCustomArg("testmode", "true");
|
||||
}
|
||||
var response = await client.SendEmailAsync(message);
|
||||
if (response.StatusCode != System.Net.HttpStatusCode.Accepted)
|
||||
{
|
||||
var errorBody = await response.Body.ReadAsStringAsync();
|
||||
throw new Exception($"Failed to send email: {response.StatusCode} - {errorBody}");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
//TODO: Log error
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -15,9 +15,11 @@
|
||||
<PackageReference Include="Dapper.FluentMap" Version="2.0.0" />
|
||||
<PackageReference Include="Microsoft.Data.SqlClient" Version="6.0.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration" Version="9.0.2" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="9.0.3" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="9.0.2" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.2" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging" Version="9.0.2" />
|
||||
<PackageReference Include="SendGrid" Version="9.29.3" />
|
||||
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.5.0" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
14
Surge365.MassEmailReact.Web/package-lock.json
generated
14
Surge365.MassEmailReact.Web/package-lock.json
generated
@ -27,6 +27,7 @@
|
||||
"react-hook-form": "^7.54.2",
|
||||
"react-icons": "^5.3.0",
|
||||
"react-router-dom": "^7.0.1",
|
||||
"react-toastify": "^11.0.5",
|
||||
"yup": "^1.6.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
@ -4241,6 +4242,19 @@
|
||||
"react-dom": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/react-toastify": {
|
||||
"version": "11.0.5",
|
||||
"resolved": "https://registry.npmjs.org/react-toastify/-/react-toastify-11.0.5.tgz",
|
||||
"integrity": "sha512-EpqHBGvnSTtHYhCPLxML05NLY2ZX0JURbAdNYa6BUkk+amz4wbKBQvoKQAB0ardvSarUBuY4Q4s1sluAzZwkmA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"clsx": "^2.1.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^18 || ^19",
|
||||
"react-dom": "^18 || ^19"
|
||||
}
|
||||
},
|
||||
"node_modules/react-transition-group": {
|
||||
"version": "4.4.5",
|
||||
"resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz",
|
||||
|
||||
@ -30,6 +30,7 @@
|
||||
"react-hook-form": "^7.54.2",
|
||||
"react-icons": "^5.3.0",
|
||||
"react-router-dom": "^7.0.1",
|
||||
"react-toastify": "^11.0.5",
|
||||
"yup": "^1.6.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@ -34,6 +34,7 @@ import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider';
|
||||
import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs';
|
||||
import dayjs, { Dayjs } from 'dayjs'; // Import Dayjs for date handling
|
||||
import utc from 'dayjs/plugin/utc'; // Import the UTC plugin
|
||||
import { toast } from "react-toastify";
|
||||
|
||||
dayjs.extend(utc);
|
||||
|
||||
@ -142,7 +143,13 @@ const schema = yup.object().shape({
|
||||
|
||||
const nameIsAvailable = async (id: number, name: string) => {
|
||||
const response = await fetch(`/api/mailings/available?${id > 0 ? "id=" + id + "&" : ""}name=${name}`);
|
||||
return await response.json();
|
||||
const data = await response.json();
|
||||
return data.available;
|
||||
};
|
||||
const getNextAvailableName = async (id: number, name: string) => {
|
||||
const response = await fetch(`/api/mailings/nextavailablename?${id > 0 ? "id=" + id + "&" : ""}name=${name}`);
|
||||
const data = await response.json();
|
||||
return data.name;
|
||||
};
|
||||
|
||||
const defaultMailing: Mailing = {
|
||||
@ -184,36 +191,43 @@ const MailingEdit = ({ open, mailing, onClose, onSave }: MailingEditProps) => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
if (mailing) {
|
||||
mailing.scheduleDate = null;
|
||||
mailing.recurringTypeCode = null;
|
||||
mailing.recurringStartDate = null;
|
||||
const initializeMailingEdit = async () => {
|
||||
if (open) {
|
||||
if (mailing) {
|
||||
mailing.scheduleDate = null;
|
||||
mailing.recurringTypeCode = null;
|
||||
mailing.recurringStartDate = null;
|
||||
if (mailing.id == 0) {
|
||||
mailing.name = await getNextAvailableName(mailing.id, mailing.name);
|
||||
}
|
||||
}
|
||||
setApproved(false);
|
||||
setRecurring(false);
|
||||
setScheduleForLater(false);
|
||||
reset(mailing || defaultMailing, { keepDefaultValues: true });
|
||||
if (setupData.testEmailLists.length > 0) {
|
||||
setTestEmailListId(setupData.testEmailLists[0].id);
|
||||
setEmails(setupData.testEmailLists[0].emails);
|
||||
} else {
|
||||
setTestEmailListId(null);
|
||||
setEmails([]);
|
||||
}
|
||||
if (mailing?.templateId) {
|
||||
const template = setupData.templates.find(t => t.id === mailing.templateId) || null;
|
||||
setCurrentTemplate(template);
|
||||
} else {
|
||||
setCurrentTemplate(null);
|
||||
}
|
||||
if (mailing?.targetId) {
|
||||
const target = setupData.targets.find(t => t.id === mailing.targetId) || null;
|
||||
setCurrentTarget(target);
|
||||
} else {
|
||||
setCurrentTarget(null);
|
||||
}
|
||||
}
|
||||
setApproved(false);
|
||||
setRecurring(false);
|
||||
setScheduleForLater(false);
|
||||
reset(mailing || defaultMailing, { keepDefaultValues: true });
|
||||
if (setupData.testEmailLists.length > 0) {
|
||||
setTestEmailListId(setupData.testEmailLists[0].id);
|
||||
setEmails(setupData.testEmailLists[0].emails);
|
||||
} else {
|
||||
setTestEmailListId(null);
|
||||
setEmails([]);
|
||||
}
|
||||
if (mailing?.templateId) {
|
||||
const template = setupData.templates.find(t => t.id === mailing.templateId) || null;
|
||||
setCurrentTemplate(template);
|
||||
} else {
|
||||
setCurrentTemplate(null);
|
||||
}
|
||||
if (mailing?.targetId) {
|
||||
const target = setupData.targets.find(t => t.id === mailing.targetId) || null;
|
||||
setCurrentTarget(target);
|
||||
} else {
|
||||
setCurrentTarget(null);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
initializeMailingEdit();
|
||||
}, [open, mailing, reset, setupData.testEmailLists, setupData.targets, setupData.templates]);
|
||||
|
||||
const handleSave = async (formData: Mailing) => {
|
||||
@ -261,7 +275,47 @@ const MailingEdit = ({ open, mailing, onClose, onSave }: MailingEditProps) => {
|
||||
setEmails(newEmails);
|
||||
};
|
||||
|
||||
const handleTestMailing = () => {
|
||||
const handleTestMailing = async () => {
|
||||
setLoading(true); // Show loading state
|
||||
|
||||
const isValid = await trigger();
|
||||
if (!isValid) {
|
||||
console.log("Form is invalid, cannot test mailing");
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Get the current form data
|
||||
const formData = control._formValues as Mailing;
|
||||
|
||||
// Prepare the payload matching TestMailingDto
|
||||
const testMailingPayload = {
|
||||
mailing: formData,
|
||||
emailAddresses: emails,
|
||||
};
|
||||
|
||||
// Make the API call to /api/mailings/test
|
||||
const response = await fetch("/api/mailings/test", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(testMailingPayload),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to test mailing");
|
||||
}
|
||||
|
||||
toast.success("Test mailing sent successfully");
|
||||
console.log("Test mailing sent successfully");
|
||||
} catch (error) {
|
||||
console.error("Test mailing error:", error);
|
||||
toast.error("Failed to send test mailing");
|
||||
} finally {
|
||||
setLoading(false); // Reset loading state
|
||||
}
|
||||
};
|
||||
|
||||
const handleTestEmailListChange = (list: TestEmailList | null) => {
|
||||
|
||||
@ -79,7 +79,7 @@ const TestEmailListEdit = ({ open, testEmailList, onClose, onSave }: TestEmailLi
|
||||
}, [open, testEmailList, reset]);
|
||||
|
||||
const handleSave = async (formData: TestEmailListForm) => {
|
||||
const emails = formData.emails.split("\n");
|
||||
const emails = formData.emails.split("\n").filter(email => email.trim() !== "");
|
||||
const testEmailListDto = {
|
||||
id: formData.id,
|
||||
name: formData.name,
|
||||
|
||||
@ -27,6 +27,8 @@ import { createTheme, ThemeProvider, Theme } from '@mui/material/styles';
|
||||
import CssBaseline from '@mui/material/CssBaseline';
|
||||
import ProtectedPageWrapper from '@/components/auth/ProtectedPageWrapper';
|
||||
|
||||
import { ToastContainer } from "react-toastify";
|
||||
|
||||
const PageWrapper = ProtectedPageWrapper;
|
||||
|
||||
//interface PageWrapperProps {
|
||||
@ -86,6 +88,7 @@ const App = () => {
|
||||
<AuthProvider>
|
||||
<Router basename="/">
|
||||
<AuthCheck />
|
||||
<ToastContainer />
|
||||
<Routes>
|
||||
<Route path="/" element={<Navigate to="/home" replace />} />
|
||||
<Route
|
||||
|
||||
@ -2,7 +2,8 @@
|
||||
import VisibilityIcon from '@mui/icons-material/Visibility';
|
||||
import ContentCopyIcon from '@mui/icons-material/ContentCopy';
|
||||
import RefreshIcon from '@mui/icons-material/Refresh';
|
||||
import { List, Card, CardContent, Typography, Box, useTheme, useMediaQuery, CircularProgress, IconButton } from '@mui/material';
|
||||
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
|
||||
import { List, Card, CardContent, Typography, Box, useTheme, useMediaQuery, CircularProgress, IconButton, Collapse, TextField, Button } from '@mui/material';
|
||||
import { DataGrid, GridColDef, GridRenderCellParams, GridToolbarContainer, GridToolbarQuickFilter, GridToolbarExport, GridToolbarDensitySelector, GridToolbarColumnsButton } from '@mui/x-data-grid';
|
||||
import MailingStatistic from '@/types/mailingStatistic';
|
||||
import Mailing from '@/types/mailing';
|
||||
@ -19,6 +20,13 @@ function CompletedMailings() {
|
||||
const [selectedMailing, setSelectedMailing] = useState<Mailing | null>(null);
|
||||
const [viewOpen, setViewOpen] = useState<boolean>(false);
|
||||
const [editOpen, setEditOpen] = useState<boolean>(false);
|
||||
const [searchOpen, setSearchOpen] = useState<boolean>(false);
|
||||
|
||||
const defaultStartDate = new Date();
|
||||
defaultStartDate.setMonth(defaultStartDate.getMonth() - 3);
|
||||
|
||||
const [startDate, setStartDate] = useState<Date | null>(defaultStartDate);
|
||||
const [endDate, setEndDate] = useState<Date | null>(null);
|
||||
|
||||
const columns: GridColDef<MailingStatistic>[] = [
|
||||
{
|
||||
@ -57,7 +65,29 @@ function CompletedMailings() {
|
||||
const fetchMailingStats = async () => {
|
||||
setMailingsLoading(true);
|
||||
try {
|
||||
const response = await fetch("/api/mailings/status/S/stats");
|
||||
let url = "/api/mailings/status/S/stats";
|
||||
// Add query parameters if dates are provided
|
||||
const params = [];
|
||||
if (startDate) {
|
||||
const formattedStart = startDate.toLocaleDateString('en-US', {
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
year: 'numeric'
|
||||
}).replace(/\//g, '-');
|
||||
params.push(`startDate=${formattedStart}`);
|
||||
}
|
||||
if (endDate) {
|
||||
const formattedEnd = endDate.toLocaleDateString('en-US', {
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
year: 'numeric'
|
||||
}).replace(/\//g, '-');
|
||||
params.push(`endDate=${formattedEnd}`);
|
||||
}
|
||||
if (params.length > 0) {
|
||||
url += `?${params.join('&')}`;
|
||||
}
|
||||
const response = await fetch(url);
|
||||
const statsData = await response.json();
|
||||
if (statsData) {
|
||||
setMailingStats(statsData);
|
||||
@ -116,6 +146,9 @@ function CompletedMailings() {
|
||||
useEffect(() => {
|
||||
fetchMailingStats(); // Initial fetch
|
||||
}, []);
|
||||
const handleSearch = () => {
|
||||
fetchMailingStats();
|
||||
};
|
||||
|
||||
return (
|
||||
<Box ref={gridContainerRef} sx={{
|
||||
@ -125,7 +158,38 @@ function CompletedMailings() {
|
||||
duration: theme.transitions.duration.standard,
|
||||
})
|
||||
}}>
|
||||
<Box sx={{ position: 'absolute', inset: 0 }}>
|
||||
<Box sx={{ p: 2 }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', cursor: 'pointer' }} onClick={() => setSearchOpen(!searchOpen)}>
|
||||
<Typography variant="h6">Search Filters</Typography>
|
||||
<ExpandMoreIcon sx={{
|
||||
ml: 1,
|
||||
transform: searchOpen ? 'rotate(180deg)' : 'rotate(0deg)',
|
||||
transition: 'transform 0.2s'
|
||||
}} />
|
||||
</Box>
|
||||
<Collapse in={searchOpen}>
|
||||
<Box sx={{ mt: 2, mb: 2, display: 'flex', gap: 2, flexWrap: 'wrap' }}>
|
||||
<TextField
|
||||
label="Start Date"
|
||||
type="date"
|
||||
value={startDate ? startDate.toISOString().split('T')[0] : ''}
|
||||
onChange={(e) => setStartDate(e.target.value ? new Date(e.target.value) : null)}
|
||||
InputLabelProps={{ shrink: true }}
|
||||
/>
|
||||
<TextField
|
||||
label="End Date"
|
||||
type="date"
|
||||
value={endDate ? endDate.toISOString().split('T')[0] : ''}
|
||||
onChange={(e) => setEndDate(e.target.value ? new Date(e.target.value) : null)}
|
||||
InputLabelProps={{ shrink: true }}
|
||||
/>
|
||||
<Button variant="contained" onClick={handleSearch}>
|
||||
Search
|
||||
</Button>
|
||||
</Box>
|
||||
</Collapse>
|
||||
</Box>
|
||||
<Box sx={{ position: 'absolute', inset: 0, top: searchOpen ? 140 : 60 }}>
|
||||
{isMobile ? (
|
||||
<List>
|
||||
{mailingStats.map((stat) => (
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user