Update mailing and target management features
- Added new methods for creating mailings and testing targets. - Updated configuration files for JWT settings and connection strings. - Introduced new DTOs for target column updates and test targets. - Enhanced MailingStatistic with a new SentDate property. - Created new components for handling cancelled mailings and target samples. - Refactored authentication in Login.tsx to use fetch API. - Updated various services and repositories to support new functionalities.
This commit is contained in:
parent
26abe9e028
commit
f5b1fe6397
13
Surge365.MassEmailReact.API/.config/dotnet-tools.json
Normal file
13
Surge365.MassEmailReact.API/.config/dotnet-tools.json
Normal file
@ -0,0 +1,13 @@
|
||||
{
|
||||
"version": 1,
|
||||
"isRoot": true,
|
||||
"tools": {
|
||||
"dotnet-ef": {
|
||||
"version": "9.0.3",
|
||||
"commands": [
|
||||
"dotnet-ef"
|
||||
],
|
||||
"rollForward": false
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -67,7 +67,6 @@ namespace Surge365.MassEmailReact.API.Controllers
|
||||
return mailing is not null ? Ok(mailing) : NotFound($"Mailing statistics with id '{id}' not found.");
|
||||
}
|
||||
|
||||
|
||||
[HttpPost]
|
||||
public async Task<IActionResult> CreateMailing([FromBody] MailingUpdateDto mailingUpdateDto)
|
||||
{
|
||||
|
||||
@ -78,5 +78,19 @@ namespace Surge365.MassEmailReact.API.Controllers
|
||||
return StatusCode(500, new { message = "Error fetching sample data", error = ex.Message });
|
||||
}
|
||||
}
|
||||
[HttpPost("test")]
|
||||
public async Task<IActionResult> TestTargeByID(TestTargetDto testTarget)
|
||||
{
|
||||
try
|
||||
{
|
||||
var sample = await _targetService.TestTargetAsync(testTarget);
|
||||
return Ok(sample);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Log the exception (e.g., using ILogger if injected)
|
||||
return StatusCode(500, new { message = "Error fetching sample data", error = ex.Message });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -5,5 +5,22 @@
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
},
|
||||
"ConnectionStringTemplate": "data source=##server_name##,##port##;initial catalog=##database_name##;User ID=##username##;Password=##password##;persist security info=False;packet size=4096;TrustServerCertificate=True;"
|
||||
}
|
||||
"AllowedHosts": "*",
|
||||
"Jwt": {
|
||||
"Secret": "Z9R5aFml+eRMeb7tyf8N9wCq3tZpS/EM6nGqOxlXPtOw4cJ3zS1AByczrIlD5F9d"
|
||||
},
|
||||
"AppCode": "MassEmailReactApi",
|
||||
"AuthAppCode": "MassEmailWeb",
|
||||
"EnvironmentCode": "UAT",
|
||||
"ConnectionStrings": {
|
||||
"Marketing.ConnectionString": "data source=uat.surge365.com;initial catalog=Marketing;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",
|
||||
"ConnectionStringTemplate": "data source=##server_name##,##port##;initial catalog=##database_name##;User ID=##username##;Password=##password##;persist security info=False;packet size=4096;TrustServerCertificate=True;",
|
||||
"DefaultUnsubscribeUrl": "https://uat.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"
|
||||
}
|
||||
18
Surge365.MassEmailReact.API/appsettings.Uat.json
Normal file
18
Surge365.MassEmailReact.API/appsettings.Uat.json
Normal file
@ -0,0 +1,18 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
},
|
||||
"AllowedHosts": "*",
|
||||
"Jwt": {
|
||||
"Secret": "1bXgXk7v/W9XksGoNiqWvM7+9/BERZonShxqoCVvdi8Ew47M1VFzJGA9sPMgkmn/HRmuZ83iytNsHXI6GkAb8g=="
|
||||
},
|
||||
"EnvironmentCode": "UAT",
|
||||
"DefaultUnsubscribeUrl": "https://uat.emailopentracking.surge365.com/unsubscribe.htm",
|
||||
"SendGrid_TestMode": true,
|
||||
"RegularExpression_Email": "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$",
|
||||
"SendGrid_Url": "smtp.sendgrid.net",
|
||||
"SendGrid_Port": "587"
|
||||
}
|
||||
@ -7,18 +7,18 @@
|
||||
},
|
||||
"AllowedHosts": "*",
|
||||
"Jwt": {
|
||||
"Secret": "Z9R5aFml+eRMeb7tyf8N9wCq3tZpS/EM6nGqOxlXPtOw4cJ3zS1AByczrIlD5F9d"
|
||||
"Secret": "4r1AJ0riBpEhgaTxhTWMIPs5rv9AlVZjTqrGUoU3DUz4i/Dx9ZfGciIubNODQRO0z3qJZq6VqxGXdsFRJgSb6Q=="
|
||||
},
|
||||
"AppCode": "MassEmailReactApi",
|
||||
"AuthAppCode": "MassEmailWeb",
|
||||
"EnvironmentCode": "UAT",
|
||||
"ConnectionStrings": {
|
||||
"Marketing.ConnectionString": "data source=uat.surge365.com;initial catalog=Marketing;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
|
||||
"Marketing.ConnectionString": "data source=localhost;initial catalog=Marketing;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=localhost;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;",
|
||||
"DefaultUnsubscribeUrl": "http://emailopentracking.surge365.com/unsubscribe.htm",
|
||||
"ConnectionStringTemplate": "data source=##server_name##,##port##;initial catalog=##database_name##;User ID=##username##;Password=##password##;persist security info=False;packet size=4096;TrustServerCertificate=True;",
|
||||
"DefaultUnsubscribeUrl": "https://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",
|
||||
|
||||
@ -0,0 +1,18 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Surge365.MassEmailReact.Domain.Entities
|
||||
{
|
||||
public class TargetColumnUpdateDto
|
||||
{
|
||||
public int? Id { get; private set; }
|
||||
public string TypeCode { get; set; } = "";
|
||||
public string DataTypeCode { get; set; } = "";
|
||||
public string Name { get; set; } = "";
|
||||
public bool WriteBack { get; set; }
|
||||
public bool IsEmailAddress { get; set; }
|
||||
}
|
||||
}
|
||||
@ -13,8 +13,9 @@ namespace Surge365.MassEmailReact.Application.DTOs
|
||||
}
|
||||
public class TargetSampleColumn
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public string Type { get; set; }
|
||||
public string Name { get; set; } = "";
|
||||
public string DataType { get; set; } = "";
|
||||
public string Type { get; set; } = "";
|
||||
}
|
||||
|
||||
public class TargetColumnType
|
||||
|
||||
@ -16,5 +16,6 @@ namespace Surge365.MassEmailReact.Domain.Entities
|
||||
public string FilterQuery { get; set; } = "";
|
||||
public bool AllowWriteBack { get; set; } = false;
|
||||
public bool IsActive { get; set; } = true;
|
||||
public List<TargetColumnUpdateDto> Columns { get; set; } = new List<TargetColumnUpdateDto>();
|
||||
}
|
||||
}
|
||||
}
|
||||
17
Surge365.MassEmailReact.Application/DTOs/TestTargetDto.cs
Normal file
17
Surge365.MassEmailReact.Application/DTOs/TestTargetDto.cs
Normal file
@ -0,0 +1,17 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Surge365.MassEmailReact.Domain.Entities
|
||||
{
|
||||
public class TestTargetDto
|
||||
{
|
||||
public int? Id { get; set; }
|
||||
public int ServerId { get; set; }
|
||||
public string DatabaseName { get; set; } = "";
|
||||
public string ViewName { get; set; } = "";
|
||||
public string FilterQuery { get; set; } = "";
|
||||
}
|
||||
}
|
||||
@ -8,8 +8,8 @@ namespace Surge365.MassEmailReact.Application.Interfaces
|
||||
{
|
||||
Task<Mailing?> GetByIdAsync(int id);
|
||||
Task<List<Mailing>> GetAllAsync(bool activeOnly = true);
|
||||
Task<List<Mailing>> GetByStatusAsync(string code, string? startDate, string? endDate);
|
||||
Task<List<MailingStatistic>> GetStatisticsByStatusAsync(string code, 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<MailingStatistic?> GetStatisticByIdAsync(int id);
|
||||
Task<bool> NameIsAvailableAsync(int? id, string name);
|
||||
Task<string> GetNextAvailableNameAsync(int? id, string name);
|
||||
|
||||
@ -9,8 +9,8 @@ namespace Surge365.MassEmailReact.Application.Interfaces
|
||||
Task<Mailing?> GetByIdAsync(int id);
|
||||
Task<List<Mailing>> GetAllAsync(bool activeOnly = true);
|
||||
|
||||
Task<List<Mailing>> GetByStatusAsync(string code, string? startDate, string? endDate);
|
||||
Task<List<MailingStatistic>> GetStatisticsByStatusAsync(string code, 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<MailingStatistic?> GetStatisticByIdAsync(int id);
|
||||
Task<bool> NameIsAvailableAsync(int? id, string name);
|
||||
Task<string> GetNextAvailableNameAsync(int? id, string name);
|
||||
|
||||
@ -10,5 +10,6 @@ namespace Surge365.MassEmailReact.Application.Interfaces
|
||||
Task<int?> CreateAsync(TargetUpdateDto targetDto);
|
||||
Task<bool> UpdateAsync(TargetUpdateDto targetDto);
|
||||
Task<TargetSample?> TestTargetAsync(int targetId);
|
||||
Task<TargetSample?> TestTargetAsync(TestTargetDto testTarget);
|
||||
}
|
||||
}
|
||||
|
||||
@ -10,6 +10,7 @@ namespace Surge365.MassEmailReact.Domain.Entities
|
||||
{
|
||||
public int? MailingId { get; private set; }
|
||||
public string MailingName { get; set; } = "";
|
||||
public string SentDate { get; set; } = "";
|
||||
public int SpamCount { get; set; }
|
||||
public int UniqueClickCount { get; set; }
|
||||
public int ClickCount { get; set; }
|
||||
@ -26,13 +27,14 @@ namespace Surge365.MassEmailReact.Domain.Entities
|
||||
|
||||
public MailingStatistic() { }
|
||||
|
||||
private MailingStatistic(int? mailingId, string mailingName, int spamCount, int uniqueClickCount, int clickCount,
|
||||
private MailingStatistic(int? mailingId, string mailingName, string sentDate, int spamCount, int uniqueClickCount, int clickCount,
|
||||
int uniqueOpenCount, int openCount, int invalidCount, int blockedCount,
|
||||
int failedCount, int deliveredCount, int sendCount, int emailCount,
|
||||
int bounceCount, int unsubscribeCount)
|
||||
{
|
||||
MailingId = mailingId;
|
||||
MailingName = mailingName;
|
||||
SentDate = sentDate;
|
||||
SpamCount = spamCount;
|
||||
UniqueClickCount = uniqueClickCount;
|
||||
ClickCount = clickCount;
|
||||
@ -48,12 +50,12 @@ namespace Surge365.MassEmailReact.Domain.Entities
|
||||
UnsubscribeCount = unsubscribeCount;
|
||||
}
|
||||
|
||||
public static MailingStatistic Create(int? mailingId, string mailingName, int spamCount, int uniqueClickCount,
|
||||
public static MailingStatistic Create(int? mailingId, string mailingName, string sentDate, int spamCount, int uniqueClickCount,
|
||||
int clickCount, int uniqueOpenCount, int openCount, int invalidCount,
|
||||
int blockedCount, int failedCount, int deliveredCount, int sendCount,
|
||||
int emailCount, int bounceCount, int unsubscribeCount)
|
||||
{
|
||||
return new MailingStatistic(mailingId, mailingName, spamCount, uniqueClickCount, clickCount, uniqueOpenCount,
|
||||
return new MailingStatistic(mailingId, mailingName, sentDate, spamCount, uniqueClickCount, clickCount, uniqueOpenCount,
|
||||
openCount, invalidCount, blockedCount, failedCount, deliveredCount,
|
||||
sendCount, emailCount, bounceCount, unsubscribeCount);
|
||||
}
|
||||
|
||||
@ -16,6 +16,7 @@ namespace Surge365.MassEmailReact.Domain.Entities
|
||||
public string FilterQuery { get; set; } = "";
|
||||
public bool AllowWriteBack { get; set; }
|
||||
public bool IsActive { get; set; }
|
||||
public List<TargetColumn> Columns { get; set; } = new List<TargetColumn>();
|
||||
|
||||
public Target() { }
|
||||
private Target(int id, int serverId, string name, string databaseName, string viewName, string filterQuery, bool allowWriteBack, bool isActive)
|
||||
|
||||
39
Surge365.MassEmailReact.Domain/Entities/TargetColumn.cs
Normal file
39
Surge365.MassEmailReact.Domain/Entities/TargetColumn.cs
Normal file
@ -0,0 +1,39 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Surge365.MassEmailReact.Domain.Entities
|
||||
{
|
||||
public class TargetColumn
|
||||
{
|
||||
public int Id { get; private set; }
|
||||
public int TargetId { get; set; }
|
||||
public string TypeCode { get; set; } = "";
|
||||
public string DataTypeCode { get; set; } = "";
|
||||
public string Name { get; set; } = "";
|
||||
public bool WriteBack { get; set; }
|
||||
public bool IsEmailAddress { get; set; }
|
||||
|
||||
public TargetColumn() { }
|
||||
|
||||
private TargetColumn(int id, int targetId, string typeCode, string dataTypeCode, string name,
|
||||
bool writeBack, bool isEmailAddress)
|
||||
{
|
||||
Id = id;
|
||||
TargetId = targetId;
|
||||
TypeCode = typeCode;
|
||||
DataTypeCode = dataTypeCode;
|
||||
Name = name;
|
||||
WriteBack = writeBack;
|
||||
IsEmailAddress = isEmailAddress;
|
||||
}
|
||||
|
||||
public static TargetColumn Create(int id, int targetId, string typeCode, string dataTypeCode,
|
||||
string name, bool writeBack, bool isEmailAddress)
|
||||
{
|
||||
return new TargetColumn(id, targetId, typeCode, dataTypeCode, name, writeBack, isEmailAddress);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -18,6 +18,7 @@ namespace Surge365.MassEmailReact.Infrastructure.DapperMaps
|
||||
FluentMapper.Initialize(config =>
|
||||
{
|
||||
config.AddMap(new TargetMap());
|
||||
config.AddMap(new TargetColumnMap());
|
||||
config.AddMap(new ServerMap());
|
||||
config.AddMap(new TestEmailListMap());
|
||||
config.AddMap(new BouncedEmailMap());
|
||||
|
||||
@ -9,6 +9,7 @@ namespace Surge365.MassEmailReact.Infrastructure.DapperMaps
|
||||
{
|
||||
Map(m => m.MailingId).ToColumn("blast_key");
|
||||
Map(m => m.MailingName).ToColumn("blast_name");
|
||||
Map(m => m.SentDate).ToColumn("sent_date");
|
||||
Map(m => m.SpamCount).ToColumn("spam_count");
|
||||
Map(m => m.UniqueClickCount).ToColumn("unique_click_count");
|
||||
Map(m => m.ClickCount).ToColumn("click_count");
|
||||
|
||||
@ -0,0 +1,24 @@
|
||||
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 TargetColumnMap : EntityMap<TargetColumn>
|
||||
{
|
||||
public TargetColumnMap()
|
||||
{
|
||||
Map(tc => tc.Id).ToColumn("target_column_key");
|
||||
Map(tc => tc.TargetId).ToColumn("target_key");
|
||||
Map(tc => tc.TypeCode).ToColumn("target_column_type_code");
|
||||
Map(tc => tc.DataTypeCode).ToColumn("data_type_code");
|
||||
Map(tc => tc.Name).ToColumn("name");
|
||||
Map(tc => tc.WriteBack).ToColumn("write_back");
|
||||
Map(tc => tc.IsEmailAddress).ToColumn("is_email_address");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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, string? startDate, string? endDate)
|
||||
public async Task<List<Mailing>> GetByStatusAsync(string codes, 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, start_date = startDate, end_date = endDate }, commandType: CommandType.StoredProcedure)).ToList();
|
||||
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();
|
||||
}
|
||||
public async Task<List<MailingStatistic>> GetStatisticsByStatusAsync(string code, string? startDate, string? endDate)
|
||||
public async Task<List<MailingStatistic>> GetStatisticsByStatusAsync(string codes, 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, start_date = startDate, end_date = endDate }, commandType: CommandType.StoredProcedure)).ToList();
|
||||
return (await conn.QueryAsync<MailingStatistic>("mem_get_blast_statistic_by_status", new { blast_status_codes = codes, start_date = startDate, end_date = endDate }, commandType: CommandType.StoredProcedure)).ToList();
|
||||
}
|
||||
public async Task<MailingStatistic?> GetStatisticByIdAsync(int id)
|
||||
{
|
||||
@ -75,7 +75,7 @@ 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_available2", parameters, commandType: CommandType.StoredProcedure);
|
||||
await conn.ExecuteAsync("mem_is_blast_name_available", parameters, commandType: CommandType.StoredProcedure);
|
||||
|
||||
return parameters.Get<bool>("@available");
|
||||
}
|
||||
|
||||
@ -12,6 +12,7 @@ using System.Collections.Generic;
|
||||
using System.Data;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
@ -44,8 +45,22 @@ namespace Surge365.MassEmailReact.Infrastructure.Repositories
|
||||
ArgumentNullException.ThrowIfNull(_connectionStringName);
|
||||
|
||||
using SqlConnection conn = new SqlConnection(_config.GetConnectionString(_connectionStringName));
|
||||
await conn.OpenAsync();
|
||||
|
||||
return (await conn.QueryAsync<Target>("mem_get_target_by_id", new { target_key = targetKey }, commandType: CommandType.StoredProcedure)).FirstOrDefault();
|
||||
using var multi = await conn.QueryMultipleAsync(
|
||||
"mem_get_target_by_id",
|
||||
new { target_key = targetKey },
|
||||
commandType: CommandType.StoredProcedure);
|
||||
|
||||
// Read the first result set (Target)
|
||||
var target = await multi.ReadSingleOrDefaultAsync<Target>();
|
||||
if (target == null) return null;
|
||||
|
||||
// Read the second result set (TargetColumns)
|
||||
var columns = await multi.ReadAsync<TargetColumn>();
|
||||
target.Columns = columns.ToList();
|
||||
|
||||
return target;
|
||||
}
|
||||
public async Task<List<Target>> GetAllAsync(bool activeOnly = true)
|
||||
{
|
||||
@ -53,8 +68,31 @@ namespace Surge365.MassEmailReact.Infrastructure.Repositories
|
||||
ArgumentNullException.ThrowIfNull(_connectionStringName);
|
||||
|
||||
using SqlConnection conn = new SqlConnection(_config.GetConnectionString(_connectionStringName));
|
||||
await conn.OpenAsync();
|
||||
|
||||
return (await conn.QueryAsync<Target>("mem_get_target_all", new { active_only = activeOnly }, commandType: CommandType.StoredProcedure)).ToList();
|
||||
using var multi = await conn.QueryMultipleAsync(
|
||||
"mem_get_target_all",
|
||||
new { active_only = activeOnly },
|
||||
commandType: CommandType.StoredProcedure);
|
||||
|
||||
// Read the first result set (Targets)
|
||||
var targets = (await multi.ReadAsync<Target>()).ToList();
|
||||
if (!targets.Any()) return targets;
|
||||
|
||||
// Read the second result set (TargetColumns)
|
||||
var columns = (await multi.ReadAsync<TargetColumn>()).ToList();
|
||||
|
||||
// Map columns to their respective targets
|
||||
var targetDictionary = targets.ToDictionary(t => t.Id!.Value);
|
||||
foreach (var column in columns)
|
||||
{
|
||||
if (targetDictionary.TryGetValue(column.TargetId, out var target))
|
||||
{
|
||||
target.Columns.Add(column);
|
||||
}
|
||||
}
|
||||
|
||||
return targets;
|
||||
}
|
||||
|
||||
public async Task<int?> CreateAsync(Target target)
|
||||
@ -74,6 +112,8 @@ namespace Surge365.MassEmailReact.Infrastructure.Repositories
|
||||
parameters.Add("@filter_query", target.FilterQuery, DbType.String);
|
||||
parameters.Add("@allow_write_back", target.AllowWriteBack, DbType.Boolean);
|
||||
parameters.Add("@is_active", target.IsActive, DbType.Boolean);
|
||||
if(target.Columns != null)
|
||||
parameters.Add("@column_json", JsonSerializer.Serialize(target.Columns), DbType.String);
|
||||
|
||||
// Output parameter
|
||||
parameters.Add("@success", dbType: DbType.Boolean, direction: ParameterDirection.Output);
|
||||
@ -103,6 +143,8 @@ namespace Surge365.MassEmailReact.Infrastructure.Repositories
|
||||
parameters.Add("@filter_query", target.FilterQuery, DbType.String);
|
||||
parameters.Add("@allow_write_back", target.AllowWriteBack, DbType.Boolean);
|
||||
parameters.Add("@is_active", target.IsActive, DbType.Boolean);
|
||||
if (target.Columns != null)
|
||||
parameters.Add("@column_json", JsonSerializer.Serialize(target.Columns), DbType.String);
|
||||
|
||||
// Output parameter
|
||||
parameters.Add("@success", dbType: DbType.Boolean, direction: ParameterDirection.Output);
|
||||
@ -211,6 +253,7 @@ namespace Surge365.MassEmailReact.Infrastructure.Repositories
|
||||
targetSample.Columns[name] = new TargetSampleColumn
|
||||
{
|
||||
Name = name,
|
||||
DataType = dataType,
|
||||
Type = typeCode
|
||||
};
|
||||
}
|
||||
|
||||
@ -73,13 +73,13 @@ namespace Surge365.MassEmailReact.Infrastructure.Services
|
||||
{
|
||||
return await _mailingRepository.GetAllAsync(activeOnly);
|
||||
}
|
||||
public async Task<List<Mailing>> GetByStatusAsync(string statusCode, string? startDate, string? endDate)
|
||||
public async Task<List<Mailing>> GetByStatusAsync(string codes, string? startDate, string? endDate)
|
||||
{
|
||||
return await _mailingRepository.GetByStatusAsync(statusCode, startDate, endDate);
|
||||
return await _mailingRepository.GetByStatusAsync(codes, startDate, endDate);
|
||||
}
|
||||
public async Task<List<MailingStatistic>> GetStatisticsByStatusAsync(string code, string? startDate, string? endDate)
|
||||
public async Task<List<MailingStatistic>> GetStatisticsByStatusAsync(string codes, string? startDate, string? endDate)
|
||||
{
|
||||
return await _mailingRepository.GetStatisticsByStatusAsync(code, startDate, endDate);
|
||||
return await _mailingRepository.GetStatisticsByStatusAsync(codes, startDate, endDate);
|
||||
}
|
||||
public async Task<MailingStatistic?> GetStatisticByIdAsync(int id)
|
||||
{
|
||||
|
||||
@ -55,6 +55,12 @@ namespace Surge365.MassEmailReact.Infrastructure.Services
|
||||
target.AllowWriteBack = targetDto.AllowWriteBack;
|
||||
target.IsActive = targetDto.IsActive;
|
||||
|
||||
target.Columns = new List<TargetColumn>();
|
||||
foreach (var columnDto in targetDto.Columns)
|
||||
{
|
||||
target.Columns.Add(TargetColumn.Create(columnDto.Id ?? 0, target.Id ?? 0, columnDto.TypeCode, columnDto.DataTypeCode, columnDto.Name, columnDto.WriteBack, columnDto.IsEmailAddress));
|
||||
}
|
||||
|
||||
return await _targetRepository.CreateAsync(target);
|
||||
}
|
||||
public async Task<bool> UpdateAsync(TargetUpdateDto targetDto)
|
||||
@ -73,6 +79,11 @@ namespace Surge365.MassEmailReact.Infrastructure.Services
|
||||
target.AllowWriteBack = targetDto.AllowWriteBack;
|
||||
target.IsActive = targetDto.IsActive;
|
||||
|
||||
target.Columns = new List<TargetColumn>();
|
||||
foreach (var columnDto in targetDto.Columns)
|
||||
{
|
||||
target.Columns.Add(TargetColumn.Create(columnDto.Id ?? 0, target.Id ?? 0, columnDto.TypeCode, columnDto.DataTypeCode, columnDto.Name, columnDto.WriteBack, columnDto.IsEmailAddress));
|
||||
}
|
||||
return await _targetRepository.UpdateAsync(target);
|
||||
}
|
||||
public async Task<TargetSample?> TestTargetAsync(int targetId)
|
||||
@ -85,5 +96,12 @@ namespace Surge365.MassEmailReact.Infrastructure.Services
|
||||
|
||||
return await _targetRepository.TestTargetAsync(server.ServerName, server.Port, server.Username, server.Password, target.DatabaseName, target.ViewName, target.FilterQuery);
|
||||
}
|
||||
public async Task<TargetSample?> TestTargetAsync(TestTargetDto testTarget)
|
||||
{
|
||||
Server? server = await _serverRepository.GetByIdAsync(testTarget.ServerId, true);
|
||||
if (server == null) return null;
|
||||
|
||||
return await _targetRepository.TestTargetAsync(server.ServerName, server.Port, server.Username, server.Password, testTarget.DatabaseName, testTarget.ViewName, testTarget.FilterQuery);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,610 +0,0 @@
|
||||
|
||||
|
||||
//global variables
|
||||
var table;
|
||||
var allUsers = [];
|
||||
var user = null;
|
||||
|
||||
//login stuff
|
||||
function logout() {
|
||||
$.webMethod({
|
||||
'methodPage': 'UserMethods',
|
||||
'methodName': 'LogOut',
|
||||
'parameters': {},
|
||||
success: function (json) {
|
||||
|
||||
document.location.href = '/login.aspx';
|
||||
|
||||
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function checkLoggedInStatus() {
|
||||
$.webMethod({
|
||||
'methodPage': 'UserMethods',
|
||||
'methodName': 'CheckAuthToken',
|
||||
'parameters': {},
|
||||
success: function (json) {
|
||||
if ($.getBoolean(json.success)) {
|
||||
$.localStorage("session_currentUser", json.data);
|
||||
|
||||
|
||||
//set user info
|
||||
user = $.localStorage("session_currentUser");
|
||||
|
||||
//set franchise if it isn't set yet.
|
||||
//franchiseDeferred = $.Deferred();
|
||||
|
||||
//call back to page that called this method.
|
||||
// $.when(franchiseDeferred).done(function () {
|
||||
/*
|
||||
if ($.sessionStorage("Auth-Impersonate-Guid") != null) {
|
||||
//session is being impersonated, override name and signout
|
||||
$("#spanProfileName").html("Signed in as " + user.FirstName + ' ' + user.LastName);
|
||||
$("#spanProfileNameTitle").html("Signed in as " + user.FirstName + ' ' + user.LastName);
|
||||
}
|
||||
else {
|
||||
$("#spanProfileName").html(user.FirstName + ' ' + user.LastName);
|
||||
$("#spanProfileNameTitle").html(user.FirstName + ' ' + user.LastName);
|
||||
} */
|
||||
|
||||
// $("#spanMenuName").html(user.FirstName + ' ' + user.LastName);
|
||||
|
||||
// var userId = $.localStorage("session_currentUser").userId;
|
||||
// var imgid = userId + "&t=" + new Date().getTime();
|
||||
/*
|
||||
if (user.HasProfileImage) {
|
||||
$("#imgRightProfile").attr("src", "/webservices/getprofileimage.ashx?id=" + imgid);
|
||||
$("#imgRightProfile").on('error', function () {
|
||||
$("#imgRightProfile").attr("src", "/img/generic_avatar.jpg");
|
||||
});
|
||||
|
||||
$("#imgLeftProfile").attr("src", "/webservices/getprofileimage.ashx?id=" + imgid);
|
||||
$("#imgLeftProfile").on('error', function () {
|
||||
$("#imgLeftProfile").attr("src", "/img/generic_avatar.jpg");
|
||||
});
|
||||
|
||||
$("#imgMainProfile").attr("src", "/webservices/getprofileimage.ashx?id=" + imgid);
|
||||
$("#imgMainProfile").on('error', function () {
|
||||
$("#imgMainProfile").attr("src", "/img/generic_avatar.jpg");
|
||||
});
|
||||
}
|
||||
else {
|
||||
$("#imgRightProfile").attr("src", "/img/generic_avatar.jpg");
|
||||
$("#imgLeftProfile").attr("src", "/img/generic_avatar.jpg");
|
||||
$("#imgMainProfile").attr("src", "/img/generic_avatar.jpg");
|
||||
} */
|
||||
|
||||
loadPage();
|
||||
|
||||
}
|
||||
else {
|
||||
$.sessionStorage("redirect_url", document.location.href);
|
||||
document.location.href = '/login';
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function setCurrentFranchise(d) {
|
||||
$.usaHaulersDB.getFranchises().then((data) => {
|
||||
$.sessionStorage("currentFranchise", data);
|
||||
if (d != null) {
|
||||
d.resolve();
|
||||
}
|
||||
});
|
||||
|
||||
/*
|
||||
search = {};
|
||||
search.FranchiseCode = $.sessionStorage("franchiseCode");
|
||||
|
||||
$.webMethod({
|
||||
'methodPage': 'UserMethods/',
|
||||
'methodName': 'GetReport_Franchise',
|
||||
'parameters': { "search": search },
|
||||
success: function (json) {
|
||||
|
||||
if ($.getBoolean(json.success)) {
|
||||
$.sessionStorage("currentFranchise", json.data[0]);
|
||||
if (d != null) {
|
||||
d.resolve();
|
||||
}
|
||||
}
|
||||
}
|
||||
}); */
|
||||
|
||||
}
|
||||
|
||||
function createSetting(obj, settingTypeCode, value) {
|
||||
|
||||
//see if setting exists
|
||||
var bFound = false;
|
||||
for (var x = 0; x < obj.Settings.length; x++) {
|
||||
if (obj.Settings[x].SettingTypeCode == settingTypeCode) {
|
||||
bFound = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (bFound) {
|
||||
//existing setting
|
||||
obj.Settings[x].Value = value;
|
||||
}
|
||||
else {
|
||||
|
||||
|
||||
setting = {}
|
||||
setting.SettingTypeCode = settingTypeCode;
|
||||
setting.Value = value;
|
||||
|
||||
if (obj.Settings == null) {
|
||||
obj.Settings = [];
|
||||
}
|
||||
obj.Settings.push(setting);
|
||||
}
|
||||
}
|
||||
|
||||
function createCaseSetting(obj, settingTypeCode, value) {
|
||||
|
||||
//see if setting exists
|
||||
var bFound = false;
|
||||
for (var x = 0; x < obj.settings.length; x++) {
|
||||
if (obj.settings[x].case_setting_type_code == settingTypeCode) {
|
||||
bFound = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (bFound) {
|
||||
//existing setting
|
||||
obj.settings[x].value = value;
|
||||
}
|
||||
else {
|
||||
|
||||
|
||||
setting = {}
|
||||
setting.case_setting_type_code = settingTypeCode;
|
||||
setting.value = value;
|
||||
|
||||
if (obj.settings == null) {
|
||||
obj.settings = [];
|
||||
}
|
||||
obj.settings.push(setting);
|
||||
}
|
||||
}
|
||||
|
||||
function getReportSetting(settings, setting_field, setting_code) {
|
||||
var value = "";
|
||||
if (settings != null) {
|
||||
for (var x = 0; x < settings.length; x++) {
|
||||
setting = settings[x];
|
||||
if (setting[setting_field] == setting_code) {
|
||||
value = setting["value"];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function getSetting(settings, settingTypeCode) {
|
||||
var value = "";
|
||||
for (var x = 0; x < settings.length; x++) {
|
||||
setting = settings[x];
|
||||
if (setting.SettingTypeCode == settingTypeCode) {
|
||||
value = setting.Value;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
//web calls
|
||||
function callMethod(methodname, callback, sessionName, deferred, search) {
|
||||
|
||||
var params = {};
|
||||
|
||||
if (search == null) {
|
||||
search = {};
|
||||
}
|
||||
|
||||
|
||||
$.webMethod({
|
||||
'methodPage': 'UserMethods/',
|
||||
'methodName': methodname,
|
||||
'parameters': { "search": search },
|
||||
success: function (json) {
|
||||
|
||||
$.sessionStorage(sessionName, json.data);
|
||||
|
||||
if ($.getBoolean(json.success)) {
|
||||
if (deferred != null) {
|
||||
deferred.resolve(true);
|
||||
}
|
||||
if (callback != null) {
|
||||
callback();
|
||||
}
|
||||
}
|
||||
else {
|
||||
notify("Error", "There was an error performing this action. Please try again.")
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
function callSearchTransactions(methodname, callback, sessionName, deferred, search) {
|
||||
//REPEAT OF CALL METHOD,, JSON.DATA DOESN'T WORK HERE..
|
||||
//TODO MAKE DYNAMIC LATER.
|
||||
|
||||
var params = {};
|
||||
|
||||
if (search == null) {
|
||||
search = {};
|
||||
}
|
||||
|
||||
|
||||
$.webMethod({
|
||||
'methodPage': 'UserMethods/',
|
||||
'methodName': methodname,
|
||||
'parameters': { "search": search },
|
||||
success: function (json) {
|
||||
|
||||
$.sessionStorage(sessionName, json);
|
||||
|
||||
if ($.getBoolean(json.success)) {
|
||||
if (deferred != null) {
|
||||
deferred.resolve(true);
|
||||
}
|
||||
if (callback != null) {
|
||||
callback();
|
||||
}
|
||||
}
|
||||
else {
|
||||
notify("Error", "There was an error performing this action. Please try again.")
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
//overlay
|
||||
function showwait(item) {
|
||||
$("#waitOverlay").show();
|
||||
if (item) {
|
||||
item.show();
|
||||
|
||||
}
|
||||
else {
|
||||
$("#loading").show();
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
function hidewait(item) {
|
||||
$("#waitOverlay").hide();
|
||||
if (item) {
|
||||
item.hide();
|
||||
}
|
||||
else {
|
||||
$("#loading").hide();
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
//helper function
|
||||
function getParameterByName(name) {
|
||||
name = name.replace(/[\[]/, "\\[").replace(/[\]]/, "\\]");
|
||||
var regex = new RegExp("[\\?&]" + name + "=([^&#]*)"),
|
||||
results = regex.exec(location.search);
|
||||
return results == null ? "" : decodeURIComponent(results[1].replace(/\+/g, " "));
|
||||
}
|
||||
function getCurrency(val) {
|
||||
return '$' + parseFloat(val, 10).toFixed(2).replace(/(\d)(?=(\d{3})+\.)/g, "$1,").toString()
|
||||
}
|
||||
function getDate(val) {
|
||||
var newDate = new Date(Date.parse(val)).toLocaleDateString()
|
||||
return newDate;
|
||||
}
|
||||
function changeDateFormat(inputDate) { // expects Y-m-d
|
||||
var splitDate = inputDate.split('-');
|
||||
if (splitDate.count == 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var year = splitDate[0];
|
||||
var month = splitDate[1];
|
||||
var day = splitDate[2];
|
||||
|
||||
return month + '/' + day + '/' + year;
|
||||
}
|
||||
|
||||
function getTime(val) {
|
||||
var newDate = new Date(Date.parse(val)).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
||||
return newDate;
|
||||
}
|
||||
function getDateAndTime(val) {
|
||||
var newDate = new Date(Date.parse(val)).toLocaleDateString() + ' ' + new Date(Date.parse(val)).toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true })
|
||||
return newDate;
|
||||
}
|
||||
function getDateAndTimeAndSeconds(val) {
|
||||
var newDate = new Date(Date.parse(val)).toLocaleDateString() + ' ' + new Date(Date.parse(val)).toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true, second: '2-digit' })
|
||||
return newDate;
|
||||
}
|
||||
function notify(title, message) {
|
||||
$.gritter.add({
|
||||
title: title,
|
||||
text: message,
|
||||
sticky: false,
|
||||
time: '2000',
|
||||
|
||||
});
|
||||
}
|
||||
function getObjectByKey(key, value, objectArray) {
|
||||
for (var x = 0; x < objectArray.length; x++) {
|
||||
if (objectArray[x][key] == value) {
|
||||
return objectArray[x];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function formatSelect2Data(id, name, data) {
|
||||
var newdata = [];
|
||||
//var blank = { };
|
||||
// blank.id = "";
|
||||
//blank.text = "";
|
||||
//data.push(blank);
|
||||
for (var x = 0; x < data.length; x++) {
|
||||
var obj = {};
|
||||
obj.id = data[x][id];
|
||||
obj.text = data[x][name]
|
||||
newdata.push(obj);
|
||||
|
||||
}
|
||||
return newdata;
|
||||
}
|
||||
|
||||
function formatSelect2DataForReturnedBatched(id, name, data) {
|
||||
var newdata = [];
|
||||
//var blank = { };
|
||||
// blank.id = "";
|
||||
//blank.text = "";
|
||||
//data.push(blank);
|
||||
for (var x = 0; x < data.length; x++) {
|
||||
if (data[x].Type != "ShiftCharges") {
|
||||
var obj = {};
|
||||
obj.id = data[x][id];
|
||||
obj.text = data[x][name]
|
||||
newdata.push(obj);
|
||||
}
|
||||
|
||||
}
|
||||
return newdata;
|
||||
}
|
||||
|
||||
function formatSelect2DataForUser(id, data) {
|
||||
var newdata = [];
|
||||
//var blank = { };
|
||||
// blank.id = "";
|
||||
//blank.text = "";
|
||||
//data.push(blank);
|
||||
for (var x = 0; x < data.length; x++) {
|
||||
var obj = {};
|
||||
obj.id = data[x][id];
|
||||
obj.text = data[x]["FirstName"] + ' ' + data[x]["LastName"] + ' - ' + data[x]["userId"];
|
||||
newdata.push(obj);
|
||||
|
||||
}
|
||||
return newdata;
|
||||
}
|
||||
|
||||
//batches
|
||||
function fillDropDown(dropdown, method, session, deferred, fieldname, fieldvalue, placeholder) {
|
||||
if ($.sessionStorage(session) == null) {
|
||||
callMethod(method, fillDropDown, session, deferred, null);
|
||||
}
|
||||
else {
|
||||
var data = formatSelect2Data(fieldname, fieldvalue, $.sessionStorage(session));
|
||||
$(control).select2({
|
||||
placeholder: placeholder,
|
||||
allowClear: true,
|
||||
selectOnClose: false,
|
||||
//closeOnSelect: false,
|
||||
data: data,
|
||||
width: "200px"
|
||||
});
|
||||
batchDeferred.resolve(true);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var currentDropdown;
|
||||
//location
|
||||
function fillLocationsDropDown(deferred) {
|
||||
|
||||
|
||||
|
||||
var search = {};
|
||||
if (!$.userHasRole("Administrators")) {
|
||||
search.userId = $.localStorage("session_currentUser").userId;
|
||||
}
|
||||
else {
|
||||
search = null;
|
||||
}
|
||||
|
||||
$.webMethod({
|
||||
'methodPage': 'UserMethods/',
|
||||
'methodName': 'SearchLocations',
|
||||
'parameters': { "search": search },
|
||||
success: function (json) {
|
||||
|
||||
$.sessionStorage("session_locations", json.data);
|
||||
|
||||
if ($.getBoolean(json.success)) {
|
||||
var data = formatSelect2Data("ID", "Name", $.sessionStorage("session_locations"));
|
||||
|
||||
if ($.userHasRole("Administrators")) {
|
||||
$("#selLocation").select2({
|
||||
placeholder: "Select a location...",
|
||||
allowClear: true,
|
||||
selectOnClose: false,
|
||||
//closeOnSelect: false,
|
||||
data: data
|
||||
});
|
||||
$("#selLocation").enable(true);
|
||||
}
|
||||
else {
|
||||
$("#selLocation").select2({
|
||||
|
||||
data: data
|
||||
});
|
||||
$("#selLocation").select2('val', data[0].id);
|
||||
$("#selLocation").enable(false);
|
||||
}
|
||||
|
||||
if (deferred != null) {
|
||||
deferred.resolve(true);
|
||||
}
|
||||
}
|
||||
else {
|
||||
notify("Error", "There was an error performing this action. Please try again.")
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
}
|
||||
|
||||
var currentSpinner = null
|
||||
function spin2(spin) {
|
||||
//spinner = $("#spanSpinner");
|
||||
currentSpinner.show();
|
||||
|
||||
if (spin) {
|
||||
rotation = function () {
|
||||
currentSpinner.rotate({
|
||||
angle: 0,
|
||||
animateTo: 360,
|
||||
callback: rotation
|
||||
});
|
||||
}
|
||||
rotation();
|
||||
}
|
||||
else {
|
||||
currentSpinner.stopRotate();
|
||||
currentSpinner.hide();
|
||||
}
|
||||
}
|
||||
|
||||
function spin(spin, spinner) {
|
||||
//spinner = $("#spanSpinner");
|
||||
spinner.show();
|
||||
|
||||
if (spin) {
|
||||
rotation = function () {
|
||||
spinner.rotate({
|
||||
angle: 0,
|
||||
animateTo: 360,
|
||||
callback: rotation
|
||||
});
|
||||
}
|
||||
rotation();
|
||||
}
|
||||
else {
|
||||
spinner.stopRotate();
|
||||
spinner.hide();
|
||||
}
|
||||
}
|
||||
|
||||
//employee
|
||||
function fillEmployeesDropDown(deferred, dropdown) {
|
||||
|
||||
if (dropdown == null) {
|
||||
dropdown = $("#selEmployee");
|
||||
}
|
||||
$.webMethod({
|
||||
'methodPage': 'UserMethods/',
|
||||
'methodName': 'GetUsers',
|
||||
'parameters': { "activeOnly": false },
|
||||
success: function (json) {
|
||||
|
||||
$.sessionStorage("session_users", json.data);
|
||||
|
||||
if ($.getBoolean(json.success)) {
|
||||
var data = formatSelect2DataForUser("userId", $.sessionStorage("session_users"));
|
||||
dropdown.select2({
|
||||
placeholder: "Select an employee...",
|
||||
allowClear: true,
|
||||
selectOnClose: false,
|
||||
//closeOnSelect: false,
|
||||
data: data
|
||||
});
|
||||
|
||||
if (deferred != null) {
|
||||
deferred.resolve(true);
|
||||
}
|
||||
}
|
||||
else {
|
||||
notify("Error", "There was an error performing this action. Please try again.")
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
//new imove global functions
|
||||
|
||||
|
||||
function stripCharacters(str) {
|
||||
return str.replace(/[-' ]/g, '').toUpperCase();
|
||||
}
|
||||
|
||||
function searchUsers(d) {
|
||||
search = {};
|
||||
|
||||
$.webMethod({
|
||||
'methodPage': 'UserMethods/',
|
||||
'methodName': 'SearchUsers',
|
||||
'parameters': { "search": search },
|
||||
success: function (json) {
|
||||
|
||||
|
||||
if ($.getBoolean(json.success)) {
|
||||
|
||||
allUsers = json.data;
|
||||
d.resolve();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function growlWarn(msg) {
|
||||
$.bootstrapGrowl("<strong>" + msg + "</strong>", {
|
||||
type: 'warning',
|
||||
align: 'center',
|
||||
width: 'auto',
|
||||
allow_dismiss: true,
|
||||
offset: { from: 'top', amount: 60 }
|
||||
});
|
||||
}
|
||||
function growlSuccess(msg) {
|
||||
$.bootstrapGrowl("<strong>" + msg + "</strong>", {
|
||||
type: 'success',
|
||||
align: 'center',
|
||||
width: 'auto',
|
||||
allow_dismiss: true,
|
||||
offset: { from: 'top', amount: 60 }
|
||||
});
|
||||
}
|
||||
|
||||
function getUser(users, id) {
|
||||
for (var x = 0; x < users.length; x++) {
|
||||
if (users[x].userId == id) {
|
||||
return users[x];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,342 +1,342 @@
|
||||
//iMove utility functions
|
||||
////iMove utility functions
|
||||
|
||||
|
||||
//ie is the version of IE running.
|
||||
var ie = (function () {
|
||||
////ie is the version of IE running.
|
||||
//var ie = (function () {
|
||||
|
||||
var undef,
|
||||
v = 3,
|
||||
div = document.createElement('div'),
|
||||
all = div.getElementsByTagName('i');
|
||||
// var undef,
|
||||
// v = 3,
|
||||
// div = document.createElement('div'),
|
||||
// all = div.getElementsByTagName('i');
|
||||
|
||||
while (
|
||||
div.innerHTML = '<!--[if gt IE ' + (++v) + ']><i></i><![endif]-->',
|
||||
all[0]
|
||||
);
|
||||
// while (
|
||||
// div.innerHTML = '<!--[if gt IE ' + (++v) + ']><i></i><![endif]-->',
|
||||
// all[0]
|
||||
// );
|
||||
|
||||
return v > 4 ? v : undef;
|
||||
// return v > 4 ? v : undef;
|
||||
|
||||
} ());
|
||||
//} ());
|
||||
|
||||
$(document).ajaxSend(function (event, xhr, settings) {
|
||||
var authToken = $.cookie('Auth-Token');
|
||||
if (authToken != null && authToken != undefined && authToken.trim().length > 0) {
|
||||
xhr.setRequestHeader('Auth-Token', authToken);
|
||||
}
|
||||
//$(document).ajaxSend(function (event, xhr, settings) {
|
||||
// var authToken = $.cookie('Auth-Token');
|
||||
// if (authToken != null && authToken != undefined && authToken.trim().length > 0) {
|
||||
// xhr.setRequestHeader('Auth-Token', authToken);
|
||||
// }
|
||||
|
||||
var impersonateGuid = $.getParameterByName("impersonateid");
|
||||
if (impersonateGuid != null && impersonateGuid != undefined && impersonateGuid.trim().length > 0) {
|
||||
$.sessionStorage('Auth-Impersonate-Guid', impersonateGuid);
|
||||
}
|
||||
// var impersonateGuid = $.getParameterByName("impersonateid");
|
||||
// if (impersonateGuid != null && impersonateGuid != undefined && impersonateGuid.trim().length > 0) {
|
||||
// $.sessionStorage('Auth-Impersonate-Guid', impersonateGuid);
|
||||
// }
|
||||
|
||||
impersonateGuid = $.sessionStorage('Auth-Impersonate-Guid');
|
||||
if (impersonateGuid != null && impersonateGuid != undefined && impersonateGuid.trim().length > 0) {
|
||||
xhr.setRequestHeader('Auth-Impersonate-Guid', impersonateGuid);
|
||||
}
|
||||
// impersonateGuid = $.sessionStorage('Auth-Impersonate-Guid');
|
||||
// if (impersonateGuid != null && impersonateGuid != undefined && impersonateGuid.trim().length > 0) {
|
||||
// xhr.setRequestHeader('Auth-Impersonate-Guid', impersonateGuid);
|
||||
// }
|
||||
|
||||
var franchiseCode = $.sessionStorage('franchiseCode');
|
||||
if (franchiseCode != null && franchiseCode != undefined && franchiseCode.trim().length > 0) {
|
||||
xhr.setRequestHeader('Auth-Current-Franchise', franchiseCode);
|
||||
}
|
||||
});
|
||||
// var franchiseCode = $.sessionStorage('franchiseCode');
|
||||
// if (franchiseCode != null && franchiseCode != undefined && franchiseCode.trim().length > 0) {
|
||||
// xhr.setRequestHeader('Auth-Current-Franchise', franchiseCode);
|
||||
// }
|
||||
//});
|
||||
|
||||
$.webMethod = function (options) {
|
||||
var settings = $.extend({
|
||||
'protocol': location.protocol, //http or https
|
||||
'methodPage': '', //This should be something like UserMethods
|
||||
'methodName': '', //Something like Createuser
|
||||
'contentType': 'application/json; charset=utf-8',
|
||||
'dataType': 'json',
|
||||
'async': true,
|
||||
'cache': false,
|
||||
timeout: 300000,
|
||||
'parameters': {},
|
||||
success: function (response) { },
|
||||
error: function (xhr, error, error_thrown) { }
|
||||
}, options);
|
||||
//$.webMethod = function (options) {
|
||||
// var settings = $.extend({
|
||||
// 'protocol': location.protocol, //http or https
|
||||
// 'methodPage': '', //This should be something like UserMethods
|
||||
// 'methodName': '', //Something like Createuser
|
||||
// 'contentType': 'application/json; charset=utf-8',
|
||||
// 'dataType': 'json',
|
||||
// 'async': true,
|
||||
// 'cache': false,
|
||||
// timeout: 300000,
|
||||
// 'parameters': {},
|
||||
// success: function (response) { },
|
||||
// error: function (xhr, error, error_thrown) { }
|
||||
// }, options);
|
||||
|
||||
if (settings.protocol.indexOf(':') < 0) {
|
||||
settings.protocol = settings.protocol + ':';
|
||||
}
|
||||
// if (settings.protocol.indexOf(':') < 0) {
|
||||
// settings.protocol = settings.protocol + ':';
|
||||
// }
|
||||
|
||||
var result;
|
||||
var baseUrl = window.API_BASE_URL;
|
||||
//var baseUrl = $("base").attr("href");
|
||||
if (baseUrl === undefined || baseUrl === null || baseUrl.length === 0)
|
||||
baseUrl = "";
|
||||
if (baseUrl.length > 0 && baseUrl[baseUrl.length - 1] !== "/")
|
||||
baseUrl = baseUrl + "/";
|
||||
var url = baseUrl + settings.methodPage + (settings.methodName === undefined || settings.methodName === null || settings.methodName.length === 0 ? "" : "/" + settings.methodName);
|
||||
$.ajax({
|
||||
type: "POST",
|
||||
url: url,
|
||||
data: JSON.stringify(settings.parameters),
|
||||
contentType: settings.contentType,
|
||||
dataType: settings.dataType,
|
||||
async: settings.async,
|
||||
cache: settings.cache,
|
||||
timeout: settings.timeout,
|
||||
beforeSend: null,
|
||||
success: function (value, textStatus, request) {
|
||||
if (value.hasOwnProperty("d"))
|
||||
result = $.parseJSON(value.d);
|
||||
else if (typeof value === 'object')
|
||||
result = value;
|
||||
else
|
||||
result = $.parseJSON(value);
|
||||
var authToken = request.getResponseHeader('Auth-Token');
|
||||
var loggedIn = $.getBoolean(request.getResponseHeader('usahl_logged_in'));
|
||||
$.cookie.raw = true;
|
||||
// var result;
|
||||
// var baseUrl = window.API_BASE_URL;
|
||||
// //var baseUrl = $("base").attr("href");
|
||||
// if (baseUrl === undefined || baseUrl === null || baseUrl.length === 0)
|
||||
// baseUrl = "";
|
||||
// if (baseUrl.length > 0 && baseUrl[baseUrl.length - 1] !== "/")
|
||||
// baseUrl = baseUrl + "/";
|
||||
// var url = baseUrl + settings.methodPage + (settings.methodName === undefined || settings.methodName === null || settings.methodName.length === 0 ? "" : "/" + settings.methodName);
|
||||
// $.ajax({
|
||||
// type: "POST",
|
||||
// url: url,
|
||||
// data: JSON.stringify(settings.parameters),
|
||||
// contentType: settings.contentType,
|
||||
// dataType: settings.dataType,
|
||||
// async: settings.async,
|
||||
// cache: settings.cache,
|
||||
// timeout: settings.timeout,
|
||||
// beforeSend: null,
|
||||
// success: function (value, textStatus, request) {
|
||||
// if (value.hasOwnProperty("d"))
|
||||
// result = $.parseJSON(value.d);
|
||||
// else if (typeof value === 'object')
|
||||
// result = value;
|
||||
// else
|
||||
// result = $.parseJSON(value);
|
||||
// var authToken = request.getResponseHeader('Auth-Token');
|
||||
// var loggedIn = $.getBoolean(request.getResponseHeader('usahl_logged_in'));
|
||||
// $.cookie.raw = true;
|
||||
|
||||
if ($.getRememberMe() === true) {
|
||||
$.cookie('Auth-Token', authToken, { expires: 14, path: '/' });
|
||||
$.cookie('usahl_logged_in', loggedIn, { expires: 14, path: '/' });
|
||||
}
|
||||
else {
|
||||
$.cookie('Auth-Token', authToken, { expires: 365, path: '/' });
|
||||
$.cookie('usahl_logged_in', loggedIn, { expires: 365, path: '/' });
|
||||
}
|
||||
if (settings.success !== undefined)
|
||||
settings.success(result);
|
||||
},
|
||||
error: function (xhr, error, errorThrown) {
|
||||
if (settings.error !== undefined)
|
||||
settings.error(xhr, error, errorThrown);
|
||||
}
|
||||
});
|
||||
}
|
||||
// if ($.getRememberMe() === true) {
|
||||
// $.cookie('Auth-Token', authToken, { expires: 14, path: '/' });
|
||||
// $.cookie('usahl_logged_in', loggedIn, { expires: 14, path: '/' });
|
||||
// }
|
||||
// else {
|
||||
// $.cookie('Auth-Token', authToken, { expires: 365, path: '/' });
|
||||
// $.cookie('usahl_logged_in', loggedIn, { expires: 365, path: '/' });
|
||||
// }
|
||||
// if (settings.success !== undefined)
|
||||
// settings.success(result);
|
||||
// },
|
||||
// error: function (xhr, error, errorThrown) {
|
||||
// if (settings.error !== undefined)
|
||||
// settings.error(xhr, error, errorThrown);
|
||||
// }
|
||||
// });
|
||||
//}
|
||||
|
||||
$.webMethodAsync = async function (options) {
|
||||
var settings = $.extend({
|
||||
'protocol': location.protocol, //http or https
|
||||
'methodPage': '', //This should be something like UserMethods.
|
||||
'methodName': '', //Something like Createuser
|
||||
'contentType': 'application/json; charset=utf-8',
|
||||
'dataType': 'json',
|
||||
'async': true,
|
||||
'cache': false,
|
||||
timeout: 300000,
|
||||
'parameters': {},
|
||||
success: function (response) { },
|
||||
error: function (xhr, error, error_thrown) { }
|
||||
}, options);
|
||||
//$.webMethodAsync = async function (options) {
|
||||
// var settings = $.extend({
|
||||
// 'protocol': location.protocol, //http or https
|
||||
// 'methodPage': '', //This should be something like UserMethods.
|
||||
// 'methodName': '', //Something like Createuser
|
||||
// 'contentType': 'application/json; charset=utf-8',
|
||||
// 'dataType': 'json',
|
||||
// 'async': true,
|
||||
// 'cache': false,
|
||||
// timeout: 300000,
|
||||
// 'parameters': {},
|
||||
// success: function (response) { },
|
||||
// error: function (xhr, error, error_thrown) { }
|
||||
// }, options);
|
||||
|
||||
if (settings.protocol.indexOf(':') < 0) {
|
||||
settings.protocol = settings.protocol + ':';
|
||||
}
|
||||
// if (settings.protocol.indexOf(':') < 0) {
|
||||
// settings.protocol = settings.protocol + ':';
|
||||
// }
|
||||
|
||||
var result;
|
||||
var baseUrl = window.API_BASE_URL;
|
||||
//var baseUrl = $("base").attr("href");
|
||||
if (baseUrl === undefined || baseUrl === null || baseUrl.length === 0)
|
||||
baseUrl = "";
|
||||
if (baseUrl.length > 0 && baseUrl[baseUrl.length - 1] !== "/")
|
||||
baseUrl = baseUrl + "/";
|
||||
var url = baseUrl + settings.methodPage + (settings.methodName === undefined || settings.methodName === null || settings.methodName.length === 0 ? "" : "/" + settings.methodName);
|
||||
return $.ajax({
|
||||
type: "POST",
|
||||
url: url,
|
||||
data: JSON.stringify(settings.parameters),
|
||||
contentType: settings.contentType,
|
||||
dataType: settings.dataType,
|
||||
async: settings.async,
|
||||
cache: settings.cache,
|
||||
timeout: settings.timeout,
|
||||
beforeSend: null,
|
||||
success: function (value, textStatus, request) {
|
||||
if (value.hasOwnProperty("d"))
|
||||
result = $.parseJSON(value.d);
|
||||
else if (typeof value === 'object')
|
||||
result = value;
|
||||
else
|
||||
result = $.parseJSON(value);
|
||||
var authToken = request.getResponseHeader('Auth-Token');
|
||||
var loggedIn = $.getBoolean(request.getResponseHeader('usahl_logged_in'));
|
||||
$.cookie.raw = true;
|
||||
// var result;
|
||||
// var baseUrl = window.API_BASE_URL;
|
||||
// //var baseUrl = $("base").attr("href");
|
||||
// if (baseUrl === undefined || baseUrl === null || baseUrl.length === 0)
|
||||
// baseUrl = "";
|
||||
// if (baseUrl.length > 0 && baseUrl[baseUrl.length - 1] !== "/")
|
||||
// baseUrl = baseUrl + "/";
|
||||
// var url = baseUrl + settings.methodPage + (settings.methodName === undefined || settings.methodName === null || settings.methodName.length === 0 ? "" : "/" + settings.methodName);
|
||||
// return $.ajax({
|
||||
// type: "POST",
|
||||
// url: url,
|
||||
// data: JSON.stringify(settings.parameters),
|
||||
// contentType: settings.contentType,
|
||||
// dataType: settings.dataType,
|
||||
// async: settings.async,
|
||||
// cache: settings.cache,
|
||||
// timeout: settings.timeout,
|
||||
// beforeSend: null,
|
||||
// success: function (value, textStatus, request) {
|
||||
// if (value.hasOwnProperty("d"))
|
||||
// result = $.parseJSON(value.d);
|
||||
// else if (typeof value === 'object')
|
||||
// result = value;
|
||||
// else
|
||||
// result = $.parseJSON(value);
|
||||
// var authToken = request.getResponseHeader('Auth-Token');
|
||||
// var loggedIn = $.getBoolean(request.getResponseHeader('usahl_logged_in'));
|
||||
// $.cookie.raw = true;
|
||||
|
||||
if ($.getRememberMe() === true) {
|
||||
$.cookie('Auth-Token', authToken, { expires: 14, path: '/' });
|
||||
$.cookie('usahl_logged_in', loggedIn, { expires: 14, path: '/' });
|
||||
}
|
||||
else {
|
||||
$.cookie('Auth-Token', authToken, { expires: 365, path: '/' });
|
||||
$.cookie('usahl_logged_in', loggedIn, { expires: 365, path: '/' });
|
||||
}
|
||||
if (settings.success !== undefined)
|
||||
settings.success(result);
|
||||
},
|
||||
error: function (xhr, error, errorThrown) {
|
||||
if (settings.error !== undefined)
|
||||
settings.error(xhr, error, errorThrown);
|
||||
}
|
||||
});
|
||||
}
|
||||
// if ($.getRememberMe() === true) {
|
||||
// $.cookie('Auth-Token', authToken, { expires: 14, path: '/' });
|
||||
// $.cookie('usahl_logged_in', loggedIn, { expires: 14, path: '/' });
|
||||
// }
|
||||
// else {
|
||||
// $.cookie('Auth-Token', authToken, { expires: 365, path: '/' });
|
||||
// $.cookie('usahl_logged_in', loggedIn, { expires: 365, path: '/' });
|
||||
// }
|
||||
// if (settings.success !== undefined)
|
||||
// settings.success(result);
|
||||
// },
|
||||
// error: function (xhr, error, errorThrown) {
|
||||
// if (settings.error !== undefined)
|
||||
// settings.error(xhr, error, errorThrown);
|
||||
// }
|
||||
// });
|
||||
//}
|
||||
|
||||
$.getBoolean = function (variable) {
|
||||
var vtype;
|
||||
var toReturn;
|
||||
//$.getBoolean = function (variable) {
|
||||
// var vtype;
|
||||
// var toReturn;
|
||||
|
||||
if (variable != null) {
|
||||
switch (typeof (variable)) {
|
||||
case 'boolean':
|
||||
vtype = "boolean";
|
||||
return variable;
|
||||
break;
|
||||
// if (variable != null) {
|
||||
// switch (typeof (variable)) {
|
||||
// case 'boolean':
|
||||
// vtype = "boolean";
|
||||
// return variable;
|
||||
// break;
|
||||
|
||||
case 'number':
|
||||
vtype = "number";
|
||||
if (variable == 0)
|
||||
toReturn = false;
|
||||
else toReturn = true;
|
||||
break;
|
||||
// case 'number':
|
||||
// vtype = "number";
|
||||
// if (variable == 0)
|
||||
// toReturn = false;
|
||||
// else toReturn = true;
|
||||
// break;
|
||||
|
||||
case 'string':
|
||||
vtype = "string";
|
||||
if (variable.toLowerCase() == "true" || variable.toLowerCase() == "yes")
|
||||
toReturn = true;
|
||||
else if (variable.toLowerCase() == "false" || variable.toLowerCase() == "no")
|
||||
toReturn = false;
|
||||
else if (variable.length > 0)
|
||||
toReturn = true;
|
||||
else if (variable.length == 0)
|
||||
toReturn = false;
|
||||
break;
|
||||
// case 'string':
|
||||
// vtype = "string";
|
||||
// if (variable.toLowerCase() == "true" || variable.toLowerCase() == "yes")
|
||||
// toReturn = true;
|
||||
// else if (variable.toLowerCase() == "false" || variable.toLowerCase() == "no")
|
||||
// toReturn = false;
|
||||
// else if (variable.length > 0)
|
||||
// toReturn = true;
|
||||
// else if (variable.length == 0)
|
||||
// toReturn = false;
|
||||
// break;
|
||||
|
||||
}
|
||||
// }
|
||||
|
||||
return toReturn;
|
||||
}
|
||||
};
|
||||
// return toReturn;
|
||||
// }
|
||||
//};
|
||||
|
||||
$.isLoggedIn = function () {
|
||||
return $.getBoolean($.cookie('usahl_logged_in'));
|
||||
}
|
||||
//$.isLoggedIn = function () {
|
||||
// return $.getBoolean($.cookie('usahl_logged_in'));
|
||||
//}
|
||||
|
||||
//Send the auth-token in all ajax requests
|
||||
$.ajaxSetup({
|
||||
beforeSend: function (xhr, settings) {
|
||||
xhr.setRequestHeader('Auth-Token', $.cookie('Auth-Token'));
|
||||
}
|
||||
});
|
||||
////Send the auth-token in all ajax requests
|
||||
//$.ajaxSetup({
|
||||
// beforeSend: function (xhr, settings) {
|
||||
// xhr.setRequestHeader('Auth-Token', $.cookie('Auth-Token'));
|
||||
// }
|
||||
//});
|
||||
|
||||
$.sessionStorage = function (key, value) {
|
||||
if (value === undefined) {
|
||||
var val = window.sessionStorage.getItem(key);
|
||||
if ((/^usahl_json/).test(val)) {
|
||||
val = val.substring(11, val.length);
|
||||
val = $.parseJSON(val);
|
||||
}
|
||||
return val;
|
||||
}
|
||||
else {
|
||||
var val = value;
|
||||
if (typeof value === 'object') {
|
||||
val = "usahl_json:" + JSON.stringify(value);
|
||||
}
|
||||
//$.sessionStorage = function (key, value) {
|
||||
// if (value === undefined) {
|
||||
// var val = window.sessionStorage.getItem(key);
|
||||
// if ((/^usahl_json/).test(val)) {
|
||||
// val = val.substring(11, val.length);
|
||||
// val = $.parseJSON(val);
|
||||
// }
|
||||
// return val;
|
||||
// }
|
||||
// else {
|
||||
// var val = value;
|
||||
// if (typeof value === 'object') {
|
||||
// val = "usahl_json:" + JSON.stringify(value);
|
||||
// }
|
||||
|
||||
window.sessionStorage.setItem(key, val);
|
||||
}
|
||||
};
|
||||
// window.sessionStorage.setItem(key, val);
|
||||
// }
|
||||
//};
|
||||
|
||||
$.sessionStorageClear = function () {
|
||||
window.sessionStorage.clear();
|
||||
};
|
||||
//$.sessionStorageClear = function () {
|
||||
// window.sessionStorage.clear();
|
||||
//};
|
||||
|
||||
$.sessionStorageRemove = function (key) {
|
||||
window.sessionStorage.removeItem(key);
|
||||
};
|
||||
//$.sessionStorageRemove = function (key) {
|
||||
// window.sessionStorage.removeItem(key);
|
||||
//};
|
||||
|
||||
$.localStorage = function (key, value) {
|
||||
if (value === undefined) {
|
||||
var val = window.localStorage.getItem(key);
|
||||
if ((/^usahl_json/).test(val)) {
|
||||
val = val.substring(11, val.length);
|
||||
val = $.parseJSON(val);
|
||||
}
|
||||
return val;
|
||||
}
|
||||
else {
|
||||
var val = value;
|
||||
if (typeof value === 'object') {
|
||||
val = "usahl_json:" + JSON.stringify(value);
|
||||
}
|
||||
//$.localStorage = function (key, value) {
|
||||
// if (value === undefined) {
|
||||
// var val = window.localStorage.getItem(key);
|
||||
// if ((/^usahl_json/).test(val)) {
|
||||
// val = val.substring(11, val.length);
|
||||
// val = $.parseJSON(val);
|
||||
// }
|
||||
// return val;
|
||||
// }
|
||||
// else {
|
||||
// var val = value;
|
||||
// if (typeof value === 'object') {
|
||||
// val = "usahl_json:" + JSON.stringify(value);
|
||||
// }
|
||||
|
||||
window.localStorage.setItem(key, val);
|
||||
}
|
||||
};
|
||||
// window.localStorage.setItem(key, val);
|
||||
// }
|
||||
//};
|
||||
|
||||
$.localStorageClear = function () {
|
||||
window.localStorage.clear();
|
||||
};
|
||||
//$.localStorageClear = function () {
|
||||
// window.localStorage.clear();
|
||||
//};
|
||||
|
||||
$.localStorageRemove = function (key) {
|
||||
window.localStorage.removeItem(key);
|
||||
};
|
||||
//$.localStorageRemove = function (key) {
|
||||
// window.localStorage.removeItem(key);
|
||||
//};
|
||||
|
||||
$.setRememberMe = function (rememberMe) {
|
||||
$.sessionStorage("usahl_remember_me", rememberMe);
|
||||
}
|
||||
$.getRememberMe = function () {
|
||||
return true;
|
||||
/*
|
||||
var rememberMe = $.sessionStorage("usahl_remember_me");
|
||||
if (rememberMe === undefined || rememberMe == null) {
|
||||
return false;
|
||||
}
|
||||
//$.setRememberMe = function (rememberMe) {
|
||||
// $.sessionStorage("usahl_remember_me", rememberMe);
|
||||
//}
|
||||
//$.getRememberMe = function () {
|
||||
// return true;
|
||||
// /*
|
||||
// var rememberMe = $.sessionStorage("usahl_remember_me");
|
||||
// if (rememberMe === undefined || rememberMe == null) {
|
||||
// return false;
|
||||
// }
|
||||
|
||||
return $.getBoolean(rememberMe);
|
||||
*/
|
||||
}
|
||||
// return $.getBoolean(rememberMe);
|
||||
// */
|
||||
//}
|
||||
|
||||
$.userHasPermission = function (permissionCode) {
|
||||
var allowed = false;
|
||||
var user = $.localStorage("session_currentUser");
|
||||
$.each(user.Permissions, function () {
|
||||
if ($.compareStrings(this.Code, permissionCode)) {
|
||||
allowed = true;
|
||||
}
|
||||
});
|
||||
return allowed;
|
||||
}
|
||||
//$.userHasPermission = function (permissionCode) {
|
||||
// var allowed = false;
|
||||
// var user = $.localStorage("session_currentUser");
|
||||
// $.each(user.Permissions, function () {
|
||||
// if ($.compareStrings(this.Code, permissionCode)) {
|
||||
// allowed = true;
|
||||
// }
|
||||
// });
|
||||
// return allowed;
|
||||
//}
|
||||
|
||||
$.userHasRole = function (role) {
|
||||
var hasRole = false;
|
||||
var user = $.localStorage("session_currentUser");
|
||||
$.each(user.Roles, function () {
|
||||
if ($.compareStrings(this.FriendlyName, role)) {
|
||||
hasRole = true;
|
||||
}
|
||||
});
|
||||
return hasRole;
|
||||
}
|
||||
//$.userHasRole = function (role) {
|
||||
// var hasRole = false;
|
||||
// var user = $.localStorage("session_currentUser");
|
||||
// $.each(user.Roles, function () {
|
||||
// if ($.compareStrings(this.FriendlyName, role)) {
|
||||
// hasRole = true;
|
||||
// }
|
||||
// });
|
||||
// return hasRole;
|
||||
//}
|
||||
|
||||
$.checkForRole = function (user, role) {
|
||||
var hasRole = false;
|
||||
$.each(user.Roles, function () {
|
||||
if ($.compareStrings(this.FriendlyName, role)) {
|
||||
hasRole = true;
|
||||
}
|
||||
});
|
||||
return hasRole;
|
||||
}
|
||||
//$.checkForRole = function (user, role) {
|
||||
// var hasRole = false;
|
||||
// $.each(user.Roles, function () {
|
||||
// if ($.compareStrings(this.FriendlyName, role)) {
|
||||
// hasRole = true;
|
||||
// }
|
||||
// });
|
||||
// return hasRole;
|
||||
//}
|
||||
|
||||
$.checkForRoleCode = function (user, role) {
|
||||
var hasRole = false;
|
||||
$.each(user.roles, function () {
|
||||
if ($.compareStrings(this.role_code, role)) {
|
||||
hasRole = true;
|
||||
}
|
||||
});
|
||||
return hasRole;
|
||||
}
|
||||
//$.checkForRoleCode = function (user, role) {
|
||||
// var hasRole = false;
|
||||
// $.each(user.roles, function () {
|
||||
// if ($.compareStrings(this.role_code, role)) {
|
||||
// hasRole = true;
|
||||
// }
|
||||
// });
|
||||
// return hasRole;
|
||||
//}
|
||||
|
||||
$.compareStrings = function (string1, string2) {
|
||||
return string1.toLowerCase() === string2.toLowerCase();
|
||||
}
|
||||
//$.compareStrings = function (string1, string2) {
|
||||
// return string1.toLowerCase() === string2.toLowerCase();
|
||||
//}
|
||||
|
||||
$.getParameterByName = function (name) {
|
||||
name = name.replace(/[\[]/, "\\[").replace(/[\]]/, "\\]");
|
||||
var regex = new RegExp("[\\?&]" + name + "=([^&#]*)"),
|
||||
results = regex.exec(location.search);
|
||||
return results == null ? "" : decodeURIComponent(results[1].replace(/\+/g, " "));
|
||||
}
|
||||
//$.getParameterByName = function (name) {
|
||||
// name = name.replace(/[\[]/, "\\[").replace(/[\]]/, "\\]");
|
||||
// var regex = new RegExp("[\\?&]" + name + "=([^&#]*)"),
|
||||
// results = regex.exec(location.search);
|
||||
// return results == null ? "" : decodeURIComponent(results[1].replace(/\+/g, " "));
|
||||
//}
|
||||
18
Surge365.MassEmailReact.Web/public/web.config
Normal file
18
Surge365.MassEmailReact.Web/public/web.config
Normal file
@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<configuration>
|
||||
<system.webServer>
|
||||
<rewrite>
|
||||
<rules>
|
||||
<rule name="SPA" stopProcessing="true">
|
||||
<match url=".*" />
|
||||
<conditions logicalGrouping="MatchAll">
|
||||
<add input="{REQUEST_FILENAME}" matchType="IsFile" negate="true" />
|
||||
<add input="{REQUEST_FILENAME}" matchType="IsDirectory" negate="true" />
|
||||
<add input="{URL}" pattern="^/api(/.*|$)" negate="true" />
|
||||
</conditions>
|
||||
<action type="Rewrite" url="index.html" />
|
||||
</rule>
|
||||
</rules>
|
||||
</rewrite>
|
||||
</system.webServer>
|
||||
</configuration>
|
||||
@ -16,6 +16,7 @@ export const routeRoleRequirements: Record<string, string[]> = {
|
||||
'/scheduledMailings': ['ScheduledMailingTab'],
|
||||
'/activeMailings': ['ActiveMailingTab'],
|
||||
'/completedMailings': ['CompletedMailingTab'],
|
||||
'/cancelledMailings': ['CompletedMailingTab'],
|
||||
};
|
||||
|
||||
const ProtectedPageWrapper: React.FC<{ title: string; children: React.ReactNode }> = ({ title, children }) => {
|
||||
|
||||
@ -26,6 +26,7 @@ import ListItemText from '@mui/material/ListItemText';
|
||||
import DashboardIcon from '@mui/icons-material/Dashboard';
|
||||
import HttpIcon from '@mui/icons-material/Http';
|
||||
import AccountBoxIcon from '@mui/icons-material/AccountBox';
|
||||
import CancelIcon from '@mui/icons-material/Cancel';
|
||||
|
||||
import DnsIcon from '@mui/icons-material/Dns';
|
||||
import TargetIcon from '@mui/icons-material/TrackChanges';
|
||||
@ -78,11 +79,11 @@ interface LayoutProps {
|
||||
}
|
||||
|
||||
const Layout = ({ children }: LayoutProps) => {
|
||||
const [open, setOpen] = React.useState(true);
|
||||
const { mode, setMode } = useColorScheme(); // MUI v6 hook for theme switching
|
||||
const iconButtonRef = React.useRef<HTMLButtonElement>(null);
|
||||
const theme = useTheme();
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down("sm")); //TODO: Move this to shared utils?
|
||||
const [open, setOpen] = React.useState(!isMobile);
|
||||
const { mode, setMode } = useColorScheme(); // MUI v6 hook for theme switching
|
||||
const iconButtonRef = React.useRef<HTMLButtonElement>(null);
|
||||
const { title } = useTitle();
|
||||
const navigate = useNavigate();
|
||||
|
||||
@ -98,6 +99,7 @@ const Layout = ({ children }: LayoutProps) => {
|
||||
{ text: 'Scheduled Mailings', icon: <ScheduleSendIcon />, path: '/scheduledMailings' },
|
||||
{ text: 'Active Mailings', icon: <AutorenewIcon />, path: '/activeMailings' },
|
||||
{ text: 'Completed Mailings', icon: <CheckCircleIcon />, path: '/completedMailings' },
|
||||
{ text: 'Cancelled Mailings', icon: <CancelIcon />, path: '/cancelledMailings' },
|
||||
];
|
||||
const { userRoles, setAuth } = useAuth(); // Use context
|
||||
const [profileMenuAnchorEl, setProfileMenuAnchorEl] = React.useState<null | HTMLElement>(null);
|
||||
@ -281,6 +283,11 @@ const Layout = ({ children }: LayoutProps) => {
|
||||
variant={isMobile ? "temporary" : "persistent"}
|
||||
anchor="left"
|
||||
open={open}
|
||||
onClose={handleDrawerClose}
|
||||
ModalProps={{
|
||||
keepMounted: true, // Keep mounted to avoid re-render issues
|
||||
hideBackdrop: isMobile && !open, // Explicitly hide backdrop when closed in mobile
|
||||
}}
|
||||
sx={{
|
||||
width: isMobile ? "100%" : open ? `var(--mui-drawer-width, ${drawerWidth}px)` : 0,
|
||||
flexShrink: 0,
|
||||
|
||||
@ -35,18 +35,22 @@ const ForgotPasswordModal: React.FC<ForgotPasswordModalProps> = ({ show, onClose
|
||||
|
||||
if (validate()) {
|
||||
console.log('Processing forgot password for', username);
|
||||
await utils.webMethod({
|
||||
methodPage: 'authenticate',
|
||||
methodName: 'generatepasswordrecovery',
|
||||
parameters: { username },
|
||||
success: (json: any) => {
|
||||
if (utils.getBoolean(json.success)) {
|
||||
setRecoveryStarted(true);
|
||||
} else {
|
||||
setUsernameNotFound(true);
|
||||
}
|
||||
},
|
||||
const apiUrl = "/api/authentication/generatepasswordrecovery";
|
||||
const response = await fetch(apiUrl, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ username}),
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
if (data && utils.getBoolean(data.success)) {
|
||||
setRecoveryStarted(true);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
setUsernameNotFound(true);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@ -23,7 +23,7 @@ import Target from "@/types/target";
|
||||
import EmailList from "@/components/forms/EmailList";
|
||||
import TestEmailList from "@/types/testEmailList";
|
||||
import TemplateViewer from "@/components/modals/TemplateViewer"
|
||||
import TargetSampleViewer from "@/components/modals/TargetSampleViewer"
|
||||
import TargetSampleModal from "@/components/modals/TargetSampleModal"
|
||||
import { useSetupData, SetupData } from "@/context/SetupDataContext";
|
||||
import { useForm, Controller, Resolver } from "react-hook-form";
|
||||
import { yupResolver } from "@hookform/resolvers/yup";
|
||||
@ -176,7 +176,7 @@ const MailingEdit = ({ open, mailing, onClose, onSave }: MailingEditProps) => {
|
||||
const [emails, setEmails] = useState<string[]>([]); // State for email array
|
||||
const [templateViewerOpen, setTemplateViewerOpen] = useState<boolean>(false);
|
||||
const [currentTemplate, setCurrentTemplate] = useState<Template | null>(null);
|
||||
const [targetSampleViewerOpen, setTargetSampleViewerOpen] = useState<boolean>(false);
|
||||
const [TargetSampleModalOpen, setTargetSampleModalOpen] = useState<boolean>(false);
|
||||
const [currentTarget, setCurrentTarget] = useState<Target | null>(null);
|
||||
|
||||
const { register, trigger, control, handleSubmit, reset, formState: { errors } } = useForm<Mailing>({
|
||||
@ -336,8 +336,8 @@ const MailingEdit = ({ open, mailing, onClose, onSave }: MailingEditProps) => {
|
||||
const handleTemplateViewerOpen = () => {
|
||||
setTemplateViewerOpen(!templateViewerOpen);
|
||||
};
|
||||
const handleTargetSampleViewerOpen = () => {
|
||||
setTargetSampleViewerOpen(!targetSampleViewerOpen);
|
||||
const handleTargetSampleModalOpen = () => {
|
||||
setTargetSampleModalOpen(!TargetSampleModalOpen);
|
||||
};
|
||||
return (
|
||||
<LocalizationProvider dateAdapter={AdapterDayjs}> {/* Wrap with LocalizationProvider */}
|
||||
@ -433,7 +433,7 @@ const MailingEdit = ({ open, mailing, onClose, onSave }: MailingEditProps) => {
|
||||
/>
|
||||
{currentTarget && (
|
||||
<Button
|
||||
onClick={handleTargetSampleViewerOpen}
|
||||
onClick={handleTargetSampleModalOpen}
|
||||
variant="outlined"
|
||||
startIcon={<VisibilityIcon />}
|
||||
sx={{ height: '100%', alignSelf: 'center' }}
|
||||
@ -562,11 +562,11 @@ const MailingEdit = ({ open, mailing, onClose, onSave }: MailingEditProps) => {
|
||||
onClose={() => { setTemplateViewerOpen(false) }}
|
||||
/>
|
||||
)}
|
||||
{targetSampleViewerOpen && (
|
||||
<TargetSampleViewer
|
||||
open={targetSampleViewerOpen}
|
||||
{TargetSampleModalOpen && (
|
||||
<TargetSampleModal
|
||||
open={TargetSampleModalOpen}
|
||||
target={currentTarget!}
|
||||
onClose={() => { setTargetSampleViewerOpen(false) }}
|
||||
onClose={() => { setTargetSampleModalOpen(false) }}
|
||||
/>
|
||||
)}
|
||||
</DialogContent>
|
||||
|
||||
@ -5,7 +5,7 @@ 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 TargetSampleViewer from "@/components/modals/TargetSampleViewer"
|
||||
import TargetSampleModal from "@/components/modals/TargetSampleModal"
|
||||
|
||||
interface MailingViewProps {
|
||||
open: boolean;
|
||||
@ -16,7 +16,7 @@ interface MailingViewProps {
|
||||
function MailingView({ open, mailing, onClose }: MailingViewProps) {
|
||||
const setupData = useSetupData();
|
||||
const [templateViewerOpen, setTemplateViewerOpen] = useState<boolean>(false);
|
||||
const [targetSampleViewerOpen, setTargetSampleViewerOpen] = useState<boolean>(false);
|
||||
const [TargetSampleModalOpen, setTargetSampleModalOpen] = useState<boolean>(false);
|
||||
|
||||
if (!mailing) return null;
|
||||
|
||||
@ -55,7 +55,7 @@ function MailingView({ open, mailing, onClose }: MailingViewProps) {
|
||||
// Navigation handlers for viewing related entities
|
||||
const handleViewTarget = () => {
|
||||
if (target) {
|
||||
setTargetSampleViewerOpen(!targetSampleViewerOpen);
|
||||
setTargetSampleModalOpen(!TargetSampleModalOpen);
|
||||
}
|
||||
};
|
||||
|
||||
@ -114,11 +114,11 @@ function MailingView({ open, mailing, onClose }: MailingViewProps) {
|
||||
onClose={() => { setTemplateViewerOpen(false) }}
|
||||
/>
|
||||
)}
|
||||
{targetSampleViewerOpen && (
|
||||
<TargetSampleViewer
|
||||
open={targetSampleViewerOpen}
|
||||
{TargetSampleModalOpen && (
|
||||
<TargetSampleModal
|
||||
open={TargetSampleModalOpen}
|
||||
target={target!}
|
||||
onClose={() => { setTargetSampleViewerOpen(false) }}
|
||||
onClose={() => { setTargetSampleModalOpen(false) }}
|
||||
/>
|
||||
)}
|
||||
</DialogContent>
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import {
|
||||
Box,
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
DialogContent,
|
||||
@ -9,12 +10,20 @@ import {
|
||||
Button,
|
||||
Switch,
|
||||
FormControlLabel,
|
||||
Typography,
|
||||
IconButton,
|
||||
} from "@mui/material";
|
||||
import VisibilityIcon from '@mui/icons-material/Visibility';
|
||||
import VisibilityOffIcon from '@mui/icons-material/VisibilityOff';
|
||||
import Target from "@/types/target";
|
||||
import { useSetupData, SetupData } from "@/context/SetupDataContext";
|
||||
import { useForm, Controller, Resolver } from "react-hook-form";
|
||||
import { yupResolver } from "@hookform/resolvers/yup";
|
||||
import * as yup from "yup";
|
||||
import TargetColumn from "@/types/targetColumn";
|
||||
import TargetSampleColumn from "@/types/targetSampleColumn";
|
||||
import TargetSampleDataGrid from "@/components/shared/TargetSampleDataGrid";
|
||||
import { GridColDef } from '@mui/x-data-grid';
|
||||
|
||||
type TargetEditProps = {
|
||||
open: boolean;
|
||||
@ -32,7 +41,6 @@ const schema = yup.object().shape({
|
||||
.test("unique-name", "Name must be unique", function (value) {
|
||||
const setupData = this.options.context?.setupData as { targets: Target[] };
|
||||
if (!setupData) return true;
|
||||
|
||||
return !setupData.targets.some(
|
||||
(t) => t.name.toLowerCase() === value?.toLowerCase() && (t.id === 0 || t.id !== this.parent.id)
|
||||
);
|
||||
@ -42,13 +50,32 @@ const schema = yup.object().shape({
|
||||
filterQuery: yup.string().nullable(),
|
||||
allowWriteBack: yup.boolean().default(false),
|
||||
isActive: yup.boolean().default(true),
|
||||
columns: yup
|
||||
.array().of(
|
||||
yup.object().shape({
|
||||
id: yup.number(),
|
||||
targetId: yup.number(),
|
||||
typeCode: yup.string(),
|
||||
dataTypeCode: yup.string(),
|
||||
name: yup.string(),
|
||||
writeBack: yup.boolean(),
|
||||
isEmailAddress: yup.boolean(),
|
||||
})
|
||||
)
|
||||
.test(
|
||||
"email-address-required",
|
||||
"One column must be marked as the email address",
|
||||
(columns) => columns && columns.filter((col) => col.isEmailAddress === true).length === 1
|
||||
)
|
||||
.test(
|
||||
"unique-id-required",
|
||||
"One column must be marked as the unique ID",
|
||||
(columns) => columns && columns.filter((col) => col.typeCode === "I").length === 1
|
||||
),
|
||||
});
|
||||
|
||||
//TODO: Make DatabaseName a select using new array in setupData.servers.databases
|
||||
//TODO: Maybe Make View a select using new array in setupData.servers.databases.views+procs. But would have to allow free form entry just in case/no validation on found in select
|
||||
//TODO: Add verify/test button on form, checks that server, db, view exist, query works and returns data. Show sample data on screen.
|
||||
const defaultTarget: Target = {
|
||||
id: 0,
|
||||
id: 0,
|
||||
name: "",
|
||||
serverId: 0,
|
||||
databaseName: "",
|
||||
@ -56,38 +83,57 @@ const defaultTarget: Target = {
|
||||
filterQuery: "",
|
||||
allowWriteBack: false,
|
||||
isActive: true,
|
||||
columns: [],
|
||||
};
|
||||
|
||||
const useColumnErrors = (errors: any, columns: TargetColumn[]) => {
|
||||
const hasEmail = columns.some((col) => col.isEmailAddress);
|
||||
const hasUniqueId = columns.some((col) => col.typeCode === "I");
|
||||
return {
|
||||
emailError: errors.columns && !hasEmail ? "One column must be marked as the email address" : "",
|
||||
uniqueIdError: errors.columns && !hasUniqueId ? "One column must be marked as the unique ID" : "",
|
||||
};
|
||||
};
|
||||
|
||||
const TargetEdit = ({ open, target, onClose, onSave }: TargetEditProps) => {
|
||||
const isNew = !target || target.id === 0;
|
||||
const setupData: SetupData = useSetupData();
|
||||
const [targetTested, setTargetTested] = useState<boolean>(false);
|
||||
const [targetTestError, setTargetTestError] = useState<boolean>(false);
|
||||
const [targetTestErrorMessage, setTargetTestErrorMessage] = useState<string>('');
|
||||
const [availableColumns, setAvailableColumns] = useState<TargetColumn[]>([]);
|
||||
const [sampleColumns, setSampleColumns] = useState<GridColDef[]>([]);
|
||||
const [sampleRows, setSampleRows] = useState<{ [key: string]: string }[]>([]);
|
||||
const [showSampleGrid, setShowSampleGrid] = useState<boolean>(true);
|
||||
|
||||
const { register, trigger, control, handleSubmit, reset, formState: { errors } } = useForm<Target>({
|
||||
mode: "onBlur",
|
||||
defaultValues: target || defaultTarget,
|
||||
resolver: yupResolver(schema) as Resolver<Target>,
|
||||
context: { setupData }
|
||||
,
|
||||
context: { setupData },
|
||||
});
|
||||
//const [formData, setFormData] = useState<Target>(target ? { ...target } : { ...defaultTarget });
|
||||
//const [serverError, setServerError] = useState(false); // Track validation
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
useEffect(() => { //Reset form to unedited state on open or target change
|
||||
const [loading, setLoading] = useState(false);
|
||||
const { emailError, uniqueIdError } = useColumnErrors(errors, control._formValues.columns || []);
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
if (target && target.id > 0) {
|
||||
setTargetTested(true);
|
||||
}
|
||||
setAvailableColumns(target?.columns || []);
|
||||
setTargetTestErrorMessage('');
|
||||
setTargetTestError(false);
|
||||
setSampleColumns([]);
|
||||
setSampleRows([]);
|
||||
setShowSampleGrid(false);
|
||||
reset(target || defaultTarget, { keepDefaultValues: true });
|
||||
}
|
||||
}, [open, target, reset]);
|
||||
|
||||
//const handleChange = (field: string, value: any) => {
|
||||
// setFormData((prev) => ({ ...prev, [field]: value?.id || "" }));
|
||||
|
||||
// if (field === "serverId" && value) setServerError(false);
|
||||
//};
|
||||
}, [open, target, reset]);
|
||||
|
||||
const handleSave = async (formData: Target) => {
|
||||
const apiUrl = isNew ? "/api/targets" : `/api/targets/${formData.id}`;
|
||||
const method = isNew ? "POST" : "PUT";
|
||||
const method = isNew ? "POST" : "PUT";
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await fetch(apiUrl, {
|
||||
@ -95,11 +141,9 @@ const TargetEdit = ({ open, target, onClose, onSave }: TargetEditProps) => {
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(formData),
|
||||
});
|
||||
|
||||
if (!response.ok) throw new Error(isNew ? "Failed to create" : "Failed to update");
|
||||
|
||||
const updatedTarget = await response.json();
|
||||
onSave(updatedTarget);
|
||||
onSave(updatedTarget);
|
||||
onClose();
|
||||
} catch (error) {
|
||||
console.error("Update error:", error);
|
||||
@ -108,24 +152,120 @@ const TargetEdit = ({ open, target, onClose, onSave }: TargetEditProps) => {
|
||||
}
|
||||
};
|
||||
|
||||
const getTargetColumnsFromSample = (columns: TargetSampleColumn[]) => {
|
||||
const targetColumns: TargetColumn[] = [];
|
||||
for (const colName in columns) {
|
||||
const col = columns[colName];
|
||||
targetColumns.push({
|
||||
name: col.name,
|
||||
typeCode: col.type === "E" ? "G" : col.type,
|
||||
dataTypeCode: col.dataType,
|
||||
id: 0,
|
||||
targetId: target?.id || 0,
|
||||
writeBack: false,
|
||||
isEmailAddress: col.type === "E",
|
||||
});
|
||||
}
|
||||
return targetColumns;
|
||||
};
|
||||
|
||||
const handleTestTarget = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await fetch(`/api/targets/test`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
serverId: control._formValues.serverId,
|
||||
databaseName: control._formValues.databaseName,
|
||||
viewName: control._formValues.viewName,
|
||||
filterQuery: control._formValues.filterQuery,
|
||||
}),
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
if (response.ok) {
|
||||
let columns: TargetColumn[] = [];
|
||||
if (data?.columns) {
|
||||
columns = getTargetColumnsFromSample(data.columns);
|
||||
}
|
||||
|
||||
const gridColumns: GridColDef[] = Object.keys(columns ?? {}).map((colName) => ({
|
||||
field: colName,
|
||||
headerName: colName,
|
||||
minWidth: 150,
|
||||
width: 200,
|
||||
flex: 1,
|
||||
sortable: true,
|
||||
}));
|
||||
|
||||
const gridRows = data?.rows?.map((row: { [key: string]: string }, index: number) => ({
|
||||
id: index.toString(),
|
||||
...row,
|
||||
})) || [];
|
||||
|
||||
setAvailableColumns(columns);
|
||||
setSampleColumns(gridColumns);
|
||||
setSampleRows(gridRows);
|
||||
setTargetTested(true);
|
||||
setTargetTestError(false);
|
||||
setTargetTestErrorMessage('');
|
||||
reset({ ...control._formValues, columns });
|
||||
} else {
|
||||
setTargetTested(false);
|
||||
setTargetTestError(true);
|
||||
setTargetTestErrorMessage(data.error);
|
||||
setSampleColumns([]);
|
||||
setSampleRows([]);
|
||||
}
|
||||
} catch (error) {
|
||||
setTargetTested(false);
|
||||
setTargetTestError(true);
|
||||
setTargetTestErrorMessage(error instanceof Error ? error.message : String(error));
|
||||
setSampleColumns([]);
|
||||
setSampleRows([]);
|
||||
console.error("Test target error:", error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const getColumnOptions = (typeCode: string) => {
|
||||
switch (typeCode) {
|
||||
case "E": // Email
|
||||
case "S": // Status
|
||||
return availableColumns.filter(col => col.dataTypeCode.toLowerCase() === "s");
|
||||
case "I": // Unique Identifier
|
||||
return availableColumns.filter(col =>
|
||||
col.dataTypeCode.toLowerCase() === "s" || col.dataTypeCode.toLowerCase() === "n");
|
||||
case "B": // Bounce
|
||||
case "U": // Unsubscribe
|
||||
return availableColumns.filter(col => col.dataTypeCode.toLowerCase() === "b");
|
||||
case "C": // Soft Bounce Count
|
||||
return availableColumns.filter(col => col.dataTypeCode.toLowerCase() === "n");
|
||||
default:
|
||||
return availableColumns;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onClose={onClose} maxWidth="sm" fullWidth>
|
||||
<DialogTitle>{isNew ? "Add Target" : "Edit Target id=" + target.id}</DialogTitle>
|
||||
<DialogTitle>{isNew ? "Add Target" : "Edit Target id=" + target?.id}</DialogTitle>
|
||||
<DialogContent>
|
||||
<Controller
|
||||
name="serverId"
|
||||
control={control}
|
||||
rules={{ required: "Server is required" }}
|
||||
render={({ field }) => (
|
||||
<Autocomplete {...field}
|
||||
<Autocomplete
|
||||
{...field}
|
||||
options={setupData.servers}
|
||||
getOptionLabel={(option) => option.name}
|
||||
value={setupData.servers.find((s) => s.id === Number(field.value)) || null}
|
||||
onChange={(_, newValue) => {
|
||||
field.onChange(newValue ? newValue.id : null);
|
||||
trigger("serverId");
|
||||
trigger("serverId");
|
||||
}}
|
||||
|
||||
renderInput={(params) => (
|
||||
<TextField
|
||||
{...params}
|
||||
@ -167,20 +307,16 @@ const TargetEdit = ({ open, target, onClose, onSave }: TargetEditProps) => {
|
||||
{...register("filterQuery")}
|
||||
label="Filter Query"
|
||||
fullWidth
|
||||
multiline
|
||||
rows={4}
|
||||
margin="dense"
|
||||
error={!!errors.filterQuery}
|
||||
helperText={errors.filterQuery?.message}
|
||||
/>
|
||||
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
{...register("allowWriteBack")}
|
||||
/>
|
||||
}
|
||||
control={<Switch {...register("allowWriteBack")} />}
|
||||
label="Allow Write Back"
|
||||
/>
|
||||
|
||||
<Controller
|
||||
name="isActive"
|
||||
control={control}
|
||||
@ -197,7 +333,246 @@ const TargetEdit = ({ open, target, onClose, onSave }: TargetEditProps) => {
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Box sx={{ marginTop: 1, marginBottom: 1, display: 'flex', alignItems: 'center', gap: 2 }}>
|
||||
<Button
|
||||
onClick={handleTestTarget}
|
||||
variant="outlined"
|
||||
sx={{ height: '100%', alignSelf: 'center' }}
|
||||
disabled={loading}
|
||||
>
|
||||
{loading ? "Testing..." : "Test Target"}
|
||||
</Button>
|
||||
{targetTested && sampleColumns.length > 0 && (
|
||||
<Box sx={{ display: 'flex', alignItems: 'center' }}>
|
||||
<Typography variant="subtitle1">Sample Data</Typography>
|
||||
<IconButton
|
||||
onClick={() => setShowSampleGrid(prev => !prev)}
|
||||
size="small"
|
||||
sx={{ ml: 1 }}
|
||||
>
|
||||
{showSampleGrid ? <VisibilityOffIcon /> : <VisibilityIcon />}
|
||||
</IconButton>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
{targetTestError && (
|
||||
<Typography color="error" variant="body2" sx={{ mt: 1 }}>
|
||||
{targetTestErrorMessage}
|
||||
</Typography>
|
||||
)}
|
||||
{targetTested && sampleColumns.length > 0 && showSampleGrid && (
|
||||
<Box sx={{ mt: 1, mb:1 }}>
|
||||
<TargetSampleDataGrid
|
||||
columns={sampleColumns}
|
||||
rows={sampleRows.slice(0, 4)} // Limit to first 4 rows
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
{control._formValues != null && control._formValues.columns != null && targetTested && (
|
||||
<>
|
||||
{/* Email Address Column Autocomplete */}
|
||||
<Controller
|
||||
name="columns"
|
||||
control={control}
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<Autocomplete
|
||||
options={getColumnOptions("E")}
|
||||
getOptionLabel={(option) => option.name}
|
||||
value={field.value.find((col) => col.isEmailAddress) || null}
|
||||
onChange={(_, newValue) => {
|
||||
const updatedColumns = field.value.map((col) => ({
|
||||
...col,
|
||||
typeCode: newValue && col.name === newValue.name ? "G" : col.typeCode,
|
||||
isEmailAddress: newValue && col.name === newValue.name,
|
||||
}));
|
||||
field.onChange(updatedColumns);
|
||||
trigger("columns");
|
||||
}}
|
||||
renderInput={(params) => (
|
||||
<TextField
|
||||
{...params}
|
||||
label="Email Address Column"
|
||||
fullWidth
|
||||
margin="dense"
|
||||
error={!!emailError}
|
||||
helperText={emailError}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
{/* Unique ID Column Autocomplete */}
|
||||
<Controller
|
||||
name="columns"
|
||||
control={control}
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<Autocomplete
|
||||
options={getColumnOptions("I")}
|
||||
getOptionLabel={(option) => option.name}
|
||||
value={field.value.find((col) => col.typeCode === "I") || null}
|
||||
onChange={(_, newValue) => {
|
||||
const updatedColumns = field.value.map((col) => ({
|
||||
...col,
|
||||
typeCode: newValue && col.name === newValue.name ? "I" : col.typeCode === "I" ? "G" : col.typeCode,
|
||||
isEmailAddress: newValue && col.name === newValue.name ? false : col.isEmailAddress,
|
||||
}));
|
||||
field.onChange(updatedColumns);
|
||||
trigger("columns");
|
||||
}}
|
||||
renderInput={(params) => (
|
||||
<TextField
|
||||
{...params}
|
||||
label="Unique ID Column"
|
||||
fullWidth
|
||||
margin="dense"
|
||||
error={!!uniqueIdError}
|
||||
helperText={uniqueIdError}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<Controller
|
||||
name="columns"
|
||||
control={control}
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<Autocomplete
|
||||
options={getColumnOptions("S")}
|
||||
getOptionLabel={(option) => option.name}
|
||||
value={field.value.find((col) => col.typeCode === "S") || null}
|
||||
onChange={(_, newValue) => {
|
||||
const updatedColumns = field.value.map((col) => ({
|
||||
...col,
|
||||
typeCode: newValue && col.name === newValue.name ? "S" : col.typeCode === "S" ? "G" : col.typeCode,
|
||||
isEmailAddress: newValue && col.name === newValue.name ? false : col.isEmailAddress,
|
||||
}));
|
||||
field.onChange(updatedColumns);
|
||||
trigger("columns");
|
||||
}}
|
||||
renderInput={(params) => (
|
||||
<TextField
|
||||
{...params}
|
||||
label="Status Code Column"
|
||||
fullWidth
|
||||
margin="dense"
|
||||
/*error={!!uniqueIdError}
|
||||
helperText={uniqueIdError}*/
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<Controller
|
||||
name="columns"
|
||||
control={control}
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<Autocomplete
|
||||
options={getColumnOptions("C")}
|
||||
getOptionLabel={(option) => option.name}
|
||||
value={field.value.find((col) => col.typeCode === "C") || null}
|
||||
onChange={(_, newValue) => {
|
||||
const updatedColumns = field.value.map((col) => ({
|
||||
...col,
|
||||
typeCode: newValue && col.name === newValue.name ? "C" : col.typeCode === "C" ? "G" : col.typeCode,
|
||||
isEmailAddress: newValue && col.name === newValue.name ? false : col.isEmailAddress,
|
||||
}));
|
||||
field.onChange(updatedColumns);
|
||||
trigger("columns");
|
||||
}}
|
||||
renderInput={(params) => (
|
||||
<TextField
|
||||
{...params}
|
||||
label="Soft Bounce Count Column"
|
||||
fullWidth
|
||||
margin="dense"
|
||||
/*error={!!uniqueIdError}
|
||||
helperText={uniqueIdError}*/
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<Controller
|
||||
name="columns"
|
||||
control={control}
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<Autocomplete
|
||||
options={getColumnOptions("B")}
|
||||
getOptionLabel={(option) => option.name}
|
||||
value={field.value.find((col) => col.typeCode === "B") || null}
|
||||
onChange={(_, newValue) => {
|
||||
const updatedColumns = field.value.map((col) => ({
|
||||
...col,
|
||||
typeCode: newValue && col.name === newValue.name ? "B" : col.typeCode === "B" ? "G" : col.typeCode,
|
||||
isEmailAddress: newValue && col.name === newValue.name ? false : col.isEmailAddress,
|
||||
}));
|
||||
field.onChange(updatedColumns);
|
||||
trigger("columns");
|
||||
}}
|
||||
renderInput={(params) => (
|
||||
<TextField
|
||||
{...params}
|
||||
label="Bounce Column"
|
||||
fullWidth
|
||||
margin="dense"
|
||||
/*error={!!uniqueIdError}
|
||||
helperText={uniqueIdError}*/
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<Controller
|
||||
name="columns"
|
||||
control={control}
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<Autocomplete
|
||||
options={getColumnOptions("U")}
|
||||
getOptionLabel={(option) => option.name}
|
||||
value={field.value.find((col) => col.typeCode === "U") || null}
|
||||
onChange={(_, newValue) => {
|
||||
const updatedColumns = field.value.map((col) => ({
|
||||
...col,
|
||||
typeCode: newValue && col.name === newValue.name ? "U" : col.typeCode === "U" ? "G" : col.typeCode,
|
||||
isEmailAddress: newValue && col.name === newValue.name ? false : col.isEmailAddress,
|
||||
}));
|
||||
field.onChange(updatedColumns);
|
||||
trigger("columns");
|
||||
}}
|
||||
renderInput={(params) => (
|
||||
<TextField
|
||||
{...params}
|
||||
label="Unsubscribe Column"
|
||||
fullWidth
|
||||
margin="dense"
|
||||
/*error={!!uniqueIdError}
|
||||
helperText={uniqueIdError}*/
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
/*
|
||||
B Bounce status
|
||||
C Soft bounce count
|
||||
G General
|
||||
I Unique identifier
|
||||
S Status code
|
||||
U Unsubscribe status*/
|
||||
)}
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={onClose} disabled={loading}>Cancel</Button>
|
||||
@ -209,4 +584,4 @@ const TargetEdit = ({ open, target, onClose, onSave }: TargetEditProps) => {
|
||||
);
|
||||
};
|
||||
|
||||
export default TargetEdit;
|
||||
export default TargetEdit;
|
||||
@ -9,9 +9,10 @@ import {
|
||||
} from "@mui/material";
|
||||
import Target from "@/types/target";
|
||||
import CloseIcon from '@mui/icons-material/Close';
|
||||
import { DataGrid, GridColDef } from '@mui/x-data-grid';
|
||||
import TargetSampleDataGrid from "@/components/shared/TargetSampleDataGrid";
|
||||
import { GridColDef } from '@mui/x-data-grid';
|
||||
|
||||
type TargetSampleViewerProps = {
|
||||
type TargetSampleModalProps = {
|
||||
open: boolean;
|
||||
target: Target;
|
||||
onClose: () => void;
|
||||
@ -26,7 +27,7 @@ type TargetSample = {
|
||||
rows: { [key: string]: string }[];
|
||||
};
|
||||
|
||||
const TargetSampleViewer = ({ open, target, onClose }: TargetSampleViewerProps) => {
|
||||
const TargetSampleModal = ({ open, target, onClose }: TargetSampleModalProps) => {
|
||||
const [targetSample, setTargetSample] = useState<TargetSample | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
@ -34,7 +35,6 @@ const TargetSampleViewer = ({ open, target, onClose }: TargetSampleViewerProps)
|
||||
if (open) {
|
||||
const fetchSampleData = async () => {
|
||||
setLoading(true);
|
||||
//await new Promise(resolve => setTimeout(resolve, 3000)); // Simulate loading delay
|
||||
try {
|
||||
const response = await fetch(`/api/targets/${target.id}/sample`);
|
||||
if (!response.ok) throw new Error("Failed to fetch sample data");
|
||||
@ -54,27 +54,26 @@ const TargetSampleViewer = ({ open, target, onClose }: TargetSampleViewerProps)
|
||||
const columns: GridColDef[] = Object.keys(targetSample?.columns ?? {}).map((colName) => ({
|
||||
field: colName,
|
||||
headerName: colName,
|
||||
minWidth: 150, // Minimum width to prevent excessive truncation
|
||||
width: 200, // Default width
|
||||
flex: 1, // Allow columns to grow/shrink proportionally
|
||||
minWidth: 150,
|
||||
width: 200,
|
||||
flex: 1,
|
||||
sortable: true,
|
||||
})) || [];
|
||||
|
||||
const rows = targetSample?.rows?.map((row, index) => {
|
||||
const rowData: { [key: string]: string } = { id: index.toString() };
|
||||
Object.keys(targetSample.columns).forEach((key) => {
|
||||
Object.keys(targetSample?.columns || {}).forEach((key) => {
|
||||
rowData[key] = row[key] ?? "";
|
||||
});
|
||||
return rowData;
|
||||
}) || [];
|
||||
|
||||
// Calculate dialog width based on number of columns
|
||||
const columnCount = columns.length;
|
||||
const defaultColumnWidth = 200; // Match width from columns
|
||||
const minDialogWidth = 300; // Minimum dialog width for very small datasets
|
||||
const defaultColumnWidth = 200;
|
||||
const minDialogWidth = 300;
|
||||
const calculatedWidth = Math.max(
|
||||
minDialogWidth,
|
||||
Math.min(columnCount * defaultColumnWidth, 0.98 * window.innerWidth) // Cap at 98% of viewport
|
||||
Math.min(columnCount * defaultColumnWidth, 0.98 * window.innerWidth)
|
||||
);
|
||||
|
||||
return (
|
||||
@ -84,12 +83,12 @@ const TargetSampleViewer = ({ open, target, onClose }: TargetSampleViewerProps)
|
||||
maxWidth={false}
|
||||
sx={{
|
||||
'& .MuiDialog-paper': {
|
||||
width: `${calculatedWidth}px`, // Dynamic width
|
||||
maxWidth: '98vw', // Upper limit
|
||||
width: `${calculatedWidth}px`,
|
||||
maxWidth: '98vw',
|
||||
margin: 'auto',
|
||||
height: loading ? '200px' : 'auto', // Explicitly set height for loading, reset to auto when done
|
||||
maxHeight: '90vh', // Optional: Prevent dialog from growing too tall
|
||||
}
|
||||
height: loading ? '200px' : 'auto',
|
||||
maxHeight: '90vh',
|
||||
},
|
||||
}}
|
||||
>
|
||||
{loading && (
|
||||
@ -114,34 +113,7 @@ const TargetSampleViewer = ({ open, target, onClose }: TargetSampleViewerProps)
|
||||
</Box>
|
||||
</DialogTitle>
|
||||
<DialogContent>
|
||||
<DataGrid
|
||||
rows={rows}
|
||||
columns={columns}
|
||||
autoHeight
|
||||
hideFooter
|
||||
hideFooterPagination
|
||||
sx={{
|
||||
width: "100%",
|
||||
maxHeight: "800px",
|
||||
'& .MuiDataGrid-main': {
|
||||
overflowX: 'auto',
|
||||
},
|
||||
'& .MuiDataGrid-columnHeaderTitle': {
|
||||
overflow: 'visible',
|
||||
whiteSpace: 'normal',
|
||||
lineHeight: '1.2em',
|
||||
textOverflow: 'ellipsis',
|
||||
}
|
||||
}}
|
||||
initialState={{
|
||||
pagination: {
|
||||
paginationModel: {
|
||||
pageSize: 10,
|
||||
},
|
||||
},
|
||||
}}
|
||||
pageSizeOptions={[]}
|
||||
/>
|
||||
<TargetSampleDataGrid columns={columns} rows={rows} />
|
||||
</DialogContent>
|
||||
</>
|
||||
)}
|
||||
@ -149,4 +121,4 @@ const TargetSampleViewer = ({ open, target, onClose }: TargetSampleViewerProps)
|
||||
);
|
||||
};
|
||||
|
||||
export default TargetSampleViewer;
|
||||
export default TargetSampleModal;
|
||||
@ -18,6 +18,7 @@ import NewMailings from '@/components/pages/NewMailings';
|
||||
import ScheduledMailings from '@/components/pages/ScheduledMailings';
|
||||
import ActiveMailings from '@/components/pages/ActiveMailings';
|
||||
import CompletedMailings from '@/components/pages/CompletedMailings';
|
||||
import CancelledMailings from '@/components/pages/CancelledMailings';
|
||||
import AuthCheck from '@/components/auth/AuthCheck';
|
||||
|
||||
import { ColorModeContext } from '@/theme/theme';
|
||||
@ -211,6 +212,16 @@ const App = () => {
|
||||
</PageWrapper>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/cancelledMailings"
|
||||
element={
|
||||
<PageWrapper title="Cancelled Mailings">
|
||||
<Layout>
|
||||
<CancelledMailings />
|
||||
</Layout>
|
||||
</PageWrapper>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/completedMailings"
|
||||
element={
|
||||
|
||||
@ -0,0 +1,150 @@
|
||||
import { useState, useRef, useEffect } from 'react';
|
||||
import { useSetupData, SetupData } from "@/context/SetupDataContext";
|
||||
import VisibilityIcon from '@mui/icons-material/Visibility';
|
||||
import ContentCopyIcon from '@mui/icons-material/ContentCopy';
|
||||
import RefreshIcon from '@mui/icons-material/Refresh';
|
||||
import { Box, useTheme, CircularProgress, IconButton } from '@mui/material';
|
||||
import { DataGrid, GridColDef, GridRenderCellParams, GridToolbarContainer, GridToolbarQuickFilter, GridToolbarExport, GridToolbarDensitySelector, GridToolbarColumnsButton } from '@mui/x-data-grid';
|
||||
import Mailing from '@/types/mailing';
|
||||
import MailingEdit from "@/components/modals/MailingEdit";
|
||||
import MailingView from "@/components/modals/MailingView"; // Assume this is a new read-only view component
|
||||
|
||||
function CancelledMailings() {
|
||||
const theme = useTheme();
|
||||
const setupData: SetupData = useSetupData();
|
||||
|
||||
const gridContainerRef = useRef<HTMLDivElement | null>(null);
|
||||
const [mailingsLoading, setMailingsLoading] = useState<boolean>(false);
|
||||
const [mailings, setMailings] = useState<Mailing[]>([]);
|
||||
const [selectedRow, setSelectedRow] = useState<Mailing | null>(null);
|
||||
const [viewOpen, setViewOpen] = useState<boolean>(false);
|
||||
const [editOpen, setEditOpen] = useState<boolean>(false);
|
||||
|
||||
const columns: GridColDef<Mailing>[] = [
|
||||
{
|
||||
field: "actions",
|
||||
headerName: "",
|
||||
sortable: false,
|
||||
width: 140,
|
||||
renderCell: (params: GridRenderCellParams<Mailing>) => (
|
||||
<>
|
||||
<IconButton color="primary" onClick={(e) => { e.stopPropagation(); handleView(params.row); }}>
|
||||
<VisibilityIcon />
|
||||
</IconButton>
|
||||
<IconButton color="primary" onClick={(e) => { e.stopPropagation(); handleCopy(params.row); }}>
|
||||
<ContentCopyIcon />
|
||||
</IconButton>
|
||||
</>
|
||||
),
|
||||
},
|
||||
{ field: "id", headerName: "ID", width: 80 },
|
||||
{ field: "name", headerName: "Name", flex: 1, minWidth: 160 },
|
||||
{ field: "description", headerName: "Description", flex: 1, minWidth: 200 },
|
||||
{
|
||||
field: "templateId",
|
||||
headerName: "Subject",
|
||||
flex: 1,
|
||||
minWidth: 160,
|
||||
valueGetter: (_: number, row: Mailing) => setupData.templates.find(t => t.id === row.templateId)?.subject || 'Unknown',
|
||||
},
|
||||
];
|
||||
|
||||
const reloadMailings = async () => {
|
||||
setMailingsLoading(true);
|
||||
const mailingsResponse = await fetch("/api/mailings/status/c");
|
||||
const mailingsData = await mailingsResponse.json();
|
||||
if (mailingsData) {
|
||||
setMailings(mailingsData);
|
||||
setMailingsLoading(false);
|
||||
} else {
|
||||
console.error("Failed to fetch cancelled mailings");
|
||||
setMailingsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleView = (row: Mailing) => {
|
||||
setSelectedRow(row);
|
||||
setViewOpen(true);
|
||||
};
|
||||
|
||||
const handleCopy = (row: Mailing) => {
|
||||
const newMailing = { ...row, id: 0 }; // Copy all fields except ID
|
||||
setSelectedRow(newMailing);
|
||||
setEditOpen(true);
|
||||
};
|
||||
|
||||
const handleUpdateRow = () => {
|
||||
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
reloadMailings();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Box ref={gridContainerRef} sx={{
|
||||
position: 'relative', left: 0, right: 0, height: "calc(100vh - 124px)", overflow: "hidden",
|
||||
transition: theme.transitions.create(['width', 'height'], {
|
||||
easing: theme.transitions.easing.easeInOut,
|
||||
duration: theme.transitions.duration.standard,
|
||||
})
|
||||
}}>
|
||||
<Box sx={{ position: 'absolute', inset: 0 }}>
|
||||
<DataGrid
|
||||
rows={mailings}
|
||||
columns={columns}
|
||||
autoPageSize
|
||||
sx={{ minWidth: "600px" }}
|
||||
slots={{
|
||||
toolbar: () => (
|
||||
<GridToolbarContainer sx={{ display: "flex", alignItems: "center" }}>
|
||||
<IconButton size="small" color="primary" onClick={() => reloadMailings()} sx={{ marginLeft: 1 }}>
|
||||
{mailingsLoading ? <CircularProgress size={24} color="inherit" /> : <RefreshIcon />}
|
||||
</IconButton>
|
||||
<GridToolbarColumnsButton />
|
||||
<GridToolbarDensitySelector />
|
||||
<GridToolbarExport />
|
||||
<GridToolbarQuickFilter sx={{ ml: "auto" }} />
|
||||
</GridToolbarContainer>
|
||||
),
|
||||
}}
|
||||
slotProps={{
|
||||
toolbar: {
|
||||
showQuickFilter: true,
|
||||
},
|
||||
}}
|
||||
initialState={{
|
||||
pagination: {
|
||||
paginationModel: {
|
||||
pageSize: 20,
|
||||
},
|
||||
},
|
||||
sorting: {
|
||||
sortModel: [{ field: 'id', sort: 'desc' }], // Default sort by scheduleDate, descending
|
||||
},
|
||||
}}
|
||||
pageSizeOptions={[10, 20, 50, 100]}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
{viewOpen && (
|
||||
<MailingView
|
||||
open={viewOpen}
|
||||
mailing={selectedRow}
|
||||
onClose={() => setViewOpen(false)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{editOpen && (
|
||||
<MailingEdit
|
||||
open={editOpen}
|
||||
mailing={selectedRow}
|
||||
onClose={(reason) => { if (reason !== 'backdropClick') setEditOpen(false) }}
|
||||
onSave={handleUpdateRow}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export default CancelledMailings;
|
||||
@ -45,20 +45,26 @@ function CompletedMailings() {
|
||||
</>
|
||||
),
|
||||
},
|
||||
{ field: "mailingId", headerName: "Mailing ID", width: 100 },
|
||||
{ field: "mailingId", headerName: "Mailing ID", width: 90 },
|
||||
{ field: "mailingName", headerName: "Name", flex: 1, minWidth: 160 },
|
||||
{ field: "emailCount", headerName: "Emails", width: 100 },
|
||||
{ field: "sendCount", headerName: "Active", width: 100 },
|
||||
{ field: "deliveredCount", headerName: "Delivered", width: 100 },
|
||||
{ field: "failedCount", headerName: "Failed", width: 100 },
|
||||
{ field: "blockedCount", headerName: "Blocked", width: 100 },
|
||||
{ field: "invalidCount", headerName: "Invalid", width: 100 },
|
||||
{ field: "openCount", headerName: "Opens", width: 100 },
|
||||
{ field: "sentDate", headerName: "Sent Date", width: 180,
|
||||
renderCell: (params: GridRenderCellParams<MailingStatistic>) => (
|
||||
<>
|
||||
{params.value ? new Date(params.value).toLocaleString() : ''}
|
||||
</>
|
||||
)},
|
||||
{ field: "emailCount", headerName: "Emails", width: 70 },
|
||||
{ field: "sendCount", headerName: "Active", width: 70 },
|
||||
{ field: "deliveredCount", headerName: "Delivered", width: 90 },
|
||||
{ field: "failedCount", headerName: "Failed", width: 70 },
|
||||
{ field: "blockedCount", headerName: "Blocked", width: 80 },
|
||||
{ field: "invalidCount", headerName: "Invalid", width: 80 },
|
||||
{ field: "openCount", headerName: "Opens", width: 70 },
|
||||
{ field: "uniqueOpenCount", headerName: "Unique Opens", width: 120 },
|
||||
{ field: "clickCount", headerName: "Clicks", width: 100 },
|
||||
{ field: "clickCount", headerName: "Clicks", width: 70 },
|
||||
{ field: "uniqueClickCount", headerName: "Unique Clicks", width: 120 },
|
||||
{ field: "bounceCount", headerName: "Bounces", width: 100 },
|
||||
{ field: "spamCount", headerName: "Spam", width: 100 },
|
||||
{ field: "bounceCount", headerName: "Bounces", width: 80 },
|
||||
{ field: "spamCount", headerName: "Spam", width: 60 },
|
||||
{ field: "unsubscribeCount", headerName: "Unsubscribes", width: 120 },
|
||||
];
|
||||
|
||||
@ -261,6 +267,9 @@ function CompletedMailings() {
|
||||
pageSize: 20,
|
||||
},
|
||||
},
|
||||
sorting: {
|
||||
sortModel: [{ field: 'sentDate', sort: 'desc' }], // Default sort by scheduleDate, descending
|
||||
},
|
||||
}}
|
||||
pageSizeOptions={[10, 20, 50, 100]}
|
||||
/>
|
||||
|
||||
@ -1,34 +1,19 @@
|
||||
// src/components/pages/Home.tsx
|
||||
//import React from 'react';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import Box from '@mui/material/Box';
|
||||
import Grid2 from '@mui/material/Grid2'; // v6 Grid2
|
||||
//import Card from '@mui/material/Card';
|
||||
//import CardContent from '@mui/material/CardContent';
|
||||
//import { CardActionArea } from '@mui/material';
|
||||
import { BarChart } from '@mui/x-charts/BarChart';
|
||||
|
||||
import LineChartSample from '@/components/widgets/LineChartSample';
|
||||
import RecentMailingStatsChart from '@/components/widgets/RecentMailingStatsChart';
|
||||
|
||||
const Home = () => {
|
||||
return (
|
||||
<Box sx={{ flexGrow: 1 }}>
|
||||
<Box sx={{
|
||||
position: 'relative', left: 0, right: 0, height: "calc(100vh - 124px)", maxHeight: "700px", overflow: "hidden",
|
||||
}}>
|
||||
<Typography variant="h4" component="h1" gutterBottom>
|
||||
Welcome to Surge365
|
||||
Sent Mailing Statistics
|
||||
</Typography>
|
||||
<Grid2 container spacing={2}>
|
||||
<Grid2 size={{ xs: 12, sm: 6, md: 6 }}>
|
||||
<BarChart
|
||||
xAxis={[{ scaleType: 'band', data: ['group A', 'group B', 'group C'] }]}
|
||||
series={[{ data: [4, 3, 5] }, { data: [1, 6, 3] }, { data: [2, 5, 6] }]}
|
||||
width={500}
|
||||
height={300}
|
||||
/>
|
||||
</Grid2>
|
||||
<Grid2 size={{ xs: 12, sm: 6, md: 4 }}>
|
||||
<LineChartSample />
|
||||
</Grid2>
|
||||
</Grid2>
|
||||
<RecentMailingStatsChart days={14} />
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
@ -8,8 +8,7 @@ import {
|
||||
Box,
|
||||
Alert,
|
||||
} from '@mui/material';
|
||||
import { AuthResponse, AuthErrorResponse, User, isAuthErrorResponse } from '@/types/auth';
|
||||
import utils from '@/ts/utils';
|
||||
import { AuthResponse, User } from '@/types/auth';
|
||||
//import ForgotPasswordModal from '@/components/modals/ForgotPasswordModal';
|
||||
|
||||
type SpinnerState = Record<string, boolean>;
|
||||
@ -72,35 +71,46 @@ function Login() {
|
||||
let loggedInUser: User | null = null;
|
||||
let hadLoginError: boolean = false;
|
||||
let hadLoginErrorMessage: string = '';
|
||||
await utils.webMethod<AuthResponse>({
|
||||
methodPage: 'authentication',
|
||||
methodName: 'authenticate',
|
||||
parameters: { username, password },
|
||||
success: (json: AuthResponse) => {
|
||||
try {
|
||||
localStorage.setItem('accessToken', json.accessToken);
|
||||
loggedInUser = json.user;
|
||||
} catch {
|
||||
const errorMsg: string = 'Unexpected Error';
|
||||
hadLoginError = true;
|
||||
hadLoginErrorMessage = errorMsg;
|
||||
}
|
||||
},
|
||||
error: (err: unknown) => {
|
||||
let errorMsg: string = 'Unexpected Error';
|
||||
if (isAuthErrorResponse(err)) {
|
||||
const errorResponse = err as AuthErrorResponse;
|
||||
if (errorResponse.data?.message) {
|
||||
errorMsg = errorResponse.data.message;
|
||||
}
|
||||
console.error(errorMsg);
|
||||
setLoginErrorMessage(errorMsg);
|
||||
}
|
||||
hadLoginError = true;
|
||||
hadLoginErrorMessage = errorMsg;
|
||||
},
|
||||
|
||||
const apiUrl = "/api/authentication/authenticate";
|
||||
const response = await fetch(apiUrl, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ username, password }),
|
||||
});
|
||||
|
||||
//if (!response.ok) throw new Error(isNew ? "Failed to create" : "Failed to update");
|
||||
|
||||
if (response.ok) {
|
||||
const json: AuthResponse = await response.json();
|
||||
try {
|
||||
localStorage.setItem('accessToken', json.accessToken);
|
||||
loggedInUser = json.user;
|
||||
|
||||
if (loggedInUser == null) {
|
||||
setLoginError(true);
|
||||
setIsLoading(false);
|
||||
setSpinners({ Login: false });
|
||||
} else {
|
||||
await finishUserLogin(loggedInUser);
|
||||
}
|
||||
|
||||
} catch {
|
||||
hadLoginError = true;
|
||||
hadLoginErrorMessage = 'Unexpected Error';
|
||||
}
|
||||
}
|
||||
else {
|
||||
let data = null;
|
||||
try {
|
||||
data = await response.json();
|
||||
} catch {
|
||||
// Intentionally empty, if not json ignore
|
||||
}
|
||||
hadLoginError = true;
|
||||
hadLoginErrorMessage = data?.message ?? 'Unexpected Error';
|
||||
}
|
||||
|
||||
if (hadLoginError) {
|
||||
setLoginErrorMessage(hadLoginErrorMessage);
|
||||
setLoginError(true);
|
||||
@ -131,19 +141,20 @@ function Login() {
|
||||
resetAppSettings();
|
||||
}, []);
|
||||
|
||||
const finishUserLogin = async (loggedInUser: User) => {
|
||||
const finishUserLogin = async (_: User) => {
|
||||
setIsLoading(false);
|
||||
setSpinners({ Login: false, LoginWithPasskey: false });
|
||||
|
||||
utils.localStorage('session_currentUser', loggedInUser);
|
||||
//utils.localStorage('session_currentUser', loggedInUser);
|
||||
|
||||
const redirectUrl = utils.sessionStorage('redirect_url');
|
||||
if (redirectUrl) {
|
||||
utils.sessionStorage('redirect_url', null);
|
||||
document.location.href = redirectUrl;
|
||||
} else {
|
||||
document.location.href = '/home';
|
||||
}
|
||||
//const redirectUrl = utils.sessionStorage('redirect_url');
|
||||
//if (redirectUrl) {
|
||||
// utils.sessionStorage('redirect_url', null);
|
||||
// document.location.href = redirectUrl;
|
||||
//} else {
|
||||
// document.location.href = '/home';
|
||||
//}
|
||||
document.location.href = '/home';
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@ -154,6 +154,9 @@ function NewMailings() {
|
||||
pageSize: 20,
|
||||
},
|
||||
},
|
||||
sorting: {
|
||||
sortModel: [{ field: 'id', sort: 'asc' }],
|
||||
},
|
||||
}}
|
||||
pageSizeOptions={[10, 20, 50, 100]}
|
||||
/>
|
||||
|
||||
@ -3,7 +3,7 @@ import VisibilityIcon from '@mui/icons-material/Visibility';
|
||||
import ContentCopyIcon from '@mui/icons-material/ContentCopy';
|
||||
import CancelIcon from '@mui/icons-material/Cancel';
|
||||
import RefreshIcon from '@mui/icons-material/Refresh';
|
||||
import { Box, useTheme, CircularProgress, IconButton } from '@mui/material';
|
||||
import { Box, useTheme, useMediaQuery, CircularProgress, IconButton, List, Card, CardContent, Typography } from '@mui/material';
|
||||
import { DataGrid, GridColDef, GridRenderCellParams, GridToolbarContainer, GridToolbarQuickFilter, GridToolbarExport, GridToolbarDensitySelector, GridToolbarColumnsButton } from '@mui/x-data-grid';
|
||||
import Mailing from '@/types/mailing';
|
||||
import MailingEdit from "@/components/modals/MailingEdit";
|
||||
@ -12,6 +12,7 @@ import ConfirmationDialog from "@/components/modals/ConfirmationDialog";
|
||||
|
||||
function ScheduleMailings() {
|
||||
const theme = useTheme();
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down("sm"));
|
||||
|
||||
const gridContainerRef = useRef<HTMLDivElement | null>(null);
|
||||
const [mailingsLoading, setMailingsLoading] = useState<boolean>(false);
|
||||
@ -21,6 +22,7 @@ function ScheduleMailings() {
|
||||
const [editOpen, setEditOpen] = useState<boolean>(false);
|
||||
const [confirmDialogOpen, setConfirmDialogOpen] = useState<boolean>(false);
|
||||
const [mailingToCancel, setMailingToCancel] = useState<Mailing | null>(null);
|
||||
const [forceRender, setForceRender] = useState(false);
|
||||
|
||||
const formatRecurringString = (typeCode: string, startDate: string): string => {
|
||||
const date = new Date(startDate);
|
||||
@ -82,6 +84,7 @@ function ScheduleMailings() {
|
||||
if (mailingsData) {
|
||||
setMailings(mailingsData);
|
||||
setMailingsLoading(false);
|
||||
setForceRender(true);
|
||||
} else {
|
||||
console.error("Failed to fetch scheduled mailings");
|
||||
setMailingsLoading(false);
|
||||
@ -135,6 +138,12 @@ function ScheduleMailings() {
|
||||
reloadMailings();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (forceRender) {
|
||||
setForceRender(false);
|
||||
}
|
||||
}, [forceRender]);
|
||||
|
||||
return (
|
||||
<Box ref={gridContainerRef} sx={{
|
||||
position: 'relative', left: 0, right: 0, height: "calc(100vh - 124px)", overflow: "hidden",
|
||||
@ -143,7 +152,43 @@ function ScheduleMailings() {
|
||||
duration: theme.transitions.duration.standard,
|
||||
})
|
||||
}}>
|
||||
<Box sx={{ position: 'absolute', inset: 0 }}>
|
||||
<Box sx={{ position: 'absolute', inset: 0 }}>{isMobile ? (
|
||||
mailingsLoading ? (
|
||||
<Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', height: '100%' }}>
|
||||
<CircularProgress />
|
||||
</Box>
|
||||
) : (
|
||||
<List>
|
||||
{mailings.map((mailing) => (
|
||||
<Card key={mailing.id} sx={{ marginBottom: 2 }}>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<CardContent>
|
||||
<Typography variant="h6">{mailing.name}</Typography>
|
||||
<Typography variant="body2">
|
||||
Schedule Date: {mailing.scheduleDate ? new Date(mailing.scheduleDate).toLocaleString() : 'N/A'}
|
||||
</Typography>
|
||||
<Typography variant="body2">
|
||||
Recurring: {mailing.recurringTypeCode && mailing.recurringStartDate
|
||||
? formatRecurringString(mailing.recurringTypeCode, mailing.recurringStartDate)
|
||||
: 'None'}
|
||||
</Typography>
|
||||
</CardContent>
|
||||
<Box>
|
||||
<IconButton onClick={() => handleView(mailing)}>
|
||||
<VisibilityIcon />
|
||||
</IconButton>
|
||||
<IconButton onClick={() => handleCopy(mailing)}>
|
||||
<ContentCopyIcon />
|
||||
</IconButton>
|
||||
<IconButton color="secondary" onClick={() => handleCancelClick(mailing)}>
|
||||
<CancelIcon />
|
||||
</IconButton>
|
||||
</Box>
|
||||
</Box>
|
||||
</Card>
|
||||
))}
|
||||
</List>
|
||||
)) : (
|
||||
<DataGrid
|
||||
rows={mailings}
|
||||
columns={columns}
|
||||
@ -173,11 +218,14 @@ function ScheduleMailings() {
|
||||
pageSize: 20,
|
||||
},
|
||||
},
|
||||
sorting: {
|
||||
sortModel: [{ field: 'scheduleDate', sort: 'asc' }], // Default sort by scheduleDate, descending
|
||||
},
|
||||
}}
|
||||
pageSizeOptions={[10, 20, 50, 100]}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{viewOpen && (
|
||||
<MailingView
|
||||
open={viewOpen}
|
||||
|
||||
@ -32,14 +32,14 @@ function Targets() {
|
||||
</IconButton>
|
||||
),
|
||||
},
|
||||
{ field: "id", headerName: "ID", width: 60 },
|
||||
{ field: "id", headerName: "ID", width: 100 },
|
||||
//{ field: "serverKey", headerName: "Server Key", flex: 1, minWidth: 140 },
|
||||
{ field: "name", headerName: "Name", flex: 1, minWidth: 160 },
|
||||
{ field: "databaseName", headerName: "Database", flex: 1, minWidth: 100 },
|
||||
{ field: "viewName", headerName: "View", flex: 1, minWidth: 300 },
|
||||
{ field: "filterQuery", headerName: "Filter Query", flex: 1, minWidth: 100 },
|
||||
{ field: "allowWriteBack", headerName: "WriteBack?", width: 100 },
|
||||
{ field: "isActive", headerName: "Active", width: 75 },
|
||||
{ field: "allowWriteBack", headerName: "Write Back", width: 150 },
|
||||
{ field: "isActive", headerName: "Active", width: 115 },
|
||||
];
|
||||
|
||||
|
||||
|
||||
@ -0,0 +1,41 @@
|
||||
import { DataGrid, GridColDef } from '@mui/x-data-grid';
|
||||
|
||||
type TargetSampleDataGridProps = {
|
||||
columns: GridColDef[];
|
||||
rows: { [key: string]: string }[];
|
||||
};
|
||||
|
||||
const TargetSampleDataGrid = ({ columns, rows }: TargetSampleDataGridProps) => {
|
||||
return (
|
||||
<DataGrid
|
||||
rows={rows}
|
||||
columns={columns}
|
||||
autoHeight
|
||||
hideFooter
|
||||
hideFooterPagination
|
||||
sx={{
|
||||
width: "100%",
|
||||
maxHeight: "800px",
|
||||
'& .MuiDataGrid-main': {
|
||||
overflowX: 'auto',
|
||||
},
|
||||
'& .MuiDataGrid-columnHeaderTitle': {
|
||||
overflow: 'visible',
|
||||
whiteSpace: 'normal',
|
||||
lineHeight: '1.2em',
|
||||
textOverflow: 'ellipsis',
|
||||
},
|
||||
}}
|
||||
initialState={{
|
||||
pagination: {
|
||||
paginationModel: {
|
||||
pageSize: 10,
|
||||
},
|
||||
},
|
||||
}}
|
||||
pageSizeOptions={[]}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default TargetSampleDataGrid;
|
||||
@ -0,0 +1,108 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Box } from '@mui/material';
|
||||
import { BarChart } from '@mui/x-charts/BarChart';
|
||||
import dayjs from 'dayjs';
|
||||
import MailingStatistic from '@/types/mailingStatistic';
|
||||
|
||||
export default function RecentMailingStatsChart({ days = 7 }: { days?: number }) {
|
||||
const [stats, setStats] = useState<MailingStatistic[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
var startDate = dayjs().subtract(days, 'day').format('YYYY-MM-DD');
|
||||
var endDate = dayjs().format('YYYY-MM-DD');
|
||||
|
||||
const fetchStats = async () => {
|
||||
try {
|
||||
const response = await fetch(`/api/mailings/status/s%2Csd/stats?startDate=${startDate}&endDate=${endDate}`);
|
||||
const data: MailingStatistic[] = await response.json();
|
||||
setStats(data);
|
||||
} catch (error) {
|
||||
console.error('Error fetching mailing stats:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchStats();
|
||||
const intervalId = setInterval(fetchStats, 5000);
|
||||
return () => clearInterval(intervalId);
|
||||
}, [days]);
|
||||
|
||||
if (loading) {
|
||||
return <Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', height: '100%' }}>Loading...</Box>;
|
||||
}
|
||||
|
||||
if (!stats.length) {
|
||||
return <Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', height: '100%' }}>No data available</Box>;
|
||||
}
|
||||
|
||||
// Aggregate stats by date
|
||||
const aggregatedStats = stats.reduce((acc, stat) => {
|
||||
const dateStr = stat.sentDate ? dayjs(stat.sentDate).format('YYYY-MM-DD') : dayjs().format('YYYY-MM-DD');
|
||||
if (!acc[dateStr]) {
|
||||
acc[dateStr] = {
|
||||
sentCount: 0,
|
||||
deliveredCount: 0,
|
||||
failedCount: 0,
|
||||
bounceCount: 0,
|
||||
blockedCount: 0,
|
||||
invalidCount: 0,
|
||||
};
|
||||
}
|
||||
acc[dateStr].sentCount += stat.sendCount;
|
||||
acc[dateStr].deliveredCount += stat.deliveredCount;
|
||||
acc[dateStr].failedCount += stat.failedCount;
|
||||
acc[dateStr].bounceCount += stat.bounceCount;
|
||||
acc[dateStr].blockedCount += stat.blockedCount;
|
||||
acc[dateStr].invalidCount += stat.invalidCount;
|
||||
return acc;
|
||||
}, {} as Record<string, { sentCount: number; deliveredCount: number; failedCount: number; bounceCount: number; blockedCount: number; invalidCount: number }>);
|
||||
|
||||
// Prepare chart data, sorted by date ascending
|
||||
const sortedDates = Object.keys(aggregatedStats).sort((a, b) => dayjs(a).isBefore(dayjs(b)) ? -1 : 1);
|
||||
const dates = sortedDates.map(date => new Date(date));
|
||||
const sentData = sortedDates.map(date => aggregatedStats[date].sentCount);
|
||||
const deliveredData = sortedDates.map(date => aggregatedStats[date].deliveredCount);
|
||||
const errorData = sortedDates.map(date => (
|
||||
aggregatedStats[date].failedCount +
|
||||
aggregatedStats[date].bounceCount +
|
||||
aggregatedStats[date].blockedCount +
|
||||
aggregatedStats[date].invalidCount
|
||||
));
|
||||
|
||||
return (
|
||||
<BarChart
|
||||
margin={{
|
||||
left: 80,
|
||||
right: 80,
|
||||
top: 80,
|
||||
bottom: 80,
|
||||
}}
|
||||
series={[
|
||||
{
|
||||
data: errorData,
|
||||
label: 'Errors',
|
||||
stack: 'total',
|
||||
},
|
||||
{
|
||||
data: deliveredData,
|
||||
label: 'Delivered',
|
||||
stack: 'total',
|
||||
},
|
||||
{
|
||||
data: sentData,
|
||||
label: 'Sent',
|
||||
stack: 'total',
|
||||
},
|
||||
]}
|
||||
xAxis={[
|
||||
{
|
||||
scaleType: 'band',
|
||||
data: dates,
|
||||
valueFormatter: (value) => dayjs(value).format("MMM DD"),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@ -46,91 +46,91 @@ const utils = {
|
||||
const results = regex.exec(window.location.search);
|
||||
return results ? decodeURIComponent(results[1].replace(/\+/g, ' ')) : null;
|
||||
},
|
||||
addAuthHeaders: (headers: Record<string, string> = {}): Record<string, string> => {
|
||||
const authToken = utils.getCookie('Auth-Token');
|
||||
if (authToken) {
|
||||
headers['Auth-Token'] = authToken;
|
||||
}
|
||||
//addAuthHeaders: (headers: Record<string, string> = {}): Record<string, string> => {
|
||||
// const authToken = utils.getCookie('Auth-Token');
|
||||
// if (authToken) {
|
||||
// headers['Auth-Token'] = authToken;
|
||||
// }
|
||||
|
||||
const impersonateGuid = utils.getParameterByName("impersonateid") || sessionStorage.getItem('Auth-Impersonate-Guid');
|
||||
if (impersonateGuid) {
|
||||
sessionStorage.setItem('Auth-Impersonate-Guid', impersonateGuid);
|
||||
headers['Auth-Impersonate-Guid'] = impersonateGuid;
|
||||
}
|
||||
// const impersonateGuid = utils.getParameterByName("impersonateid") || sessionStorage.getItem('Auth-Impersonate-Guid');
|
||||
// if (impersonateGuid) {
|
||||
// sessionStorage.setItem('Auth-Impersonate-Guid', impersonateGuid);
|
||||
// headers['Auth-Impersonate-Guid'] = impersonateGuid;
|
||||
// }
|
||||
|
||||
const franchiseCode = sessionStorage.getItem('franchiseCode');
|
||||
if (franchiseCode) {
|
||||
headers['Auth-Current-Franchise'] = franchiseCode;
|
||||
}
|
||||
// const franchiseCode = sessionStorage.getItem('franchiseCode');
|
||||
// if (franchiseCode) {
|
||||
// headers['Auth-Current-Franchise'] = franchiseCode;
|
||||
// }
|
||||
|
||||
return headers;
|
||||
},
|
||||
webMethod: async <T = unknown>({
|
||||
httpMethod = 'POST',
|
||||
baseMethodPath = 'api/',
|
||||
methodPage = '',
|
||||
methodName = '',
|
||||
parameters = {} as Record<string, unknown>,
|
||||
contentType = 'application/json;',
|
||||
timeout = 300000,
|
||||
success = (_data: T) => { },
|
||||
error = (_err: unknown) => { },
|
||||
}: {
|
||||
httpMethod?: string;
|
||||
baseMethodPath?: string;
|
||||
methodPage?: string;
|
||||
methodName?: string;
|
||||
contentType?: string;
|
||||
parameters?: Record<string, unknown>;
|
||||
timeout?: number;
|
||||
success?: (_data: T) => void;
|
||||
error?: (_err: unknown) => void;
|
||||
}): Promise<void> => {
|
||||
try {
|
||||
const baseUrl = window.API_BASE_URL || '';
|
||||
const url = `${baseUrl.replace(/\/$/, '')}/${baseMethodPath.replace(/\/$/, '')}/${methodPage}${methodName ? '/' + methodName : ''}`;
|
||||
// return headers;
|
||||
//},
|
||||
//webMethod: async <T = unknown>({
|
||||
// httpMethod = 'POST',
|
||||
// baseMethodPath = 'api/',
|
||||
// methodPage = '',
|
||||
// methodName = '',
|
||||
// parameters = {} as Record<string, unknown>,
|
||||
// contentType = 'application/json;',
|
||||
// timeout = 300000,
|
||||
// success = (_data: T) => { },
|
||||
// error = (_err: unknown) => { },
|
||||
//}: {
|
||||
// httpMethod?: string;
|
||||
// baseMethodPath?: string;
|
||||
// methodPage?: string;
|
||||
// methodName?: string;
|
||||
// contentType?: string;
|
||||
// parameters?: Record<string, unknown>;
|
||||
// timeout?: number;
|
||||
// success?: (_data: T) => void;
|
||||
// error?: (_err: unknown) => void;
|
||||
//}): Promise<void> => {
|
||||
// try {
|
||||
// const baseUrl = window.API_BASE_URL || '';
|
||||
// const url = `${baseUrl.replace(/\/$/, '')}/${baseMethodPath.replace(/\/$/, '')}/${methodPage}${methodName ? '/' + methodName : ''}`;
|
||||
|
||||
const headers = utils.addAuthHeaders({
|
||||
'Content-Type': contentType,
|
||||
});
|
||||
// const headers = utils.addAuthHeaders({
|
||||
// 'Content-Type': contentType,
|
||||
// });
|
||||
|
||||
const controller = new AbortController();
|
||||
//const timeoutId = setTimeout(() => controller.abort(), timeout);
|
||||
setTimeout(() => controller.abort(), timeout);
|
||||
// const controller = new AbortController();
|
||||
// //const timeoutId = setTimeout(() => controller.abort(), timeout);
|
||||
// setTimeout(() => controller.abort(), timeout);
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: httpMethod,
|
||||
headers,
|
||||
body: (httpMethod.toUpperCase() == "GET" ? null : JSON.stringify(parameters)),
|
||||
signal: controller.signal,
|
||||
});
|
||||
// const response = await fetch(url, {
|
||||
// method: httpMethod,
|
||||
// headers,
|
||||
// body: (httpMethod.toUpperCase() == "GET" ? null : JSON.stringify(parameters)),
|
||||
// signal: controller.signal,
|
||||
// });
|
||||
|
||||
if (!response.ok) {
|
||||
let data = null;
|
||||
try {
|
||||
data = await response.json();
|
||||
} catch {
|
||||
// Intentionally empty, if not json ignore
|
||||
}
|
||||
throw new ApiError(response.status, data);
|
||||
}
|
||||
// if (!response.ok) {
|
||||
// let data = null;
|
||||
// try {
|
||||
// data = await response.json();
|
||||
// } catch {
|
||||
// // Intentionally empty, if not json ignore
|
||||
// }
|
||||
// throw new ApiError(response.status, data);
|
||||
// }
|
||||
|
||||
const authToken = response.headers.get('Auth-Token');
|
||||
const loggedIn = response.headers.get('usahl_logged_in') === 'true';
|
||||
// const authToken = response.headers.get('Auth-Token');
|
||||
// const loggedIn = response.headers.get('usahl_logged_in') === 'true';
|
||||
|
||||
const expires = loggedIn ? 365 : 14;
|
||||
document.cookie = `Auth-Token=${authToken};path=/;max-age=${expires * 24 * 60 * 60}`;
|
||||
document.cookie = `usahl_logged_in=${loggedIn};path=/;max-age=${expires * 24 * 60 * 60}`;
|
||||
// const expires = loggedIn ? 365 : 14;
|
||||
// document.cookie = `Auth-Token=${authToken};path=/;max-age=${expires * 24 * 60 * 60}`;
|
||||
// document.cookie = `usahl_logged_in=${loggedIn};path=/;max-age=${expires * 24 * 60 * 60}`;
|
||||
|
||||
const data = await response.json();
|
||||
success(data);
|
||||
} catch (err) {
|
||||
if ((err as Error).name === 'AbortError') {
|
||||
console.error('Request timed out');
|
||||
}
|
||||
error(err);
|
||||
}
|
||||
},
|
||||
// const data = await response.json();
|
||||
// success(data);
|
||||
// } catch (err) {
|
||||
// if ((err as Error).name === 'AbortError') {
|
||||
// console.error('Request timed out');
|
||||
// }
|
||||
// error(err);
|
||||
// }
|
||||
//},
|
||||
getBoolean: (variable: any): boolean => {
|
||||
if (variable != null) {
|
||||
switch (typeof variable) {
|
||||
@ -144,47 +144,47 @@ const utils = {
|
||||
}
|
||||
return false;
|
||||
},
|
||||
isLoggedIn: (): boolean => {
|
||||
return utils.getBoolean(utils.getCookie('usahl_logged_in'));
|
||||
},
|
||||
sessionStorage: (key: string, value?: any): any => {
|
||||
if (value === undefined) {
|
||||
let val = window.sessionStorage.getItem(key);
|
||||
if (val && val.startsWith('usahl_json:')) {
|
||||
val = val.substring(11);
|
||||
return JSON.parse(val);
|
||||
}
|
||||
return val;
|
||||
} else {
|
||||
const val = typeof value === 'object' ? `usahl_json:${JSON.stringify(value)}` : value;
|
||||
window.sessionStorage.setItem(key, val);
|
||||
}
|
||||
},
|
||||
sessionStorageClear: (): void => {
|
||||
window.sessionStorage.clear();
|
||||
},
|
||||
sessionStorageRemove: (key: string): void => {
|
||||
window.sessionStorage.removeItem(key);
|
||||
},
|
||||
localStorage: (key: string, value?: any): any => {
|
||||
if (value === undefined) {
|
||||
let val = window.localStorage.getItem(key);
|
||||
if (val && val.startsWith('usahl_json:')) {
|
||||
val = val.substring(11);
|
||||
return JSON.parse(val);
|
||||
}
|
||||
return val;
|
||||
} else {
|
||||
const val = typeof value === 'object' ? `usahl_json:${JSON.stringify(value)}` : value;
|
||||
window.localStorage.setItem(key, val);
|
||||
}
|
||||
},
|
||||
localStorageClear: (): void => {
|
||||
window.localStorage.clear();
|
||||
},
|
||||
localStorageRemove: (key: string): void => {
|
||||
window.localStorage.removeItem(key);
|
||||
},
|
||||
//isLoggedIn: (): boolean => {
|
||||
// return utils.getBoolean(utils.getCookie('usahl_logged_in'));
|
||||
//},
|
||||
//sessionStorage: (key: string, value?: any): any => {
|
||||
// if (value === undefined) {
|
||||
// let val = window.sessionStorage.getItem(key);
|
||||
// if (val && val.startsWith('usahl_json:')) {
|
||||
// val = val.substring(11);
|
||||
// return JSON.parse(val);
|
||||
// }
|
||||
// return val;
|
||||
// } else {
|
||||
// const val = typeof value === 'object' ? `usahl_json:${JSON.stringify(value)}` : value;
|
||||
// window.sessionStorage.setItem(key, val);
|
||||
// }
|
||||
//},
|
||||
//sessionStorageClear: (): void => {
|
||||
// window.sessionStorage.clear();
|
||||
//},
|
||||
//sessionStorageRemove: (key: string): void => {
|
||||
// window.sessionStorage.removeItem(key);
|
||||
//},
|
||||
//localStorage: (key: string, value?: any): any => {
|
||||
// if (value === undefined) {
|
||||
// let val = window.localStorage.getItem(key);
|
||||
// if (val && val.startsWith('usahl_json:')) {
|
||||
// val = val.substring(11);
|
||||
// return JSON.parse(val);
|
||||
// }
|
||||
// return val;
|
||||
// } else {
|
||||
// const val = typeof value === 'object' ? `usahl_json:${JSON.stringify(value)}` : value;
|
||||
// window.localStorage.setItem(key, val);
|
||||
// }
|
||||
//},
|
||||
//localStorageClear: (): void => {
|
||||
// window.localStorage.clear();
|
||||
//},
|
||||
//localStorageRemove: (key: string): void => {
|
||||
// window.localStorage.removeItem(key);
|
||||
//},
|
||||
};
|
||||
|
||||
declare global {
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
export default interface MailingStatistic {
|
||||
mailingId: number;
|
||||
mailingName: string;
|
||||
sentDate: string;
|
||||
spamCount: number;
|
||||
uniqueClickCount: number;
|
||||
clickCount: number;
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import TargetColumn from './targetColumn';
|
||||
export interface Target {
|
||||
id: number;
|
||||
serverId: number;
|
||||
@ -7,6 +8,7 @@ export interface Target {
|
||||
filterQuery: string;
|
||||
allowWriteBack: boolean;
|
||||
isActive: boolean;
|
||||
columns: TargetColumn[];
|
||||
}
|
||||
|
||||
export default Target;
|
||||
11
Surge365.MassEmailReact.Web/src/types/targetColumn.ts
Normal file
11
Surge365.MassEmailReact.Web/src/types/targetColumn.ts
Normal file
@ -0,0 +1,11 @@
|
||||
export interface TargetColumn {
|
||||
id: number;
|
||||
targetId: number;
|
||||
typeCode: string;
|
||||
dataTypeCode: string;
|
||||
name: string;
|
||||
writeBack: boolean;
|
||||
isEmailAddress: boolean;
|
||||
}
|
||||
|
||||
export default TargetColumn;
|
||||
@ -0,0 +1,7 @@
|
||||
export interface TargetSampleColumn {
|
||||
name: string;
|
||||
type: string;
|
||||
dataType: string;
|
||||
}
|
||||
|
||||
export default TargetSampleColumn;
|
||||
Loading…
x
Reference in New Issue
Block a user