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)
|
public async Task<IActionResult> CheckNameAvailable([FromQuery] int? id, [FromQuery][Required] string name)
|
||||||
{
|
{
|
||||||
var available = await _mailingService.NameIsAvailableAsync(id, 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}")]
|
[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);
|
return Ok(mailings);
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("status/{statusCode}/stats")]
|
[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);
|
return Ok(mailings);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -67,6 +74,12 @@ namespace Surge365.MassEmailReact.API.Controllers
|
|||||||
if (mailingUpdateDto.Id != null && mailingUpdateDto.Id > 0)
|
if (mailingUpdateDto.Id != null && mailingUpdateDto.Id > 0)
|
||||||
return BadRequest("Id must be null or 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);
|
var mailingId = await _mailingService.CreateAsync(mailingUpdateDto);
|
||||||
if (mailingId == null)
|
if (mailingId == null)
|
||||||
return StatusCode(StatusCodes.Status500InternalServerError, "Failed to create mailing.");
|
return StatusCode(StatusCodes.Status500InternalServerError, "Failed to create mailing.");
|
||||||
@ -109,5 +122,18 @@ namespace Surge365.MassEmailReact.API.Controllers
|
|||||||
var updatedMailing = await _mailingService.GetByIdAsync(id);
|
var updatedMailing = await _mailingService.GetByIdAsync(id);
|
||||||
return Ok(updatedMailing);
|
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
|
"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",
|
"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<Mailing?> GetByIdAsync(int id);
|
||||||
Task<List<Mailing>> GetAllAsync(bool activeOnly = true);
|
Task<List<Mailing>> GetAllAsync(bool activeOnly = true);
|
||||||
Task<List<Mailing>> GetByStatusAsync(string code);
|
Task<List<Mailing>> GetByStatusAsync(string code, string? startDate, string? endDate);
|
||||||
Task<List<MailingStatistic>> GetStatisticsByStatusAsync(string code);
|
Task<List<MailingStatistic>> GetStatisticsByStatusAsync(string code, string? startDate, string? endDate);
|
||||||
Task<MailingStatistic?> GetStatisticByIdAsync(int id);
|
Task<MailingStatistic?> GetStatisticByIdAsync(int id);
|
||||||
Task<bool> NameIsAvailableAsync(int? id, string name);
|
Task<bool> NameIsAvailableAsync(int? id, string name);
|
||||||
|
Task<string> GetNextAvailableNameAsync(int? id, string name);
|
||||||
Task<int?> CreateAsync(Mailing mailing);
|
Task<int?> CreateAsync(Mailing mailing);
|
||||||
Task<bool> UpdateAsync(Mailing mailing);
|
Task<bool> UpdateAsync(Mailing mailing);
|
||||||
Task<bool> CancelMailingAsync(int id);
|
Task<bool> CancelMailingAsync(int id);
|
||||||
|
|||||||
@ -9,13 +9,15 @@ namespace Surge365.MassEmailReact.Application.Interfaces
|
|||||||
Task<Mailing?> GetByIdAsync(int id);
|
Task<Mailing?> GetByIdAsync(int id);
|
||||||
Task<List<Mailing>> GetAllAsync(bool activeOnly = true);
|
Task<List<Mailing>> GetAllAsync(bool activeOnly = true);
|
||||||
|
|
||||||
Task<List<Mailing>> GetByStatusAsync(string code);
|
Task<List<Mailing>> GetByStatusAsync(string code, string? startDate, string? endDate);
|
||||||
Task<List<MailingStatistic>> GetStatisticsByStatusAsync(string code);
|
Task<List<MailingStatistic>> GetStatisticsByStatusAsync(string code, string? startDate, string? endDate);
|
||||||
Task<MailingStatistic?> GetStatisticByIdAsync(int id);
|
Task<MailingStatistic?> GetStatisticByIdAsync(int id);
|
||||||
Task<bool> NameIsAvailableAsync(int? id, string name);
|
Task<bool> NameIsAvailableAsync(int? id, string name);
|
||||||
|
Task<string> GetNextAvailableNameAsync(int? id, string name);
|
||||||
|
|
||||||
Task<int?> CreateAsync(MailingUpdateDto mailingDto);
|
Task<int?> CreateAsync(MailingUpdateDto mailingDto);
|
||||||
Task<bool> UpdateAsync(MailingUpdateDto mailingDto);
|
Task<bool> UpdateAsync(MailingUpdateDto mailingDto);
|
||||||
Task<bool> CancelMailingAsync(int id);
|
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);
|
using SqlConnection conn = new SqlConnection(ConnectionString);
|
||||||
return (await conn.QueryAsync<Mailing>("mem_get_blast_all", new { active_only = activeOnly }, commandType: CommandType.StoredProcedure)).ToList();
|
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);
|
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_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);
|
ArgumentNullException.ThrowIfNull(ConnectionString);
|
||||||
|
|
||||||
using SqlConnection conn = new SqlConnection(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)
|
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("@blast_name", name, DbType.String);
|
||||||
parameters.Add("@available", dbType: DbType.Boolean, direction: ParameterDirection.Output);
|
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");
|
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)
|
public async Task<int?> CreateAsync(Mailing mailing)
|
||||||
{
|
{
|
||||||
|
|||||||
@ -1,20 +1,66 @@
|
|||||||
using Microsoft.Extensions.Configuration;
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using Surge365.MassEmailReact.Application.DTOs;
|
||||||
using Surge365.MassEmailReact.Application.Interfaces;
|
using Surge365.MassEmailReact.Application.Interfaces;
|
||||||
using Surge365.MassEmailReact.Domain.Entities;
|
using Surge365.MassEmailReact.Domain.Entities;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Data;
|
||||||
|
using System.Net.Mail;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using SendGrid;
|
||||||
|
using SendGrid.Helpers.Mail;
|
||||||
|
|
||||||
namespace Surge365.MassEmailReact.Infrastructure.Services
|
namespace Surge365.MassEmailReact.Infrastructure.Services
|
||||||
{
|
{
|
||||||
public class MailingService : IMailingService
|
public class MailingService : IMailingService
|
||||||
{
|
{
|
||||||
|
private readonly ITargetService _targetService;
|
||||||
|
private readonly ITemplateService _templateService;
|
||||||
|
private readonly IEmailDomainService _emailDomainService;
|
||||||
private readonly IMailingRepository _mailingRepository;
|
private readonly IMailingRepository _mailingRepository;
|
||||||
private readonly IConfiguration _config;
|
private readonly IConfiguration _config;
|
||||||
|
private string DefaultUnsubscribeUrl
|
||||||
public MailingService(IMailingRepository mailingRepository, IConfiguration config)
|
{
|
||||||
|
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;
|
_mailingRepository = mailingRepository;
|
||||||
|
_targetService = targetService;
|
||||||
|
_templateService = templateService;
|
||||||
|
_emailDomainService = emailDomainService;
|
||||||
_config = config;
|
_config = config;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -27,13 +73,13 @@ namespace Surge365.MassEmailReact.Infrastructure.Services
|
|||||||
{
|
{
|
||||||
return await _mailingRepository.GetAllAsync(activeOnly);
|
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)
|
public async Task<MailingStatistic?> GetStatisticByIdAsync(int id)
|
||||||
{
|
{
|
||||||
@ -43,7 +89,10 @@ namespace Surge365.MassEmailReact.Infrastructure.Services
|
|||||||
{
|
{
|
||||||
return await _mailingRepository.NameIsAvailableAsync(id, name);
|
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)
|
public async Task<int?> CreateAsync(MailingUpdateDto mailingDto)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(mailingDto, nameof(mailingDto));
|
ArgumentNullException.ThrowIfNull(mailingDto, nameof(mailingDto));
|
||||||
@ -92,5 +141,164 @@ namespace Surge365.MassEmailReact.Infrastructure.Services
|
|||||||
{
|
{
|
||||||
return await _mailingRepository.CancelMailingAsync(id);
|
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="Dapper.FluentMap" Version="2.0.0" />
|
||||||
<PackageReference Include="Microsoft.Data.SqlClient" Version="6.0.1" />
|
<PackageReference Include="Microsoft.Data.SqlClient" Version="6.0.1" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Configuration" Version="9.0.2" />
|
<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.Configuration.Json" Version="9.0.2" />
|
||||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.2" />
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.2" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Logging" 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" />
|
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.5.0" />
|
||||||
</ItemGroup>
|
</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-hook-form": "^7.54.2",
|
||||||
"react-icons": "^5.3.0",
|
"react-icons": "^5.3.0",
|
||||||
"react-router-dom": "^7.0.1",
|
"react-router-dom": "^7.0.1",
|
||||||
|
"react-toastify": "^11.0.5",
|
||||||
"yup": "^1.6.1"
|
"yup": "^1.6.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@ -4241,6 +4242,19 @@
|
|||||||
"react-dom": ">=18"
|
"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": {
|
"node_modules/react-transition-group": {
|
||||||
"version": "4.4.5",
|
"version": "4.4.5",
|
||||||
"resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz",
|
"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-hook-form": "^7.54.2",
|
||||||
"react-icons": "^5.3.0",
|
"react-icons": "^5.3.0",
|
||||||
"react-router-dom": "^7.0.1",
|
"react-router-dom": "^7.0.1",
|
||||||
|
"react-toastify": "^11.0.5",
|
||||||
"yup": "^1.6.1"
|
"yup": "^1.6.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@ -34,6 +34,7 @@ import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider';
|
|||||||
import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs';
|
import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs';
|
||||||
import dayjs, { Dayjs } from 'dayjs'; // Import Dayjs for date handling
|
import dayjs, { Dayjs } from 'dayjs'; // Import Dayjs for date handling
|
||||||
import utc from 'dayjs/plugin/utc'; // Import the UTC plugin
|
import utc from 'dayjs/plugin/utc'; // Import the UTC plugin
|
||||||
|
import { toast } from "react-toastify";
|
||||||
|
|
||||||
dayjs.extend(utc);
|
dayjs.extend(utc);
|
||||||
|
|
||||||
@ -142,7 +143,13 @@ const schema = yup.object().shape({
|
|||||||
|
|
||||||
const nameIsAvailable = async (id: number, name: string) => {
|
const nameIsAvailable = async (id: number, name: string) => {
|
||||||
const response = await fetch(`/api/mailings/available?${id > 0 ? "id=" + id + "&" : ""}name=${name}`);
|
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 = {
|
const defaultMailing: Mailing = {
|
||||||
@ -184,11 +191,15 @@ const MailingEdit = ({ open, mailing, onClose, onSave }: MailingEditProps) => {
|
|||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
const initializeMailingEdit = async () => {
|
||||||
if (open) {
|
if (open) {
|
||||||
if (mailing) {
|
if (mailing) {
|
||||||
mailing.scheduleDate = null;
|
mailing.scheduleDate = null;
|
||||||
mailing.recurringTypeCode = null;
|
mailing.recurringTypeCode = null;
|
||||||
mailing.recurringStartDate = null;
|
mailing.recurringStartDate = null;
|
||||||
|
if (mailing.id == 0) {
|
||||||
|
mailing.name = await getNextAvailableName(mailing.id, mailing.name);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
setApproved(false);
|
setApproved(false);
|
||||||
setRecurring(false);
|
setRecurring(false);
|
||||||
@ -214,6 +225,9 @@ const MailingEdit = ({ open, mailing, onClose, onSave }: MailingEditProps) => {
|
|||||||
setCurrentTarget(null);
|
setCurrentTarget(null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
initializeMailingEdit();
|
||||||
}, [open, mailing, reset, setupData.testEmailLists, setupData.targets, setupData.templates]);
|
}, [open, mailing, reset, setupData.testEmailLists, setupData.targets, setupData.templates]);
|
||||||
|
|
||||||
const handleSave = async (formData: Mailing) => {
|
const handleSave = async (formData: Mailing) => {
|
||||||
@ -261,7 +275,47 @@ const MailingEdit = ({ open, mailing, onClose, onSave }: MailingEditProps) => {
|
|||||||
setEmails(newEmails);
|
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) => {
|
const handleTestEmailListChange = (list: TestEmailList | null) => {
|
||||||
|
|||||||
@ -79,7 +79,7 @@ const TestEmailListEdit = ({ open, testEmailList, onClose, onSave }: TestEmailLi
|
|||||||
}, [open, testEmailList, reset]);
|
}, [open, testEmailList, reset]);
|
||||||
|
|
||||||
const handleSave = async (formData: TestEmailListForm) => {
|
const handleSave = async (formData: TestEmailListForm) => {
|
||||||
const emails = formData.emails.split("\n");
|
const emails = formData.emails.split("\n").filter(email => email.trim() !== "");
|
||||||
const testEmailListDto = {
|
const testEmailListDto = {
|
||||||
id: formData.id,
|
id: formData.id,
|
||||||
name: formData.name,
|
name: formData.name,
|
||||||
|
|||||||
@ -27,6 +27,8 @@ import { createTheme, ThemeProvider, Theme } from '@mui/material/styles';
|
|||||||
import CssBaseline from '@mui/material/CssBaseline';
|
import CssBaseline from '@mui/material/CssBaseline';
|
||||||
import ProtectedPageWrapper from '@/components/auth/ProtectedPageWrapper';
|
import ProtectedPageWrapper from '@/components/auth/ProtectedPageWrapper';
|
||||||
|
|
||||||
|
import { ToastContainer } from "react-toastify";
|
||||||
|
|
||||||
const PageWrapper = ProtectedPageWrapper;
|
const PageWrapper = ProtectedPageWrapper;
|
||||||
|
|
||||||
//interface PageWrapperProps {
|
//interface PageWrapperProps {
|
||||||
@ -86,6 +88,7 @@ const App = () => {
|
|||||||
<AuthProvider>
|
<AuthProvider>
|
||||||
<Router basename="/">
|
<Router basename="/">
|
||||||
<AuthCheck />
|
<AuthCheck />
|
||||||
|
<ToastContainer />
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/" element={<Navigate to="/home" replace />} />
|
<Route path="/" element={<Navigate to="/home" replace />} />
|
||||||
<Route
|
<Route
|
||||||
|
|||||||
@ -2,7 +2,8 @@
|
|||||||
import VisibilityIcon from '@mui/icons-material/Visibility';
|
import VisibilityIcon from '@mui/icons-material/Visibility';
|
||||||
import ContentCopyIcon from '@mui/icons-material/ContentCopy';
|
import ContentCopyIcon from '@mui/icons-material/ContentCopy';
|
||||||
import RefreshIcon from '@mui/icons-material/Refresh';
|
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 { DataGrid, GridColDef, GridRenderCellParams, GridToolbarContainer, GridToolbarQuickFilter, GridToolbarExport, GridToolbarDensitySelector, GridToolbarColumnsButton } from '@mui/x-data-grid';
|
||||||
import MailingStatistic from '@/types/mailingStatistic';
|
import MailingStatistic from '@/types/mailingStatistic';
|
||||||
import Mailing from '@/types/mailing';
|
import Mailing from '@/types/mailing';
|
||||||
@ -19,6 +20,13 @@ function CompletedMailings() {
|
|||||||
const [selectedMailing, setSelectedMailing] = useState<Mailing | null>(null);
|
const [selectedMailing, setSelectedMailing] = useState<Mailing | null>(null);
|
||||||
const [viewOpen, setViewOpen] = useState<boolean>(false);
|
const [viewOpen, setViewOpen] = useState<boolean>(false);
|
||||||
const [editOpen, setEditOpen] = 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>[] = [
|
const columns: GridColDef<MailingStatistic>[] = [
|
||||||
{
|
{
|
||||||
@ -57,7 +65,29 @@ function CompletedMailings() {
|
|||||||
const fetchMailingStats = async () => {
|
const fetchMailingStats = async () => {
|
||||||
setMailingsLoading(true);
|
setMailingsLoading(true);
|
||||||
try {
|
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();
|
const statsData = await response.json();
|
||||||
if (statsData) {
|
if (statsData) {
|
||||||
setMailingStats(statsData);
|
setMailingStats(statsData);
|
||||||
@ -116,6 +146,9 @@ function CompletedMailings() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchMailingStats(); // Initial fetch
|
fetchMailingStats(); // Initial fetch
|
||||||
}, []);
|
}, []);
|
||||||
|
const handleSearch = () => {
|
||||||
|
fetchMailingStats();
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box ref={gridContainerRef} sx={{
|
<Box ref={gridContainerRef} sx={{
|
||||||
@ -125,7 +158,38 @@ function CompletedMailings() {
|
|||||||
duration: theme.transitions.duration.standard,
|
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 ? (
|
{isMobile ? (
|
||||||
<List>
|
<List>
|
||||||
{mailingStats.map((stat) => (
|
{mailingStats.map((stat) => (
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user