Refactor authentication and implement target management
- Updated `AuthenticationController` to return user data instead of token. - Added `ITargetService` and `ITargetRepository` to `Program.cs`. - Enhanced `appsettings.json` with connection timeout and security notes. - Modified `IAuthService` to reflect new authentication response structure. - Implemented connection retry mechanism in `DataAccess.cs`. - Refactored UI components to use MUI styling in `Layout.tsx`, `App.tsx`, and others. - Created new files for target management, including `TargetsController`, `TargetUpdateDto`, and related services. - Added `TargetEdit` modal for editing target details and `LineChartSample` for data visualization. - Updated package dependencies in `package-lock.json` and project file.
This commit is contained in:
parent
b3f266f9a8
commit
4180e50c9c
@ -22,10 +22,11 @@ namespace Surge365.MassEmailReact.Server.Controllers
|
|||||||
var authResponse = await _authService.Authenticate(request.Username, request.Password);
|
var authResponse = await _authService.Authenticate(request.Username, request.Password);
|
||||||
if (!authResponse.authenticated)
|
if (!authResponse.authenticated)
|
||||||
return Unauthorized(new { message = authResponse.errorMessage });
|
return Unauthorized(new { message = authResponse.errorMessage });
|
||||||
else if(authResponse.token == null)
|
else if(authResponse.data == null)
|
||||||
return Unauthorized(new { message = "Invalid credentials" });
|
return Unauthorized(new { message = "Invalid credentials" });
|
||||||
|
|
||||||
return Ok(new { success = true, authResponse.token.Value.accessToken });
|
//TODO: Store user in session
|
||||||
|
return Ok(new { success = true, authResponse.data.Value.accessToken, authResponse.data.Value.user }); //TODO: Send refresh token in http only cookie.
|
||||||
}
|
}
|
||||||
[HttpPost("refreshtoken")]
|
[HttpPost("refreshtoken")]
|
||||||
public IActionResult RefreshToken([FromBody] RefreshTokenRequest request)
|
public IActionResult RefreshToken([FromBody] RefreshTokenRequest request)
|
||||||
|
|||||||
54
Surge365.MassEmailReact.API/Controllers/TargetsController.cs
Normal file
54
Surge365.MassEmailReact.API/Controllers/TargetsController.cs
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
using Microsoft.AspNetCore.DataProtection.KeyManagement;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Surge365.MassEmailReact.Application.DTOs;
|
||||||
|
using Surge365.MassEmailReact.Application.Interfaces;
|
||||||
|
using Surge365.MassEmailReact.Domain.Entities;
|
||||||
|
|
||||||
|
namespace Surge365.MassEmailReact.Server.Controllers
|
||||||
|
{
|
||||||
|
[Route("api/[controller]")]
|
||||||
|
[ApiController]
|
||||||
|
public class TargetsController : ControllerBase
|
||||||
|
{
|
||||||
|
private readonly ITargetService _targetService;
|
||||||
|
|
||||||
|
public TargetsController(ITargetService targetService)
|
||||||
|
{
|
||||||
|
_targetService = targetService;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
[HttpGet("GetAll")]
|
||||||
|
public async Task<IActionResult> GetAll([FromQuery] bool? activeOnly)
|
||||||
|
{
|
||||||
|
var targets = await _targetService.GetAllAsync(activeOnly == null || activeOnly.Value ? true : false);
|
||||||
|
return Ok(targets);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("{key}")]
|
||||||
|
public async Task<IActionResult> GetByKey(int id)
|
||||||
|
{
|
||||||
|
var target = await _targetService.GetByIdAsync(id);
|
||||||
|
return target is not null ? Ok(target) : NotFound($"Target with key '{id}' not found.");
|
||||||
|
}
|
||||||
|
[HttpPut("{id}")]
|
||||||
|
public async Task<IActionResult> UpdateTarget(int id, [FromBody] TargetUpdateDto targetUpdateDto)
|
||||||
|
{
|
||||||
|
if (id != targetUpdateDto.Id)
|
||||||
|
return BadRequest("ID in URL does not match ID in request body");
|
||||||
|
|
||||||
|
var existingTarget = await _targetService.GetByIdAsync(id);
|
||||||
|
if (existingTarget == null)
|
||||||
|
return NotFound($"Target with ID {id} not found");
|
||||||
|
|
||||||
|
var success = await _targetService.UpdateAsync(targetUpdateDto);
|
||||||
|
if (!success)
|
||||||
|
return StatusCode(StatusCodes.Status500InternalServerError, "Failed to update target.");
|
||||||
|
|
||||||
|
var updatedTarget = await _targetService.GetByIdAsync(id);
|
||||||
|
|
||||||
|
return Ok(updatedTarget);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,5 +1,7 @@
|
|||||||
using Microsoft.AspNetCore.Identity;
|
using Microsoft.AspNetCore.Identity;
|
||||||
using Surge365.MassEmailReact.Application.Interfaces;
|
using Surge365.MassEmailReact.Application.Interfaces;
|
||||||
|
using Surge365.MassEmailReact.Domain.Entities;
|
||||||
|
using Surge365.MassEmailReact.Infrastructure.DapperMaps;
|
||||||
using Surge365.MassEmailReact.Infrastructure.Repositories;
|
using Surge365.MassEmailReact.Infrastructure.Repositories;
|
||||||
using Surge365.MassEmailReact.Infrastructure.Services;
|
using Surge365.MassEmailReact.Infrastructure.Services;
|
||||||
|
|
||||||
@ -12,6 +14,8 @@ builder.Services.AddControllers();
|
|||||||
builder.Services.AddOpenApi();
|
builder.Services.AddOpenApi();
|
||||||
builder.Services.AddScoped<IUserRepository, UserRepository>();
|
builder.Services.AddScoped<IUserRepository, UserRepository>();
|
||||||
builder.Services.AddScoped<IAuthService, AuthService>();
|
builder.Services.AddScoped<IAuthService, AuthService>();
|
||||||
|
builder.Services.AddScoped<ITargetService, TargetService>();
|
||||||
|
builder.Services.AddScoped<ITargetRepository, TargetRepository>();
|
||||||
var app = builder.Build();
|
var app = builder.Build();
|
||||||
|
|
||||||
app.UseDefaultFiles();
|
app.UseDefaultFiles();
|
||||||
@ -31,4 +35,6 @@ app.MapControllers();
|
|||||||
|
|
||||||
app.MapFallbackToFile("/index.html");
|
app.MapFallbackToFile("/index.html");
|
||||||
|
|
||||||
|
DapperConfiguration.ConfigureMappings();
|
||||||
|
|
||||||
app.Run();
|
app.Run();
|
||||||
|
|||||||
@ -12,6 +12,7 @@
|
|||||||
"AppCode": "MassEmailReactApi",
|
"AppCode": "MassEmailReactApi",
|
||||||
"EnvironmentCode": "UAT",
|
"EnvironmentCode": "UAT",
|
||||||
"ConnectionStrings": {
|
"ConnectionStrings": {
|
||||||
"Marketing.ConnectionString": "data source=uat.surge365.com;initial catalog=Marketing;User ID=ytb;Password=YTB()nl!n3;Encrypt=yes;TrustServerCertificate=yes;Application Name=##application_name##"
|
"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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
20
Surge365.MassEmailReact.Application/DTOs/TargetUpdateDto.cs
Normal file
20
Surge365.MassEmailReact.Application/DTOs/TargetUpdateDto.cs
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Surge365.MassEmailReact.Domain.Entities
|
||||||
|
{
|
||||||
|
public class TargetUpdateDto
|
||||||
|
{
|
||||||
|
public int? Id { get; set; }
|
||||||
|
public int ServerId { get; set; }
|
||||||
|
public string Name { get; set; } = "";
|
||||||
|
public string DatabaseName { get; set; } = "";
|
||||||
|
public string ViewName { get; set; } = "";
|
||||||
|
public string FilterQuery { get; set; } = "";
|
||||||
|
public bool AllowWriteBack { get; set; } = false;
|
||||||
|
public bool IsActive { get; set; } = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,8 +1,10 @@
|
|||||||
namespace Surge365.MassEmailReact.Application.Interfaces
|
using Surge365.MassEmailReact.Domain.Entities;
|
||||||
|
|
||||||
|
namespace Surge365.MassEmailReact.Application.Interfaces
|
||||||
{
|
{
|
||||||
public interface IAuthService
|
public interface IAuthService
|
||||||
{
|
{
|
||||||
Task<(bool authenticated, (string accessToken, string refreshToken)? token, string errorMessage)> Authenticate(string username, string password);
|
Task<(bool authenticated, (User user, string accessToken, string refreshToken)? data, string errorMessage)> Authenticate(string username, string password);
|
||||||
(string accessToken, string refreshToken)? GenerateTokens(Guid userId, string refreshToken);
|
(string accessToken, string refreshToken)? GenerateTokens(Guid userId, string refreshToken);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,16 @@
|
|||||||
|
using Surge365.MassEmailReact.Domain.Entities;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Surge365.MassEmailReact.Application.Interfaces
|
||||||
|
{
|
||||||
|
public interface ITargetRepository
|
||||||
|
{
|
||||||
|
Task<Target?> GetByIdAsync(int id);
|
||||||
|
Task<List<Target>> GetAllAsync(bool activeOnly = true);
|
||||||
|
Task<bool> UpdateAsync(Target target);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,11 @@
|
|||||||
|
using Surge365.MassEmailReact.Domain.Entities;
|
||||||
|
|
||||||
|
namespace Surge365.MassEmailReact.Application.Interfaces
|
||||||
|
{
|
||||||
|
public interface ITargetService
|
||||||
|
{
|
||||||
|
Task<Target?> GetByIdAsync(int id);
|
||||||
|
Task<List<Target>> GetAllAsync(bool activeOnly = true);
|
||||||
|
Task<bool> UpdateAsync(TargetUpdateDto targetDto);
|
||||||
|
}
|
||||||
|
}
|
||||||
37
Surge365.MassEmailReact.Domain/Entities/Target.cs
Normal file
37
Surge365.MassEmailReact.Domain/Entities/Target.cs
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Surge365.MassEmailReact.Domain.Entities
|
||||||
|
{
|
||||||
|
public class Target
|
||||||
|
{
|
||||||
|
public int? Id { get; private set; }
|
||||||
|
public int ServerId { get; set; }
|
||||||
|
public string Name { get; set; }
|
||||||
|
public string DatabaseName { get; set; }
|
||||||
|
public string ViewName { get; set; }
|
||||||
|
public string FilterQuery { get; set; }
|
||||||
|
public bool AllowWriteBack { get; set; }
|
||||||
|
public bool IsActive { get; set; }
|
||||||
|
|
||||||
|
public Target() { }
|
||||||
|
private Target(int id, int serverId, string name, string databaseName, string viewName, string filterQuery, bool allowWriteBack, bool isActive)
|
||||||
|
{
|
||||||
|
Id = id;
|
||||||
|
ServerId = ServerId;
|
||||||
|
Name = name;
|
||||||
|
DatabaseName = databaseName;
|
||||||
|
ViewName = viewName;
|
||||||
|
FilterQuery = filterQuery;
|
||||||
|
AllowWriteBack = allowWriteBack;
|
||||||
|
IsActive = isActive;
|
||||||
|
}
|
||||||
|
public static Target Create(int id, int serverId, string name, string databaseName, string viewName, string filterQuery, bool allowWriteBack, bool isActive)
|
||||||
|
{
|
||||||
|
return new Target(id, serverId, name, databaseName, viewName, filterQuery, allowWriteBack, isActive);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -21,7 +21,7 @@ namespace Surge365.MassEmailReact.Domain.Entities
|
|||||||
UserKey = userKey;
|
UserKey = userKey;
|
||||||
UserId = userId;
|
UserId = userId;
|
||||||
Username = username;
|
Username = username;
|
||||||
Username = firstName ?? "";
|
FirstName = firstName ?? "";
|
||||||
MiddleInitial = middleInitial ?? "";
|
MiddleInitial = middleInitial ?? "";
|
||||||
LastName = lastName ?? "";
|
LastName = lastName ?? "";
|
||||||
IsActive = isActive;
|
IsActive = isActive;
|
||||||
|
|||||||
@ -0,0 +1,20 @@
|
|||||||
|
using Dapper.FluentMap;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Surge365.MassEmailReact.Infrastructure.DapperMaps
|
||||||
|
{
|
||||||
|
public class DapperConfiguration
|
||||||
|
{
|
||||||
|
public static void ConfigureMappings()
|
||||||
|
{
|
||||||
|
FluentMapper.Initialize(config =>
|
||||||
|
{
|
||||||
|
config.AddMap(new TargetMap());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,25 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Dapper.FluentMap.Mapping;
|
||||||
|
using Surge365.MassEmailReact.Domain.Entities;
|
||||||
|
|
||||||
|
namespace Surge365.MassEmailReact.Infrastructure.DapperMaps
|
||||||
|
{
|
||||||
|
public class TargetMap : EntityMap<Target>
|
||||||
|
{
|
||||||
|
public TargetMap()
|
||||||
|
{
|
||||||
|
Map(t => t.Id).ToColumn("target_key");
|
||||||
|
Map(t => t.ServerId).ToColumn("server_key");
|
||||||
|
Map(t => t.Name).ToColumn("name");
|
||||||
|
Map(t => t.DatabaseName).ToColumn("database_name");
|
||||||
|
Map(t => t.ViewName).ToColumn("view_name");
|
||||||
|
Map(t => t.FilterQuery).ToColumn("filter_query");
|
||||||
|
Map(t => t.AllowWriteBack).ToColumn("allow_write_back");
|
||||||
|
Map(t => t.IsActive).ToColumn("is_active");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -233,8 +233,37 @@ namespace Surge365.MassEmailReact.Infrastructure
|
|||||||
#region Async
|
#region Async
|
||||||
internal async Task OpenConnectionAsync()
|
internal async Task OpenConnectionAsync()
|
||||||
{
|
{
|
||||||
_connection = new SqlConnection(_connectionString);
|
//_connection = new SqlConnection(_connectionString);
|
||||||
await _connection.OpenAsync();
|
//await _connection.OpenAsync();
|
||||||
|
await Task.Run(() => OpenConnectionWithRetry());
|
||||||
|
}
|
||||||
|
internal void OpenConnectionWithRetry(short maxRetries = 3, int totalTimeoutMs = 1000)
|
||||||
|
{
|
||||||
|
int attempt = 0;
|
||||||
|
while (attempt < maxRetries)
|
||||||
|
{
|
||||||
|
using (var cts = new CancellationTokenSource(totalTimeoutMs)) // Total cap, e.g., 3 seconds
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Attempt {attempt + 1}...");
|
||||||
|
_connection = new SqlConnection(_connectionString);
|
||||||
|
_connection.OpenAsync(cts.Token).Wait(); // Use async with cancellation
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
attempt++;
|
||||||
|
if (attempt == maxRetries)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Failed after {attempt} attempts: {ex.Message}");
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
Console.WriteLine($"Retrying after failure: {ex.Message}");
|
||||||
|
Thread.Sleep(1000); // Delay between retries
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
internal async Task CloseConnectionAsync()
|
internal async Task CloseConnectionAsync()
|
||||||
{
|
{
|
||||||
|
|||||||
@ -0,0 +1,92 @@
|
|||||||
|
using Dapper;
|
||||||
|
using Dapper.FluentMap;
|
||||||
|
using Microsoft.Data.SqlClient;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using Surge365.MassEmailReact.Application.Interfaces;
|
||||||
|
using Surge365.MassEmailReact.Domain.Entities;
|
||||||
|
using Surge365.MassEmailReact.Domain.Enums;
|
||||||
|
using Surge365.MassEmailReact.Domain.Enums.Extensions;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Data;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Surge365.MassEmailReact.Infrastructure.Repositories
|
||||||
|
{
|
||||||
|
public class TargetRepository : ITargetRepository
|
||||||
|
{
|
||||||
|
private IConfiguration _config;
|
||||||
|
private const string _connectionStringName = "MassEmail.ConnectionString";
|
||||||
|
private string? ConnectionString
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return _config.GetConnectionString(_connectionStringName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public TargetRepository(IConfiguration config)
|
||||||
|
{
|
||||||
|
_config = config;
|
||||||
|
#if DEBUG
|
||||||
|
if (!FluentMapper.EntityMaps.ContainsKey(typeof(Target)))
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Target dapper mapping is missing. Make sure ConfigureMappings() is called inside program.cs (program startup).");
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
public async Task<Target?> GetByIdAsync(int targetKey)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(_config);
|
||||||
|
ArgumentNullException.ThrowIfNull(_connectionStringName);
|
||||||
|
|
||||||
|
using SqlConnection conn = new SqlConnection(_config.GetConnectionString(_connectionStringName));
|
||||||
|
|
||||||
|
return (await conn.QueryAsync<Target>("SELECT * FROM mem_target WHERE target_key = @target_key", new { target_key = targetKey })).ToList().FirstOrDefault();
|
||||||
|
}
|
||||||
|
public async Task<List<Target>> GetAllAsync(bool activeOnly = true)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(_config);
|
||||||
|
ArgumentNullException.ThrowIfNull(_connectionStringName);
|
||||||
|
|
||||||
|
using SqlConnection conn = new SqlConnection(_config.GetConnectionString(_connectionStringName));
|
||||||
|
|
||||||
|
return (await conn.QueryAsync<Target>("mem_get_target_all", new { active_only = activeOnly }, commandType: CommandType.StoredProcedure)).ToList();
|
||||||
|
//return conn.Query<Target>("SELECT * FROM mem_target WHERE is_active = @active_only", new { active_only = activeOnly }).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> UpdateAsync(Target target)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(target);
|
||||||
|
ArgumentNullException.ThrowIfNull(target.Id);
|
||||||
|
using SqlConnection conn = new SqlConnection(_config.GetConnectionString(_connectionStringName));
|
||||||
|
|
||||||
|
var parameters = new DynamicParameters();
|
||||||
|
parameters.Add("@target_key", target.Id, DbType.Int32);
|
||||||
|
parameters.Add("@server_key", target.ServerId, DbType.Int32);
|
||||||
|
parameters.Add("@name", target.Name, DbType.String);
|
||||||
|
parameters.Add("@database_name", target.DatabaseName, DbType.String);
|
||||||
|
parameters.Add("@view_name", target.ViewName, DbType.String);
|
||||||
|
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);
|
||||||
|
|
||||||
|
// Output parameter
|
||||||
|
parameters.Add("@success", dbType: DbType.Boolean, direction: ParameterDirection.Output);
|
||||||
|
|
||||||
|
await conn.ExecuteAsync("mem_save_target", parameters, commandType: CommandType.StoredProcedure);
|
||||||
|
|
||||||
|
// Retrieve the output parameter value
|
||||||
|
bool success = parameters.Get<bool>("@success");
|
||||||
|
|
||||||
|
return success;
|
||||||
|
}
|
||||||
|
|
||||||
|
//public void Add(Target target)
|
||||||
|
//{
|
||||||
|
// Targets.Add(target);
|
||||||
|
//}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -26,7 +26,7 @@ namespace Surge365.MassEmailReact.Infrastructure.Services
|
|||||||
_config = config;
|
_config = config;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<(bool authenticated, (string accessToken, string refreshToken)? token, string errorMessage)> Authenticate(string username, string password)
|
public async Task<(bool authenticated, (User user, string accessToken, string refreshToken)? data, string errorMessage)> Authenticate(string username, string password)
|
||||||
{
|
{
|
||||||
var authResponse = await _userRepository.Authenticate(username, password);
|
var authResponse = await _userRepository.Authenticate(username, password);
|
||||||
if (authResponse.user == null)
|
if (authResponse.user == null)
|
||||||
@ -56,7 +56,7 @@ namespace Surge365.MassEmailReact.Infrastructure.Services
|
|||||||
var refreshToken = Convert.ToBase64String(Guid.NewGuid().ToByteArray());
|
var refreshToken = Convert.ToBase64String(Guid.NewGuid().ToByteArray());
|
||||||
//_userRepository.SaveRefreshToken(userId.Value, refreshToken); // TODO: Store refresh token in DB
|
//_userRepository.SaveRefreshToken(userId.Value, refreshToken); // TODO: Store refresh token in DB
|
||||||
|
|
||||||
return (true, (accessToken, refreshToken), "");
|
return (true, (authResponse.user, accessToken, refreshToken), "");
|
||||||
}
|
}
|
||||||
public (string accessToken, string refreshToken)? GenerateTokens(Guid userId, string refreshToken)
|
public (string accessToken, string refreshToken)? GenerateTokens(Guid userId, string refreshToken)
|
||||||
{
|
{
|
||||||
|
|||||||
@ -0,0 +1,55 @@
|
|||||||
|
using Surge365.MassEmailReact.Application.Interfaces;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Security.Claims;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using System.IdentityModel.Tokens.Jwt;
|
||||||
|
using Microsoft.IdentityModel.Tokens;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using Surge365.MassEmailReact.Domain.Entities;
|
||||||
|
using System.Security.Cryptography;
|
||||||
|
|
||||||
|
|
||||||
|
namespace Surge365.MassEmailReact.Infrastructure.Services
|
||||||
|
{
|
||||||
|
public class TargetService : ITargetService
|
||||||
|
{
|
||||||
|
private readonly ITargetRepository _targetRepository;
|
||||||
|
private readonly IConfiguration _config;
|
||||||
|
|
||||||
|
public TargetService(ITargetRepository targetRepository, IConfiguration config)
|
||||||
|
{
|
||||||
|
_targetRepository = targetRepository;
|
||||||
|
_config = config;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Target?> GetByIdAsync(int id)
|
||||||
|
{
|
||||||
|
return await _targetRepository.GetByIdAsync(id);
|
||||||
|
}
|
||||||
|
public async Task<List<Target>> GetAllAsync(bool activeOnly = true)
|
||||||
|
{
|
||||||
|
return await _targetRepository.GetAllAsync(activeOnly);
|
||||||
|
}
|
||||||
|
public async Task<bool> UpdateAsync(TargetUpdateDto targetDto)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(targetDto, nameof(targetDto));
|
||||||
|
ArgumentNullException.ThrowIfNull(targetDto.Id, nameof(targetDto.Id));
|
||||||
|
|
||||||
|
var target = await _targetRepository.GetByIdAsync(targetDto.Id.Value);
|
||||||
|
if (target == null || target.Id == null) return false;
|
||||||
|
|
||||||
|
target.ServerId = targetDto.ServerId;
|
||||||
|
target.Name = targetDto.Name;
|
||||||
|
target.DatabaseName = targetDto.DatabaseName;
|
||||||
|
target.ViewName = targetDto.ViewName;
|
||||||
|
target.FilterQuery = targetDto.FilterQuery;
|
||||||
|
target.AllowWriteBack = targetDto.AllowWriteBack;
|
||||||
|
target.IsActive = targetDto.IsActive;
|
||||||
|
|
||||||
|
return await _targetRepository.UpdateAsync(target);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -12,6 +12,7 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Dapper.FluentMap" Version="2.0.0" />
|
||||||
<PackageReference Include="Microsoft.Data.SqlClient" Version="6.0.1" />
|
<PackageReference Include="Microsoft.Data.SqlClient" Version="6.0.1" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Configuration" Version="9.0.2" />
|
<PackageReference Include="Microsoft.Extensions.Configuration" Version="9.0.2" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="9.0.2" />
|
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="9.0.2" />
|
||||||
|
|||||||
@ -16,4 +16,7 @@
|
|||||||
<None Remove="src\components\layouts\LayoutLogin_Backup.tsx" />
|
<None Remove="src\components\layouts\LayoutLogin_Backup.tsx" />
|
||||||
<None Remove="src\components\layouts\Layout_backup.tsx" />
|
<None Remove="src\components\layouts\Layout_backup.tsx" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<Folder Include="src\hooks\" />
|
||||||
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
@ -8,6 +8,6 @@
|
|||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
<script type="module" src="/src/components/pages/main.tsx"></script>
|
<script type="module" src="/src/components/pages/AppMain.tsx"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
1059
Surge365.MassEmailReact.Web/package-lock.json
generated
1059
Surge365.MassEmailReact.Web/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -11,13 +11,21 @@
|
|||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@emotion/react": "^11.14.0",
|
||||||
|
"@emotion/styled": "^11.14.0",
|
||||||
|
"@fontsource/roboto": "^5.1.1",
|
||||||
|
"@mui/icons-material": "^6.4.5",
|
||||||
|
"@mui/material": "^6.4.5",
|
||||||
|
"@mui/x-charts": "^7.27.1",
|
||||||
|
"@mui/x-data-grid": "^7.27.1",
|
||||||
"admin-lte": "4.0.0-beta3",
|
"admin-lte": "4.0.0-beta3",
|
||||||
"bootstrap": "^5.3.3",
|
"bootstrap": "^5.3.3",
|
||||||
|
"dayjs": "^1.11.13",
|
||||||
"font-awesome": "^4.7.0",
|
"font-awesome": "^4.7.0",
|
||||||
"ionicons": "^7.4.0",
|
"ionicons": "^7.4.0",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0",
|
|
||||||
"react-bootstrap": "^2.10.9",
|
"react-bootstrap": "^2.10.9",
|
||||||
|
"react-dom": "^19.0.0",
|
||||||
"react-icons": "^5.3.0",
|
"react-icons": "^5.3.0",
|
||||||
"react-router-dom": "^7.0.1"
|
"react-router-dom": "^7.0.1"
|
||||||
},
|
},
|
||||||
|
|||||||
@ -1,120 +1,212 @@
|
|||||||
import { ReactNode } from 'react';
|
// src/components/layouts/Layout.tsx
|
||||||
//import React from 'react';
|
import React, { ReactNode } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import { styled, useColorScheme } from '@mui/material/styles';
|
||||||
|
import Box from '@mui/material/Box';
|
||||||
|
import Drawer from '@mui/material/Drawer';
|
||||||
|
import AppBar from '@mui/material/AppBar';
|
||||||
|
import Toolbar from '@mui/material/Toolbar';
|
||||||
|
import List from '@mui/material/List';
|
||||||
|
import Typography from '@mui/material/Typography';
|
||||||
|
import Divider from '@mui/material/Divider';
|
||||||
|
import IconButton from '@mui/material/IconButton';
|
||||||
|
import MenuIcon from '@mui/icons-material/Menu';
|
||||||
|
import ChevronLeftIcon from '@mui/icons-material/ChevronLeft';
|
||||||
|
import ListItem from '@mui/material/ListItem';
|
||||||
|
import ListItemButton from '@mui/material/ListItemButton';
|
||||||
|
import ListItemIcon from '@mui/material/ListItemIcon';
|
||||||
|
import ListItemText from '@mui/material/ListItemText';
|
||||||
|
import DashboardIcon from '@mui/icons-material/Dashboard';
|
||||||
|
import DirectionsCarIcon from '@mui/icons-material/DirectionsCar';
|
||||||
|
import PeopleIcon from '@mui/icons-material/People';
|
||||||
|
import { Link as RouterLink } from 'react-router-dom';
|
||||||
|
import Select, { SelectChangeEvent } from '@mui/material/Select';
|
||||||
|
import MenuItem from '@mui/material/MenuItem';
|
||||||
|
import FormControl from '@mui/material/FormControl';
|
||||||
|
import InputLabel from '@mui/material/InputLabel';
|
||||||
|
|
||||||
import 'bootstrap/dist/css/bootstrap.min.css';
|
// Constants
|
||||||
import 'admin-lte/dist/css/adminlte.min.css';
|
const drawerWidth = 240;
|
||||||
import 'font-awesome/css/font-awesome.min.css';
|
|
||||||
/*import 'ionicons/dist/css/ionicons.min.css';*/
|
|
||||||
|
|
||||||
import 'bootstrap/dist/js/bootstrap.bundle.min.js'; // Bootstrap JS
|
// Styled components
|
||||||
import 'admin-lte/dist/js/adminlte.min.js';
|
const DrawerHeader = styled('div')(({ theme }) => ({
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
padding: theme.spacing(0, 1),
|
||||||
|
...theme.mixins.toolbar,
|
||||||
|
justifyContent: 'flex-end',
|
||||||
|
}));
|
||||||
|
|
||||||
const Layout = function Layout({ children }: { children: ReactNode }) {
|
const Main = styled('main', { shouldForwardProp: (prop) => prop !== 'open' })<{
|
||||||
return (
|
open?: boolean;
|
||||||
<div className="wrapper" style={{ overflow: 'initial' }}>
|
}>(({ theme, open }) => ({
|
||||||
<link rel="stylesheet" href="/content/dist/css/skins/_all-skins.min.css" />
|
flexGrow: 1,
|
||||||
<link href="/content/plugins/datepicker/v4/bootstrap-datetimepicker.css" rel="stylesheet" />
|
padding: theme.spacing(3),
|
||||||
<link rel="stylesheet" href="/content/plugins/bootstrap-wysihtml5/bootstrap3-wysihtml5.min.css" />
|
transition: theme.transitions.create(['margin', 'width', 'padding'], {
|
||||||
<link rel="stylesheet" href="/content/plugins/datatables/dataTables.bootstrap.css" />
|
easing: theme.transitions.easing.sharp,
|
||||||
|
duration: theme.transitions.duration.enteringScreen,
|
||||||
|
}),
|
||||||
|
marginLeft: "0px !important", // Force remove any margin on the left
|
||||||
|
marginRight: "0px !important", // Force remove any margin on the left
|
||||||
|
...(open && {/*Opened specific types go here*/}),
|
||||||
|
...(!open && {/*closed specific styles go here*/})
|
||||||
|
}));
|
||||||
|
|
||||||
{/*<script src="/content/dist/js/app.min.js"></script>AdminLTE?*/}
|
interface LayoutProps {
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.11.2/moment.min.js"></script>
|
children: ReactNode;
|
||||||
{/*<script src="/content/plugins/datatables/jquery.dataTables.min.js"></script>*/}
|
|
||||||
{/*<script src="/content/plugins/validator/validator.js"></script>*/}
|
|
||||||
<script src="/content/js/jquery.surge365.utilities-1.0.js" />
|
|
||||||
<script src="/content/js/jquery.surge365.global.js?36"></script>
|
|
||||||
<script src="/content/js/jquery.surge365.webmethods-1.0.js?15"></script>
|
|
||||||
<script src="/content/js/Main-1.0.js?39"></script>
|
|
||||||
{/*<script src="/content/plugins/growl/jquery.bootstrap-growl.min.js"></script>*/}
|
|
||||||
{/*<script src="/content/plugins/input-mask-5/jquery.inputmask.js"></script>*/}
|
|
||||||
{/*<script src="/content/plugins/printThis/printThis.js"></script>*/}
|
|
||||||
{/*<script src="/content/plugins/datatables/dataTables.bootstrap.min.js"></script>*/}
|
|
||||||
<header className="main-header">
|
|
||||||
<a href="Dashboard" className="logo" style={{ backgroundColor: '#333' }}>
|
|
||||||
<span className="logo-mini" style={{ backgroundColor: '#333' }}>
|
|
||||||
<img id="imgLogo-sm" src="/content/img/imove_mini_logo.png" style={{ height: '35px', marginBottom: '3px' }} alt="Logo" className="hide" />
|
|
||||||
</span>
|
|
||||||
<span className="logo-lg" style={{ textAlign: 'center', backgroundColor: '#333' }}>
|
|
||||||
<img id="imgLogo-lg" src="/content/img/imove_mini_logo.png" style={{ height: '35px', marginBottom: '3px' }} alt="Logo" className="hide" />
|
|
||||||
<span style={{ marginLeft: '5px', verticalAlign: 'middle' }}><b>USA</b>Haulers</span>
|
|
||||||
</span>
|
|
||||||
</a>
|
|
||||||
<nav className="navbar navbar-static-top">
|
|
||||||
<a href="#" className="fa5 sidebar-toggle" data-toggle="offcanvas" role="button">
|
|
||||||
<span className="sr-only">Toggle navigation</span>
|
|
||||||
</a>
|
|
||||||
<div className="navbar-custom-menu">
|
|
||||||
<ul className="nav navbar-nav">
|
|
||||||
<li className="dropdown user user-menu">
|
|
||||||
<a href="#" className="dropdown-toggle" data-toggle="dropdown">
|
|
||||||
<span id="spanSpinner" className="fa fa-sync-alt pull-left" style={{ lineHeight: 'unset', display: 'none', fontSize: '16pt', fontWeight: 'bold', marginRight: '10px' }}></span>
|
|
||||||
<img id="imgRightProfile" src="/content/img/generic_avatar.jpg" className="user-image" alt="User Image" />
|
|
||||||
<span id="spanProfileName"></span>
|
|
||||||
</a>
|
|
||||||
<ul className="dropdown-menu">
|
|
||||||
<li className="user-header">
|
|
||||||
<img id="imgMainProfile" src="" className="img-circle" alt="User Image" />
|
|
||||||
<p>
|
|
||||||
<span id="spanProfileNameTitle"></span>
|
|
||||||
</p>
|
|
||||||
</li>
|
|
||||||
<li className="user-footer">
|
|
||||||
<div className="pull-left">
|
|
||||||
<a href="Profile" className="btn btn-default btn-flat">Profile</a>
|
|
||||||
</div>
|
|
||||||
<div className="pull-right">
|
|
||||||
<input type="button" id="btnSignOut" className="btn btn-primary" value="Sign Out" />
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</nav>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<aside className="main-sidebar">
|
|
||||||
<section className="sidebar">
|
|
||||||
<div className="user-panel">
|
|
||||||
<div className="pull-left image">
|
|
||||||
<img id="imgLeftProfile" src="/content/img/generic_avatar.jpg" className="img-circle" alt="User Image" />
|
|
||||||
</div>
|
|
||||||
<div className="pull-left info" style={{ lineHeight: 3 }}>
|
|
||||||
<p><span id="spanMenuName"></span></p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<ul className="sidebar-menu">
|
|
||||||
<li className="header">MAIN NAVIGATION</li>
|
|
||||||
<li id="liDashboard" className="active treeview">
|
|
||||||
<a href="Dashboard">
|
|
||||||
<i className="fa fa-home"></i><span>Dashboard</span>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li className="treeview">
|
|
||||||
<a href="#" id="aSignOut">
|
|
||||||
<i className="fa fa-times-circle"></i>
|
|
||||||
<span>Logout</span>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</section>
|
|
||||||
</aside>
|
|
||||||
|
|
||||||
{children}
|
|
||||||
|
|
||||||
<footer className="main-footer" style={{ padding: '2px' }}>
|
|
||||||
<div className="pull-right hidden-xs">
|
|
||||||
<b>Version</b> 1.0.0
|
|
||||||
</div>
|
|
||||||
<strong>Copyright © 2024 <a href="https://www.surge365.com">Surge365</a>.</strong> All rights reserved.
|
|
||||||
</footer>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Layout.propTypes = {
|
const Layout = ({ children }: LayoutProps) => {
|
||||||
children: PropTypes.any
|
const [open, setOpen] = React.useState(false);
|
||||||
|
const { mode, setMode } = useColorScheme(); // MUI v6 hook for theme switching
|
||||||
|
const iconButtonRef = React.useRef<HTMLButtonElement>(null)
|
||||||
|
|
||||||
|
const handleDrawerOpen = () => {
|
||||||
|
setOpen(true);
|
||||||
|
sendResize();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDrawerClose = () => {
|
||||||
|
setOpen(false);
|
||||||
|
sendResize();
|
||||||
|
};
|
||||||
|
|
||||||
|
const sendResize = () => {
|
||||||
|
//// Force window resize event after drawer state changes
|
||||||
|
//setTimeout(() => {
|
||||||
|
// window.dispatchEvent(new Event("resize"));
|
||||||
|
//}); // Delay slightly to ensure UI updates
|
||||||
|
};
|
||||||
|
const handleThemeChange = (event: SelectChangeEvent) => {
|
||||||
|
setMode(event.target.value as 'light' | 'dark');
|
||||||
|
if (iconButtonRef.current) {
|
||||||
|
const selectElement = iconButtonRef.current;
|
||||||
|
if (selectElement) {
|
||||||
|
if (selectElement instanceof HTMLElement) {
|
||||||
|
setTimeout(() => {
|
||||||
|
selectElement.focus(); // Blur the focusable input
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box sx={{ display: 'flex' }}>
|
||||||
|
{/* App Bar */}
|
||||||
|
<AppBar
|
||||||
|
position="fixed"
|
||||||
|
sx={(theme) => ({
|
||||||
|
zIndex: theme.zIndex.drawer + 1,
|
||||||
|
transition: theme.transitions.create(['width', 'margin','padding'], {
|
||||||
|
easing: theme.transitions.easing.easeInOut,
|
||||||
|
duration: theme.transitions.duration.leavingScreen,
|
||||||
|
}),
|
||||||
|
...(open && {
|
||||||
|
width: `calc(100% - ${drawerWidth}px)`,
|
||||||
|
ml: `${drawerWidth}px`,
|
||||||
|
transition: theme.transitions.create(['width', 'margin', 'padding'], {
|
||||||
|
easing: theme.transitions.easing.easeInOut,
|
||||||
|
duration: theme.transitions.duration.enteringScreen,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<Toolbar>
|
||||||
|
<IconButton
|
||||||
|
color="inherit"
|
||||||
|
aria-label="open drawer"
|
||||||
|
onClick={handleDrawerOpen}
|
||||||
|
edge="start"
|
||||||
|
ref={iconButtonRef}
|
||||||
|
sx={{ mr: 2, ...(open && { display: 'none' }) }}
|
||||||
|
>
|
||||||
|
<MenuIcon />
|
||||||
|
</IconButton>
|
||||||
|
<Typography variant="h6" noWrap component="div" sx={{ flexGrow: 1 }}>
|
||||||
|
Surge365 Dashboard
|
||||||
|
</Typography>
|
||||||
|
<FormControl sx={{ minWidth: 120 }} size="small">
|
||||||
|
<InputLabel
|
||||||
|
id="theme-select-label"
|
||||||
|
sx={{
|
||||||
|
color: 'white', // White in both modes
|
||||||
|
'&.Mui-focused': { color: 'white' }, // Keep white when focused
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Theme
|
||||||
|
</InputLabel>
|
||||||
|
<Select
|
||||||
|
labelId="theme-select-label"
|
||||||
|
id="theme-select"
|
||||||
|
value={mode || 'light'}
|
||||||
|
label="Theme"
|
||||||
|
onChange={handleThemeChange}
|
||||||
|
sx={{
|
||||||
|
color: 'white', // White text
|
||||||
|
'& .MuiSvgIcon-root': { color: 'white' }, // White dropdown arrow
|
||||||
|
'& .MuiOutlinedInput-notchedOutline': {
|
||||||
|
borderColor: 'white', // Gray in light, white in dark
|
||||||
|
},
|
||||||
|
'&:hover .MuiOutlinedInput-notchedOutline': {
|
||||||
|
borderColor: 'white', // Darker gray on hover in light
|
||||||
|
borderWidth: 2
|
||||||
|
},
|
||||||
|
'&.Mui-focused .MuiOutlinedInput-notchedOutline': {
|
||||||
|
borderColor: 'white', // Even darker gray when focused in light
|
||||||
|
borderWidth: 2
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<MenuItem value="light">Light</MenuItem>
|
||||||
|
<MenuItem value="dark">Dark</MenuItem>
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
</Toolbar>
|
||||||
|
</AppBar>
|
||||||
|
|
||||||
|
{/* Sidebar */}
|
||||||
|
<Drawer
|
||||||
|
variant="persistent"
|
||||||
|
anchor="left"
|
||||||
|
open={open}
|
||||||
|
sx={{
|
||||||
|
width: open ? `var(--mui-drawer-width, ${drawerWidth}px)` : 0,
|
||||||
|
flexShrink: 0,
|
||||||
|
'& .MuiDrawer-paper': {
|
||||||
|
width: open ? `var(--mui-drawer-width, ${drawerWidth}px)` : 0,
|
||||||
|
boxSizing: 'border-box',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DrawerHeader>
|
||||||
|
<IconButton onClick={handleDrawerClose}>
|
||||||
|
<ChevronLeftIcon />
|
||||||
|
</IconButton>
|
||||||
|
</DrawerHeader>
|
||||||
|
<Divider />
|
||||||
|
<List>
|
||||||
|
{[
|
||||||
|
{ text: 'Home', icon: <DashboardIcon />, path: '/home' },
|
||||||
|
{ text: 'Targets', icon: <DirectionsCarIcon />, path: '/targets' },
|
||||||
|
{ text: 'Templates', icon: <PeopleIcon />, path: '/templates' },
|
||||||
|
].map((item) => (
|
||||||
|
<ListItem key={item.text} disablePadding>
|
||||||
|
<ListItemButton component={RouterLink} to={item.path}>
|
||||||
|
<ListItemIcon>{item.icon}</ListItemIcon>
|
||||||
|
<ListItemText primary={item.text} />
|
||||||
|
</ListItemButton>
|
||||||
|
</ListItem>
|
||||||
|
))}
|
||||||
|
</List>
|
||||||
|
</Drawer>
|
||||||
|
|
||||||
|
{/* Main Content */}
|
||||||
|
<Main open={open}>
|
||||||
|
<DrawerHeader />
|
||||||
|
{children}
|
||||||
|
</Main>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Layout;
|
export default Layout;
|
||||||
@ -0,0 +1,101 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import AdminLTELogo from 'admin-lte/dist/assets/img/AdminLTELogo.png';
|
||||||
|
interface LayoutProps {
|
||||||
|
children: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Layout: React.FC<LayoutProps> = ({ children }) => {
|
||||||
|
return (
|
||||||
|
<div className="app-wrapper">
|
||||||
|
{/* Header */}
|
||||||
|
<nav className="app-header navbar navbar-expand bg-body">
|
||||||
|
<div className="container-fluid">
|
||||||
|
{/* Start Navbar Links */}
|
||||||
|
<ul className="navbar-nav">
|
||||||
|
<li className="nav-item">
|
||||||
|
<a className="nav-link" data-lte-toggle="sidebar" href="#" role="button">
|
||||||
|
<i className="bi bi-list"></i>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li className="nav-item d-none d-md-block">
|
||||||
|
<a href="./home" className="nav-link">Home</a>
|
||||||
|
</li>
|
||||||
|
<li className="nav-item d-none d-md-block">
|
||||||
|
<a href="#" className="nav-link">TODO</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
{/* End Navbar Links */}
|
||||||
|
<ul className="navbar-nav ms-auto">
|
||||||
|
<li className="nav-item">
|
||||||
|
<a className="nav-link" data-widget="navbar-search" href="#" role="button">
|
||||||
|
<i className="bi bi-search"></i>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{/* Additional navbar items (messages, notifications, user menu) can go here */}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
{/* Sidebar */}
|
||||||
|
<aside className="app-sidebar bg-body-secondary shadow" data-bs-theme="dark">
|
||||||
|
<div className="sidebar-brand">
|
||||||
|
<a href="./home" className="brand-link">
|
||||||
|
<img
|
||||||
|
src={AdminLTELogo}
|
||||||
|
alt="AdminLTE Logo"
|
||||||
|
className="brand-image opacity-75 shadow"
|
||||||
|
/>
|
||||||
|
<span className="brand-text fw-light">AdminLTE 4</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div className="sidebar-wrapper">
|
||||||
|
<nav className="mt-2">
|
||||||
|
<ul
|
||||||
|
className="nav sidebar-menu flex-column"
|
||||||
|
data-lte-toggle="treeview"
|
||||||
|
role="menu"
|
||||||
|
data-accordion="false"
|
||||||
|
>
|
||||||
|
<li className="nav-item menu-open">
|
||||||
|
<a href="./targets" className="nav-link active">
|
||||||
|
<i className="nav-icon bi bi-speedometer"></i>
|
||||||
|
<p>
|
||||||
|
Targets
|
||||||
|
<i className="nav-arrow bi bi-chevron-right"></i>
|
||||||
|
</p>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li className="nav-item menu-open">
|
||||||
|
<a href="./templates" className="nav-link active">
|
||||||
|
<i className="nav-icon bi bi-speedometer"></i>
|
||||||
|
<p>
|
||||||
|
Templates
|
||||||
|
<i className="nav-arrow bi bi-chevron-right"></i>
|
||||||
|
</p>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{/* Additional sidebar menu items can be added here */}
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
{/* Main Content Area */}
|
||||||
|
<main className="app-main">
|
||||||
|
{children}
|
||||||
|
</main>
|
||||||
|
|
||||||
|
{/* Footer */}
|
||||||
|
<footer className="app-footer">
|
||||||
|
<div className="float-end d-none d-sm-inline">Version 0.0.1</div>
|
||||||
|
<strong>
|
||||||
|
Copyright © 2025
|
||||||
|
<a href="https://adminlte.io" className="text-decoration-none">Surge365</a>.
|
||||||
|
</strong>
|
||||||
|
All rights reserved.
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Layout;
|
||||||
120
Surge365.MassEmailReact.Web/src/components/layouts/LayoutOld.tsx
Normal file
120
Surge365.MassEmailReact.Web/src/components/layouts/LayoutOld.tsx
Normal file
@ -0,0 +1,120 @@
|
|||||||
|
import { ReactNode } from 'react';
|
||||||
|
//import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
import 'bootstrap/dist/css/bootstrap.min.css';
|
||||||
|
import 'admin-lte/dist/css/adminlte.min.css';
|
||||||
|
import 'font-awesome/css/font-awesome.min.css';
|
||||||
|
/*import 'ionicons/dist/css/ionicons.min.css';*/
|
||||||
|
|
||||||
|
import 'bootstrap/dist/js/bootstrap.bundle.min.js'; // Bootstrap JS
|
||||||
|
import 'admin-lte/dist/js/adminlte.min.js';
|
||||||
|
|
||||||
|
const Layout = function Layout({ children }: { children: ReactNode }) {
|
||||||
|
return (
|
||||||
|
<div className="wrapper" style={{ overflow: 'initial' }}>
|
||||||
|
<link rel="stylesheet" href="/content/dist/css/skins/_all-skins.min.css" />
|
||||||
|
<link href="/content/plugins/datepicker/v4/bootstrap-datetimepicker.css" rel="stylesheet" />
|
||||||
|
<link rel="stylesheet" href="/content/plugins/bootstrap-wysihtml5/bootstrap3-wysihtml5.min.css" />
|
||||||
|
<link rel="stylesheet" href="/content/plugins/datatables/dataTables.bootstrap.css" />
|
||||||
|
|
||||||
|
{/*<script src="/content/dist/js/app.min.js"></script>AdminLTE?*/}
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.11.2/moment.min.js"></script>
|
||||||
|
{/*<script src="/content/plugins/datatables/jquery.dataTables.min.js"></script>*/}
|
||||||
|
{/*<script src="/content/plugins/validator/validator.js"></script>*/}
|
||||||
|
<script src="/content/js/jquery.surge365.utilities-1.0.js" />
|
||||||
|
<script src="/content/js/jquery.surge365.global.js?36"></script>
|
||||||
|
<script src="/content/js/jquery.surge365.webmethods-1.0.js?15"></script>
|
||||||
|
<script src="/content/js/Main-1.0.js?39"></script>
|
||||||
|
{/*<script src="/content/plugins/growl/jquery.bootstrap-growl.min.js"></script>*/}
|
||||||
|
{/*<script src="/content/plugins/input-mask-5/jquery.inputmask.js"></script>*/}
|
||||||
|
{/*<script src="/content/plugins/printThis/printThis.js"></script>*/}
|
||||||
|
{/*<script src="/content/plugins/datatables/dataTables.bootstrap.min.js"></script>*/}
|
||||||
|
<header className="main-header">
|
||||||
|
<a href="Dashboard" className="logo" style={{ backgroundColor: '#333' }}>
|
||||||
|
<span className="logo-mini" style={{ backgroundColor: '#333' }}>
|
||||||
|
<img id="imgLogo-sm" src="/content/img/imove_mini_logo.png" style={{ height: '35px', marginBottom: '3px' }} alt="Logo" className="hide" />
|
||||||
|
</span>
|
||||||
|
<span className="logo-lg" style={{ textAlign: 'center', backgroundColor: '#333' }}>
|
||||||
|
<img id="imgLogo-lg" src="/content/img/imove_mini_logo.png" style={{ height: '35px', marginBottom: '3px' }} alt="Logo" className="hide" />
|
||||||
|
<span style={{ marginLeft: '5px', verticalAlign: 'middle' }}><b>USA</b>Haulers</span>
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
<nav className="navbar navbar-static-top">
|
||||||
|
<a href="#" className="fa5 sidebar-toggle" data-toggle="offcanvas" role="button">
|
||||||
|
<span className="sr-only">Toggle navigation</span>
|
||||||
|
</a>
|
||||||
|
<div className="navbar-custom-menu">
|
||||||
|
<ul className="nav navbar-nav">
|
||||||
|
<li className="dropdown user user-menu">
|
||||||
|
<a href="#" className="dropdown-toggle" data-toggle="dropdown">
|
||||||
|
<span id="spanSpinner" className="fa fa-sync-alt pull-left" style={{ lineHeight: 'unset', display: 'none', fontSize: '16pt', fontWeight: 'bold', marginRight: '10px' }}></span>
|
||||||
|
<img id="imgRightProfile" src="/content/img/generic_avatar.jpg" className="user-image" alt="User Image" />
|
||||||
|
<span id="spanProfileName"></span>
|
||||||
|
</a>
|
||||||
|
<ul className="dropdown-menu">
|
||||||
|
<li className="user-header">
|
||||||
|
<img id="imgMainProfile" src="" className="img-circle" alt="User Image" />
|
||||||
|
<p>
|
||||||
|
<span id="spanProfileNameTitle"></span>
|
||||||
|
</p>
|
||||||
|
</li>
|
||||||
|
<li className="user-footer">
|
||||||
|
<div className="pull-left">
|
||||||
|
<a href="Profile" className="btn btn-default btn-flat">Profile</a>
|
||||||
|
</div>
|
||||||
|
<div className="pull-right">
|
||||||
|
<input type="button" id="btnSignOut" className="btn btn-primary" value="Sign Out" />
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<aside className="main-sidebar">
|
||||||
|
<section className="sidebar">
|
||||||
|
<div className="user-panel">
|
||||||
|
<div className="pull-left image">
|
||||||
|
<img id="imgLeftProfile" src="/content/img/generic_avatar.jpg" className="img-circle" alt="User Image" />
|
||||||
|
</div>
|
||||||
|
<div className="pull-left info" style={{ lineHeight: 3 }}>
|
||||||
|
<p><span id="spanMenuName"></span></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ul className="sidebar-menu">
|
||||||
|
<li className="header">MAIN NAVIGATION</li>
|
||||||
|
<li id="liDashboard" className="active treeview">
|
||||||
|
<a href="Dashboard">
|
||||||
|
<i className="fa fa-home"></i><span>Dashboard</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li className="treeview">
|
||||||
|
<a href="#" id="aSignOut">
|
||||||
|
<i className="fa fa-times-circle"></i>
|
||||||
|
<span>Logout</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
{children}
|
||||||
|
|
||||||
|
<footer className="main-footer" style={{ padding: '2px' }}>
|
||||||
|
<div className="pull-right hidden-xs">
|
||||||
|
<b>Version</b> 1.0.0
|
||||||
|
</div>
|
||||||
|
<strong>Copyright © 2024 <a href="https://www.surge365.com">Surge365</a>.</strong> All rights reserved.
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Layout.propTypes = {
|
||||||
|
children: PropTypes.any
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Layout;
|
||||||
127
Surge365.MassEmailReact.Web/src/components/modals/TargetEdit.tsx
Normal file
127
Surge365.MassEmailReact.Web/src/components/modals/TargetEdit.tsx
Normal file
@ -0,0 +1,127 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogTitle,
|
||||||
|
DialogContent,
|
||||||
|
TextField,
|
||||||
|
DialogActions,
|
||||||
|
Button,
|
||||||
|
Switch,
|
||||||
|
FormControlLabel,
|
||||||
|
} from "@mui/material";
|
||||||
|
import { Target } from "@/types/target";
|
||||||
|
|
||||||
|
type TargetEditProps = {
|
||||||
|
open: boolean;
|
||||||
|
target: Target;
|
||||||
|
onClose: () => void;
|
||||||
|
onSave: (updatedTarget: Target) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const TargetEdit = ({ open, target, onClose, onSave }: TargetEditProps) => {
|
||||||
|
const [formData, setFormData] = useState<Target>({ ...target });
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
const handleChange = (field: keyof Target, value: string | boolean) => {
|
||||||
|
setFormData((prev) => ({ ...prev, [field]: value }));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSave = async () => {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/targets/${formData.id}`, {
|
||||||
|
method: "PUT",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify(formData),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) throw new Error("Failed to update");
|
||||||
|
|
||||||
|
const updatedTarget = await response.json();
|
||||||
|
onSave(updatedTarget); // Update UI optimistically
|
||||||
|
onClose();
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Update error:", error);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onClose={onClose} maxWidth="sm" fullWidth>
|
||||||
|
<DialogTitle>Edit Target</DialogTitle>
|
||||||
|
<DialogContent>
|
||||||
|
<TextField
|
||||||
|
label="Target Key"
|
||||||
|
fullWidth
|
||||||
|
value={formData.id}
|
||||||
|
onChange={(e) => handleChange("id", e.target.value)}
|
||||||
|
margin="dense"
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
label="Server Key"
|
||||||
|
fullWidth
|
||||||
|
value={formData.serverId}
|
||||||
|
onChange={(e) => handleChange("serverId", e.target.value)}
|
||||||
|
margin="dense"
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
label="Name"
|
||||||
|
fullWidth
|
||||||
|
value={formData.name}
|
||||||
|
onChange={(e) => handleChange("name", e.target.value)}
|
||||||
|
margin="dense"
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
label="Database Name"
|
||||||
|
fullWidth
|
||||||
|
value={formData.databaseName}
|
||||||
|
onChange={(e) => handleChange("databaseName", e.target.value)}
|
||||||
|
margin="dense"
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
label="View Name"
|
||||||
|
fullWidth
|
||||||
|
value={formData.viewName}
|
||||||
|
onChange={(e) => handleChange("viewName", e.target.value)}
|
||||||
|
margin="dense"
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
label="Filter Query"
|
||||||
|
fullWidth
|
||||||
|
value={formData.filterQuery}
|
||||||
|
onChange={(e) => handleChange("filterQuery", e.target.value)}
|
||||||
|
margin="dense"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormControlLabel
|
||||||
|
control={
|
||||||
|
<Switch
|
||||||
|
checked={formData.allowWriteBack}
|
||||||
|
onChange={(e) => handleChange("allowWriteBack", e.target.checked)}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label="Allow Write Back"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormControlLabel
|
||||||
|
control={
|
||||||
|
<Switch
|
||||||
|
checked={formData.isActive}
|
||||||
|
onChange={(e) => handleChange("isActive", e.target.checked)}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label="Active"
|
||||||
|
/>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<Button onClick={onClose} disabled={loading}>Cancel</Button>
|
||||||
|
<Button onClick={handleSave} color="primary" disabled={loading}>
|
||||||
|
{loading ? "Saving..." : "Save"}
|
||||||
|
</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TargetEdit;
|
||||||
@ -1,34 +1,77 @@
|
|||||||
// App.tsx or main routing component
|
// App.tsx or main routing component
|
||||||
//import React from 'react';
|
import React from 'react';
|
||||||
import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom';
|
import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom';
|
||||||
import Layout from '@/components/layouts/Layout';
|
import Layout from '@/components/layouts/Layout';
|
||||||
import LayoutLogin from '@/components/layouts/LayoutLogin';
|
import LayoutLogin from '@/components/layouts/LayoutLogin';
|
||||||
import Vehicles from '@/components/pages/Vehicles';
|
import Home from '@/components/pages/Home';
|
||||||
import Login from '@/components/pages/Login';
|
import Login from '@/components/pages/Login';
|
||||||
|
import Targets from '@/components/pages/Targets';
|
||||||
|
import Templates from '@/components/pages/Templates';
|
||||||
|
import { ColorModeContext } from '@/theme/theme';
|
||||||
|
import { SetupDataProvider } from '@/context/SetupDataContext';
|
||||||
|
|
||||||
|
import { createTheme, ThemeProvider } from '@mui/material/styles';
|
||||||
|
import CssBaseline from '@mui/material/CssBaseline';
|
||||||
|
|
||||||
|
|
||||||
const App = () => {
|
const App = () => {
|
||||||
|
const [mode, setMode] = React.useState<'light' | 'dark'>('light');
|
||||||
|
|
||||||
|
const colorMode = React.useMemo(
|
||||||
|
() => ({
|
||||||
|
toggleColorMode: () => {
|
||||||
|
setMode((prevMode) => (prevMode === 'light' ? 'dark' : 'light'));
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
const theme = React.useMemo(() => createTheme({ palette: { mode } }), [mode]);
|
||||||
return (
|
return (
|
||||||
<Router basename="/">
|
<ColorModeContext.Provider value={colorMode}>
|
||||||
<Routes>
|
<ThemeProvider theme={theme}>
|
||||||
<Route path="/" element={<Navigate to="/login" replace />} />
|
<CssBaseline />
|
||||||
<Route
|
<SetupDataProvider>
|
||||||
path="/vehicles"
|
<Router basename="/">
|
||||||
element={
|
<Routes>
|
||||||
<Layout>
|
<Route path="/" element={<Navigate to="/login" replace />} />
|
||||||
<Vehicles />
|
<Route
|
||||||
</Layout>
|
path="/home"
|
||||||
}
|
element={
|
||||||
/>
|
<Layout>
|
||||||
<Route
|
<Home />
|
||||||
path="/login"
|
</Layout>
|
||||||
element={
|
}
|
||||||
<LayoutLogin>
|
/>
|
||||||
<Login />
|
<Route
|
||||||
</LayoutLogin>
|
path="/targets"
|
||||||
}
|
element={
|
||||||
/>
|
<Layout>
|
||||||
</Routes>
|
<Targets />
|
||||||
</Router>
|
</Layout>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path="/templates"
|
||||||
|
element={
|
||||||
|
<Layout>
|
||||||
|
<Templates />
|
||||||
|
</Layout>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path="/login"
|
||||||
|
element={
|
||||||
|
<LayoutLogin>
|
||||||
|
<Login />
|
||||||
|
</LayoutLogin>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Routes>
|
||||||
|
</Router>
|
||||||
|
</SetupDataProvider>
|
||||||
|
</ThemeProvider>
|
||||||
|
</ColorModeContext.Provider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
79
Surge365.MassEmailReact.Web/src/components/pages/AppMain.tsx
Normal file
79
Surge365.MassEmailReact.Web/src/components/pages/AppMain.tsx
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { createRoot } from 'react-dom/client'
|
||||||
|
import { createTheme, ThemeProvider } from '@mui/material/styles';
|
||||||
|
import '@/css/main.css'
|
||||||
|
import App from '@/components/pages/App'
|
||||||
|
import '@/config/constants';
|
||||||
|
|
||||||
|
|
||||||
|
//DEFAULT THEMES
|
||||||
|
const theme = createTheme({
|
||||||
|
cssVariables: {
|
||||||
|
colorSchemeSelector: 'class'
|
||||||
|
},
|
||||||
|
colorSchemes: {
|
||||||
|
light: true, // Default light scheme
|
||||||
|
dark: true, // Default dark scheme
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
MuiAppBar: {
|
||||||
|
styleOverrides: {
|
||||||
|
root: ({ theme }) => ({
|
||||||
|
backgroundColor: theme.vars.palette.primary.main,
|
||||||
|
color: theme.vars.palette.text.primary,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
MuiOutlinedInput: {
|
||||||
|
styleOverrides: {
|
||||||
|
notchedOutline: ({ theme }) => ({
|
||||||
|
borderColor: theme.palette.mode === 'light' ? theme.vars.palette.grey[500] : theme.vars.palette.text.primary,
|
||||||
|
'&:hover': {
|
||||||
|
borderColor: theme.palette.mode === 'light' ? theme.vars.palette.grey[700] : theme.vars.palette.text.primary,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
root: ({ theme }) => ({
|
||||||
|
'&.Mui-focused .MuiOutlinedInput-notchedOutline': {
|
||||||
|
borderColor: theme.palette.mode === 'light' ? theme.vars.palette.grey[900] : theme.vars.palette.text.primary,
|
||||||
|
borderWidth: 2, // Match MUI default focus width
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
//CUSTOM THEMES
|
||||||
|
//const theme = createTheme({
|
||||||
|
// cssVariables: {
|
||||||
|
// colorSchemeSelector: 'class'
|
||||||
|
// },
|
||||||
|
// colorSchemes: {
|
||||||
|
// light: {
|
||||||
|
// palette: {
|
||||||
|
// primary: { main: '#1976d2' },
|
||||||
|
// background: { default: '#fff', paper: '#f5f5f5' },
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
// dark: {
|
||||||
|
// palette: {
|
||||||
|
// primary: { main: '#90caf9' },
|
||||||
|
// background: { default: '#121212', paper: '#424242' },
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
//});
|
||||||
|
|
||||||
|
const rootElement = document.getElementById('root');
|
||||||
|
if (rootElement) {
|
||||||
|
createRoot(rootElement).render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<ThemeProvider theme={theme} defaultMode="system">
|
||||||
|
<App />
|
||||||
|
</ThemeProvider>
|
||||||
|
</React.StrictMode>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
throw new Error('Root element not found');
|
||||||
|
}
|
||||||
36
Surge365.MassEmailReact.Web/src/components/pages/Home.tsx
Normal file
36
Surge365.MassEmailReact.Web/src/components/pages/Home.tsx
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
// 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';
|
||||||
|
|
||||||
|
const Home = () => {
|
||||||
|
return (
|
||||||
|
<Box sx={{ flexGrow: 1 }}>
|
||||||
|
<Typography variant="h4" component="h1" gutterBottom>
|
||||||
|
Welcome to Surge365
|
||||||
|
</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>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Home;
|
||||||
@ -1,5 +1,6 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { Button, Form, Spinner } from 'react-bootstrap';
|
import { Button, Form, Spinner } from 'react-bootstrap';
|
||||||
|
import { AuthResponse, AuthErrorResponse, User, isAuthErrorResponse } from '@/types/auth';
|
||||||
//import { Helmet, HelmetProvider } from 'react-helmet-async';
|
//import { Helmet, HelmetProvider } from 'react-helmet-async';
|
||||||
|
|
||||||
import utils from '@/ts/utils.ts';
|
import utils from '@/ts/utils.ts';
|
||||||
@ -15,8 +16,9 @@ function Login() {
|
|||||||
const [username, setUsername] = useState('');
|
const [username, setUsername] = useState('');
|
||||||
const [password, setPassword] = useState('');
|
const [password, setPassword] = useState('');
|
||||||
const [showForgotPasswordModal, setShowForgotPasswordModal] = useState(false);
|
const [showForgotPasswordModal, setShowForgotPasswordModal] = useState(false);
|
||||||
const [user, setUser] = useState<any>(null);
|
//const [user, setUser] = useState<User | null>(null);
|
||||||
const [loginError, setLoginError] = useState<boolean>(false);
|
const [loginError, setLoginError] = useState<boolean>(false);
|
||||||
|
const [loginErrorMessage, setLoginErrorMessage] = useState<string>('');
|
||||||
|
|
||||||
//const setSpinners = (newValues: Partial<SpinnerState>) => {
|
//const setSpinners = (newValues: Partial<SpinnerState>) => {
|
||||||
// setSpinnersState((prevSpinners) => ({
|
// setSpinnersState((prevSpinners) => ({
|
||||||
@ -68,26 +70,50 @@ function Login() {
|
|||||||
|
|
||||||
if (Object.keys(formErrors).length > 0) return;
|
if (Object.keys(formErrors).length > 0) return;
|
||||||
|
|
||||||
|
//setUser(null);
|
||||||
setLoginError(false);
|
setLoginError(false);
|
||||||
let loggedInUser: any = null;
|
setLoginErrorMessage('');
|
||||||
|
let loggedInUser: User | null = null;
|
||||||
await utils.webMethod({
|
let hadLoginError: boolean = false;
|
||||||
|
let hadLoginErrorMessage: string = '';
|
||||||
|
await utils.webMethod<AuthResponse>({
|
||||||
methodPage: 'authentication',
|
methodPage: 'authentication',
|
||||||
methodName: 'authenticate',
|
methodName: 'authenticate',
|
||||||
parameters: { username: username, password: password },
|
parameters: { username, password },
|
||||||
success: (json: any) => {
|
success: (json: AuthResponse) => {
|
||||||
if (utils.getBoolean(json.success)) {
|
try {
|
||||||
loggedInUser = json.data;
|
loggedInUser = json.user;
|
||||||
setUser(loggedInUser);
|
//setUser(loggedInUser);
|
||||||
} else {
|
|
||||||
setLoginError(true);
|
|
||||||
setIsLoading(false);
|
|
||||||
spinners.Login = false;
|
|
||||||
}
|
}
|
||||||
|
catch {
|
||||||
|
const errorMsg: string = "Unexpected Error";
|
||||||
|
hadLoginError = true;
|
||||||
|
hadLoginErrorMessage = errorMsg;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: (err: unknown) => {
|
||||||
|
let errorMsg: string = "Unexpected Error";
|
||||||
|
if (isAuthErrorResponse(err)) {
|
||||||
|
if (err && err as AuthErrorResponse) {
|
||||||
|
if (err.data) {
|
||||||
|
if (err.data.message)
|
||||||
|
errorMsg = err.data.message;
|
||||||
|
}
|
||||||
|
console.error(errorMsg);
|
||||||
|
setLoginErrorMessage(errorMsg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
hadLoginError = true;
|
||||||
|
hadLoginErrorMessage = errorMsg;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (loginError) {
|
if (hadLoginError) {
|
||||||
|
setLoginErrorMessage(hadLoginErrorMessage);
|
||||||
|
setLoginError(true);
|
||||||
|
setIsLoading(false);
|
||||||
|
spinners.Login = false;
|
||||||
|
setSpinners(spinners);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -101,20 +127,20 @@ function Login() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const finishUserLogin = async (user: any) => {
|
const finishUserLogin = async (loggedInUser: User) => {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
spinners.Login = false;
|
spinners.Login = false;
|
||||||
spinners.LoginWithPasskey = false;
|
spinners.LoginWithPasskey = false;
|
||||||
setSpinners(spinners);
|
setSpinners(spinners);
|
||||||
|
|
||||||
utils.localStorage("session_currentUser", user);
|
utils.localStorage("session_currentUser", loggedInUser);
|
||||||
|
|
||||||
const redirectUrl = utils.sessionStorage("redirect_url");
|
const redirectUrl = utils.sessionStorage("redirect_url");
|
||||||
if (redirectUrl) {
|
if (redirectUrl) {
|
||||||
utils.sessionStorage("redirect_url", null);
|
utils.sessionStorage("redirect_url", null);
|
||||||
document.location.href = redirectUrl;
|
document.location.href = redirectUrl;
|
||||||
} else {
|
} else {
|
||||||
document.location.href = '/vehicles';
|
document.location.href = '/home';
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -127,7 +153,7 @@ function Login() {
|
|||||||
<h3 className="form-signin-heading mt-3 mb-1">Please sign in</h3>
|
<h3 className="form-signin-heading mt-3 mb-1">Please sign in</h3>
|
||||||
<Form id="frmLogin" onSubmit={handleLogin}>
|
<Form id="frmLogin" onSubmit={handleLogin}>
|
||||||
{loginError && (
|
{loginError && (
|
||||||
<Form.Label style={{ color: 'red' }}>Login error</Form.Label>
|
<Form.Label style={{ color: 'red' }}>{loginErrorMessage ?? "Login error"}</Form.Label>
|
||||||
)}
|
)}
|
||||||
<Form.Group className="mb-3" controlId="txtUsernamel">
|
<Form.Group className="mb-3" controlId="txtUsernamel">
|
||||||
<Form.Label className="visually-hidden">Username</Form.Label>
|
<Form.Label className="visually-hidden">Username</Form.Label>
|
||||||
|
|||||||
160
Surge365.MassEmailReact.Web/src/components/pages/Targets.tsx
Normal file
160
Surge365.MassEmailReact.Web/src/components/pages/Targets.tsx
Normal file
@ -0,0 +1,160 @@
|
|||||||
|
import { useState, useRef } from 'react';
|
||||||
|
import { useSetupData, SetupData } from "@/context/SetupDataContext";
|
||||||
|
//import Typography from '@mui/material/Typography';
|
||||||
|
import { List, Card, CardContent, Typography, Box, useTheme, useMediaQuery, Button, CircularProgress } from '@mui/material';
|
||||||
|
import { DataGrid, GridColDef, GridRenderCellParams, GridRowModel, GridToolbarContainer, GridToolbarQuickFilter, GridToolbarExport, GridToolbarDensitySelector, GridToolbarColumnsButton } from '@mui/x-data-grid';
|
||||||
|
//import utils from '@/ts/utils';
|
||||||
|
import { Target } from '@/types/target';
|
||||||
|
import TargetEdit from "@/components/modals/TargetEdit";
|
||||||
|
|
||||||
|
|
||||||
|
function Targets() {
|
||||||
|
const theme = useTheme();
|
||||||
|
const setupData: SetupData = useSetupData();
|
||||||
|
const isMobile = useMediaQuery(theme.breakpoints.down("sm"));
|
||||||
|
|
||||||
|
//const [targets, setTargets] = useState<Target[]>([]);
|
||||||
|
const gridContainerRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
const [selectedRow, setSelectedRow] = useState<Target | null>(null);
|
||||||
|
const [open, setOpen] = useState<boolean>(false);
|
||||||
|
|
||||||
|
//TODO: Update columns to target format
|
||||||
|
const columns: GridColDef<Target>[] = [
|
||||||
|
{
|
||||||
|
field: "actions",
|
||||||
|
headerName: "Actions",
|
||||||
|
sortable: false,
|
||||||
|
renderCell: (params: GridRenderCellParams<Target>) => (
|
||||||
|
<Button variant="contained" color="primary" size="small" onClick={() => handleEdit(params.row)}>
|
||||||
|
Edit
|
||||||
|
</Button>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{ field: "id", headerName: "ID", width: 60 },
|
||||||
|
//{ 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 },
|
||||||
|
];
|
||||||
|
|
||||||
|
|
||||||
|
//async function loadTargets(activeOnly: boolean) {
|
||||||
|
// let hadError = false;
|
||||||
|
// let hadErrorMessage = "";
|
||||||
|
// await utils.webMethod<[Target]>({
|
||||||
|
// httpMethod: 'GET',
|
||||||
|
// methodPage: 'targets',
|
||||||
|
// methodName: 'GetAll?activeOnly=' + activeOnly,
|
||||||
|
// success: (targets: [Target]) => {
|
||||||
|
// try {
|
||||||
|
// setTargets(targets);
|
||||||
|
// }
|
||||||
|
// catch {
|
||||||
|
// const errorMsg: string = "Unexpected Error";
|
||||||
|
// hadError = true;
|
||||||
|
// hadErrorMessage = errorMsg;
|
||||||
|
// }
|
||||||
|
// },
|
||||||
|
// error: () => {
|
||||||
|
// const errorMsg: string = "Unexpected Error";
|
||||||
|
// hadError = true;
|
||||||
|
// hadErrorMessage = errorMsg;
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
// if (hadError)
|
||||||
|
// alert(hadErrorMessage); //TODO: Make this look better. MUI Alert popup?
|
||||||
|
//}
|
||||||
|
const handleEdit = (row: GridRowModel<Target>) => {
|
||||||
|
setSelectedRow(row);
|
||||||
|
setOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleUpdateRow = (updatedRow: Target) => {
|
||||||
|
setupData.setTargets(updatedRow);
|
||||||
|
};
|
||||||
|
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 }}>
|
||||||
|
{isMobile ? (
|
||||||
|
<List>
|
||||||
|
{setupData.targets.map((row) => (
|
||||||
|
<Card key={row.id} sx={{ marginBottom: 2 }}>
|
||||||
|
<CardContent>
|
||||||
|
<Typography variant="h6">{row.name}</Typography>
|
||||||
|
<Typography variant="body2">Target Key: {row.id}</Typography>
|
||||||
|
{/*<Typography variant="body2">Server Key: {row.serverKey}</Typography>*/}
|
||||||
|
<Typography variant="body2">Database: {row.databaseName}</Typography>
|
||||||
|
<Typography variant="body2">View: {row.viewName}</Typography>
|
||||||
|
<Typography variant="body2">Filter: {row.filterQuery}</Typography>
|
||||||
|
<Typography variant="body2">Writeback: {row.allowWriteBack ? "Yes" : "No"}</Typography>
|
||||||
|
<Typography variant="body2">Active: {row.isActive ? "Yes" : "No"}</Typography>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</List>
|
||||||
|
) : (
|
||||||
|
<DataGrid
|
||||||
|
rows={setupData.targets}
|
||||||
|
columns={columns}
|
||||||
|
autoPageSize
|
||||||
|
sx={{ minWidth: "600px" }}
|
||||||
|
//getRowId={(row) => row.id!}//Set this if object doesn't have id as unique id (like targetKey etc)
|
||||||
|
slots={{
|
||||||
|
toolbar: () => (
|
||||||
|
<GridToolbarContainer sx={{ display: "flex", alignItems: "center" }}>
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
color="primary"
|
||||||
|
onClick={() => setupData.reloadTargets()} // Refresh only active targets
|
||||||
|
sx={{ marginRight: 2 }}
|
||||||
|
>
|
||||||
|
{setupData.targetsLoading ? <CircularProgress size={24} color="inherit" /> : "Refresh"}
|
||||||
|
</Button>
|
||||||
|
<GridToolbarColumnsButton />
|
||||||
|
<GridToolbarDensitySelector />
|
||||||
|
<GridToolbarExport />
|
||||||
|
<GridToolbarQuickFilter sx={{ ml: "auto" }} /> {/* Keeps the search filter in the toolbar */}
|
||||||
|
</GridToolbarContainer>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
slotProps={{
|
||||||
|
toolbar: {
|
||||||
|
showQuickFilter: true,
|
||||||
|
quickFilterProps: { /*debounceMs: 500/*Optional: Adds debounce to search*/ },
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
initialState={{
|
||||||
|
pagination: {
|
||||||
|
paginationModel: {
|
||||||
|
pageSize: 20,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
pageSizeOptions={[10, 20, 50, 100]}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Target Edit Modal */}
|
||||||
|
{selectedRow && (
|
||||||
|
<TargetEdit
|
||||||
|
open={open}
|
||||||
|
target={selectedRow}
|
||||||
|
onClose={() => setOpen(false)}
|
||||||
|
onSave={handleUpdateRow}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Targets;
|
||||||
@ -0,0 +1,63 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
const Templates: React.FC = () => {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{/* Content Header */}
|
||||||
|
<div className="app-content-header">
|
||||||
|
<div className="container-fluid">
|
||||||
|
<div className="row">
|
||||||
|
<div className="col-sm-6">
|
||||||
|
<h3 className="mb-0">Templates</h3>
|
||||||
|
</div>
|
||||||
|
<div className="col-sm-6">
|
||||||
|
<ol className="breadcrumb float-sm-end">
|
||||||
|
<li className="breadcrumb-item">
|
||||||
|
<a href="./home">Home</a>
|
||||||
|
</li>
|
||||||
|
<li className="breadcrumb-item active" aria-current="page">
|
||||||
|
Templates
|
||||||
|
</li>
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Page-specific Content */}
|
||||||
|
<div className="app-content">
|
||||||
|
<div className="container-fluid">
|
||||||
|
<div className="row">
|
||||||
|
{/* Example: Small Box Widget 1 */}
|
||||||
|
<div className="col-lg-3 col-6">
|
||||||
|
<div className="small-box text-bg-primary">
|
||||||
|
<div className="inner">
|
||||||
|
<h3>150</h3>
|
||||||
|
<p>Templates</p>
|
||||||
|
</div>
|
||||||
|
<svg
|
||||||
|
className="small-box-icon"
|
||||||
|
fill="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path d="M2.25 2.25a.75.75 0 000 1.5h1.386c.17 0 .318.114.362.278l2.558 9.592a3.752 3.752 0 00-2.806 3.63c0 .414.336.75.75.75h15.75a.75.75 0 000-1.5H5.378A2.25 2.25 0 017.5 15h11.218a.75.75 0 00.674-.421 60.358 60.358 0 002.96-7.228.75.75 0 00-.525-.965A60.864 60.864 0 005.68 4.509l-.232-.867A1.875 1.875 0 003.636 2.25H2.25zM3.75 20.25a1.5 1.5 0 113 0 1.5 1.5 0 01-3 0zM16.5 20.25a1.5 1.5 0 113 0 1.5 1.5 0 01-3 0z"></path>
|
||||||
|
</svg>
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
className="small-box-footer link-light link-underline-opacity-0 link-underline-opacity-50-hover"
|
||||||
|
>
|
||||||
|
More info <i className="bi bi-link-45deg"></i>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/* Additional small boxes, charts, or other widgets can be included here */}
|
||||||
|
</div>
|
||||||
|
{/* You can also add more rows for charts, direct chat, etc. */}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Templates;
|
||||||
@ -1,370 +0,0 @@
|
|||||||
//import { useState } from 'react';
|
|
||||||
|
|
||||||
function Vehicles() {
|
|
||||||
return (<div>
|
|
||||||
<link href="/content/plugins/filer/jquery.fileuploader.css" rel="stylesheet" />
|
|
||||||
<link href="/content/plugins/filer/drag-drop/css/jquery.fileuploader-theme-dragdrop.css" rel="stylesheet" />
|
|
||||||
<script src="/content/plugins/filer/jquery.fileuploader.js"></script>
|
|
||||||
|
|
||||||
<script src="/content/js/Vehicles-1.0.js"></script>
|
|
||||||
<div className="content-wrapper">
|
|
||||||
|
|
||||||
{/* Content Header (Page header) */}
|
|
||||||
<section className="content-header">
|
|
||||||
|
|
||||||
<h1>Vehicles
|
|
||||||
<small></small>
|
|
||||||
<button type="button" id="btnNewVehicle" className="btn btn-sm btn-success">New Vehicle</button>
|
|
||||||
<span id="spanLoadPatientsSpinner" className="fa fa-sync-alt" style={{fontSize: '20px', verticalAlign: 'middle', display: 'none' }}></span>
|
|
||||||
</h1>
|
|
||||||
|
|
||||||
</section>
|
|
||||||
<section className="content hide">
|
|
||||||
|
|
||||||
<div className="row">
|
|
||||||
<div className="col-md-6">
|
|
||||||
<div className="box">
|
|
||||||
<div className="box-body">
|
|
||||||
|
|
||||||
<form id="frmAddAdjustment" name="frmAddAdjustment" role="form" data-toggle="validator" className="form-horizontal">
|
|
||||||
|
|
||||||
|
|
||||||
<div className="row" role="main">
|
|
||||||
<div className="col-md-12">
|
|
||||||
<div className="form-group has-feedback">
|
|
||||||
<label className="col-sm-2 control-label">LMT</label>
|
|
||||||
<div className="col-sm-10">
|
|
||||||
<select id="selUser" className="form-control" required>
|
|
||||||
</select>
|
|
||||||
<span className="glyphicon form-control-feedback" aria-hidden="true" style={{ right: '25px' }}></span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="form-group has-feedback">
|
|
||||||
<label className="col-sm-2 control-label">Location</label>
|
|
||||||
<div className="col-sm-10">
|
|
||||||
<select id="selLocations" className="form-control" required>
|
|
||||||
</select>
|
|
||||||
<span className="glyphicon form-control-feedback" aria-hidden="true" style={{ right: '25px' }}></span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="form-group has-feedback has-success">
|
|
||||||
<label className="col-sm-2 control-label">Batch</label>
|
|
||||||
<div className="col-sm-10">
|
|
||||||
<select id="selBatch" className="form-control" required>
|
|
||||||
</select>
|
|
||||||
<span className="glyphicon form-control-feedback" aria-hidden="true" style={{ right: '25px' }}></span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="form-group has-feedback">
|
|
||||||
<label className="col-sm-2 control-label">Amount</label>
|
|
||||||
<div className="col-sm-4 has-feedback">
|
|
||||||
<input type="text" id="txtAmount" className="form-control" required pattern="^-?\d+(\.\d{1,2})?$" />
|
|
||||||
<span className="glyphicon form-control-feedback" aria-hidden="true" style={{ right: '25px' }}></span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
<div className="form-group">
|
|
||||||
<label className="col-sm-2 control-label">Revenue?</label>
|
|
||||||
<div className="col-sm-4 has-feedback">
|
|
||||||
<select id="selRevenue" className="form-control">
|
|
||||||
<option value="Yes">Yes</option>
|
|
||||||
<option value="No">No</option>
|
|
||||||
</select>
|
|
||||||
<span className="glyphicon form-control-feedback" aria-hidden="true" style={{ right: '25px' }}></span>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
</div>
|
|
||||||
<div className="form-group has-feedback">
|
|
||||||
<label className="col-sm-2 control-label">Description</label>
|
|
||||||
<div className="col-sm-10">
|
|
||||||
<input type="text" id="txtDescription" className="form-control" required />
|
|
||||||
<span className="glyphicon form-control-feedback" aria-hidden="true" style={{ right: '25px' }}></span>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
<button id="btnSaveAdjustment" type="button" className="btn btn-success pull-right ">Save Adjustment</button>
|
|
||||||
|
|
||||||
|
|
||||||
</form>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</section>
|
|
||||||
<section className="content">
|
|
||||||
|
|
||||||
{/* Info boxes */}
|
|
||||||
<div className="row">
|
|
||||||
<div className="col-md-12">
|
|
||||||
{/* Box Comment */}
|
|
||||||
<div className="box">
|
|
||||||
|
|
||||||
{/* /.box-header */}
|
|
||||||
<div className="box-body table-responsive table-condensed no-padding">
|
|
||||||
<table id="tblVehicles" className="table table-hover">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>ID</th>
|
|
||||||
<th>VIN</th>
|
|
||||||
<th>Name</th>
|
|
||||||
<th>Price</th>
|
|
||||||
<th>Stock #</th>
|
|
||||||
<th>Year</th>
|
|
||||||
<th>Make</th>
|
|
||||||
<th>Model</th>
|
|
||||||
<th>Ext. Color</th>
|
|
||||||
<th>Int. Color</th>
|
|
||||||
<th>Seating</th>
|
|
||||||
<th>Transmission</th>
|
|
||||||
<th>Title</th>
|
|
||||||
<th>Featured<br />
|
|
||||||
Image</th>
|
|
||||||
<th>Images</th>
|
|
||||||
<th>View</th>
|
|
||||||
<th>Edit</th>
|
|
||||||
</tr>
|
|
||||||
|
|
||||||
</thead>
|
|
||||||
<tbody id="divRows">
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
{/* /.box-body */}
|
|
||||||
</div>
|
|
||||||
{/* /.box */}
|
|
||||||
</div>
|
|
||||||
{/* /.col */}
|
|
||||||
</div>
|
|
||||||
{/* /.row */}
|
|
||||||
</section>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="divEditVehicle" className="modal fade">
|
|
||||||
<div className="modal-dialog modal-lg">
|
|
||||||
|
|
||||||
<div className="modal-content">
|
|
||||||
<form id="frmEditVehicle" name="frmEditVehicle" role="form" data-toggle="validator" data-focus="false" className="form-group-sm">
|
|
||||||
|
|
||||||
<div className="modal-header" style={{ background: '#00a65a' }} >
|
|
||||||
<button type="button" className="close" data-dismiss="modal" aria-label="Close">
|
|
||||||
<span aria-hidden="true">×</span>
|
|
||||||
</button>
|
|
||||||
<h4 className="modal-title" style={{ color: 'white' }}><span id="spanEditType">Edit Patient</span></h4>
|
|
||||||
</div>
|
|
||||||
<div className="modal-body">
|
|
||||||
<div className="row" role="main">
|
|
||||||
<div className="col-md-12">
|
|
||||||
<div className="form-group has-feedback">
|
|
||||||
|
|
||||||
<label className="control-label">Display Name</label>
|
|
||||||
<div className="has-feedback">
|
|
||||||
<input type="text" id="txtDisplayName" className="form-control input-sm" required placeholder="" />
|
|
||||||
<span className="glyphicon form-control-feedback" aria-hidden="true"></span>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="col-md-3">
|
|
||||||
<div className="form-group has-feedback">
|
|
||||||
|
|
||||||
<label className="control-label">VIN</label>
|
|
||||||
<div className="has-feedback">
|
|
||||||
<input type="text" id="txtVIN" className="form-control input-sm" required placeholder="" />
|
|
||||||
<span className="glyphicon form-control-feedback" aria-hidden="true"></span>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="col-md-3">
|
|
||||||
<div className="form-group has-feedback">
|
|
||||||
|
|
||||||
<label className="control-label">Stock #</label>
|
|
||||||
<div className="has-feedback">
|
|
||||||
<input type="text" id="txtStockNumber" className="form-control input-sm" required placeholder="" />
|
|
||||||
<span className="glyphicon form-control-feedback" aria-hidden="true"></span>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="col-md-3">
|
|
||||||
<div className="form-group has-feedback">
|
|
||||||
|
|
||||||
<label className="control-label">Year</label>
|
|
||||||
<div className="has-feedback">
|
|
||||||
<input type="text" id="txtYear" className="form-control input-sm" required placeholder="" />
|
|
||||||
<span className="glyphicon form-control-feedback" aria-hidden="true"></span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="col-md-3">
|
|
||||||
<div className="form-group has-feedback">
|
|
||||||
|
|
||||||
<label className="control-label">Make</label>
|
|
||||||
<div className="has-feedback">
|
|
||||||
<input type="text" id="txtMake" className="form-control input-sm" required placeholder="" />
|
|
||||||
<span className="glyphicon form-control-feedback" aria-hidden="true"></span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="col-md-3">
|
|
||||||
<div className="form-group has-feedback">
|
|
||||||
|
|
||||||
<label className="control-label">Model</label>
|
|
||||||
<div className="has-feedback">
|
|
||||||
<input type="text" id="txtModel" className="form-control input-sm" required placeholder="" />
|
|
||||||
<span className="glyphicon form-control-feedback" aria-hidden="true"></span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="col-md-3">
|
|
||||||
<div className="form-group has-feedback">
|
|
||||||
|
|
||||||
<label className="control-label">Price</label>
|
|
||||||
<div className="has-feedback">
|
|
||||||
<input type="text" id="txtPrice" className="form-control input-sm" required placeholder="" />
|
|
||||||
<span className="glyphicon form-control-feedback" aria-hidden="true"></span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="col-md-3">
|
|
||||||
<div className="form-group has-feedback">
|
|
||||||
|
|
||||||
<label className="control-label">Seating</label>
|
|
||||||
<div className="has-feedback">
|
|
||||||
<input type="text" id="txtSeating" className="form-control input-sm" required placeholder="" />
|
|
||||||
<span className="glyphicon form-control-feedback" aria-hidden="true"></span>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="col-md-3">
|
|
||||||
<div className="form-group has-feedback">
|
|
||||||
|
|
||||||
<label className="control-label">Transmission</label>
|
|
||||||
<div className="has-feedback">
|
|
||||||
<input type="text" id="txtTranmission" className="form-control input-sm" required placeholder="" />
|
|
||||||
<span className="glyphicon form-control-feedback" aria-hidden="true"></span>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="col-md-3">
|
|
||||||
<div className="form-group has-feedback">
|
|
||||||
|
|
||||||
<label className="control-label">Interior Color</label>
|
|
||||||
<div className="has-feedback">
|
|
||||||
<input type="text" id="txtInteriorColor" className="form-control input-sm" required placeholder="" />
|
|
||||||
<span className="glyphicon form-control-feedback" aria-hidden="true"></span>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="col-md-3">
|
|
||||||
<div className="form-group has-feedback">
|
|
||||||
|
|
||||||
<label className="control-label">Exterior Color</label>
|
|
||||||
<div className="has-feedback">
|
|
||||||
<input type="text" id="txtExteriorColor" className="form-control input-sm" required placeholder="" />
|
|
||||||
<span className="glyphicon form-control-feedback" aria-hidden="true"></span>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="col-md-3">
|
|
||||||
<div className="form-group has-feedback">
|
|
||||||
<label className="control-label">Featured Image</label>
|
|
||||||
<div className="has-feedback">
|
|
||||||
<img style={{ width: '75px', height: 'auto' }} src="" type="text" id="imgFeaturedImage" />
|
|
||||||
<span className="glyphicon form-control-feedback" aria-hidden="true"></span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="col-md-12">
|
|
||||||
<div className="form-group has-feedback">
|
|
||||||
<label className="control-label">Additional Images</label>
|
|
||||||
<div className="drop-zone" id="drop-zone">
|
|
||||||
Drag and drop images here or click to select
|
|
||||||
|
|
||||||
</div>
|
|
||||||
<input type="file" id="file-input" multiple style={{ display: 'none' }} />
|
|
||||||
<div className="preview" id="preview"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
</div>
|
|
||||||
<div className="modal-footer">
|
|
||||||
|
|
||||||
<div>
|
|
||||||
|
|
||||||
<button type="button" className="btn btn-default" data-dismiss="modal">Cancel</button>
|
|
||||||
<input id="btnSaveVehicle" type="button" className="btn btn-primary" value="Save" />
|
|
||||||
<span id="spanSpinner" className="fa fa-sync-alt" style={{ fontSize: '20px', verticalAlign: 'middle', display: 'none' }}></span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* /.modal-content */}
|
|
||||||
</div>
|
|
||||||
{/* /.modal-dialog */}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<div id="divUploadDocument" className="modal fade">
|
|
||||||
<div className="modal-dialog">
|
|
||||||
<div className="modal-content">
|
|
||||||
<form id="frmUploadUserDocument" name="frmUploadUserDocument" role="form" data-toggle="validator" style={{ paddingTop: '5px' }} >
|
|
||||||
<div className="modal-header">
|
|
||||||
<button type="button" className="close" data-dismiss="modal" aria-label="Close">
|
|
||||||
<span aria-hidden="true">×</span></button>
|
|
||||||
<h4 className="modal-title">Upload new document?</h4>
|
|
||||||
</div>
|
|
||||||
<div className="modal-body">
|
|
||||||
<div className="row" role="main">
|
|
||||||
<div className="col-md-12">
|
|
||||||
<div id="divNewDocument" className="col-sm-12">
|
|
||||||
<input type="file" name="newDocument" id="newDocument" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="modal-footer">
|
|
||||||
<button type="button" className="btn btn-default pull-left" data-dismiss="modal">Cancel</button>
|
|
||||||
<input id="btnSubmitNewDocumentFinal" type="button" className="btn btn-danger" value="Submit Document" />
|
|
||||||
<span id="spanSubmitNewDocumentSpinner" className="fa fa-sync-alt" style={{ fontSize: '20px', verticalAlign: 'middle', display: 'none' }} ></span>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
{/* /.modal-content */}
|
|
||||||
</div>
|
|
||||||
{/* /.modal-dialog */}
|
|
||||||
</div></div>
|
|
||||||
);
|
|
||||||
//return content;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Vehicles;
|
|
||||||
@ -1,11 +0,0 @@
|
|||||||
import { StrictMode } from 'react'
|
|
||||||
import { createRoot } from 'react-dom/client'
|
|
||||||
import '@/css/main.css'
|
|
||||||
import App from '@/components/pages/App.tsx'
|
|
||||||
import '@/config/constants';
|
|
||||||
|
|
||||||
createRoot(document.getElementById('root')).render(
|
|
||||||
<StrictMode>
|
|
||||||
<App />
|
|
||||||
</StrictMode>,
|
|
||||||
)
|
|
||||||
@ -0,0 +1,24 @@
|
|||||||
|
import { StrictMode } from 'react'
|
||||||
|
import { createRoot } from 'react-dom/client'
|
||||||
|
import '@/css/main.css'
|
||||||
|
import App from '@/components/pages/App'
|
||||||
|
import '@/config/constants';
|
||||||
|
|
||||||
|
import 'bootstrap/dist/css/bootstrap.min.css';
|
||||||
|
import 'admin-lte/dist/css/adminlte.min.css';
|
||||||
|
import 'font-awesome/css/font-awesome.min.css';
|
||||||
|
/*import 'ionicons/dist/css/ionicons.min.css';*/
|
||||||
|
|
||||||
|
import 'bootstrap/dist/js/bootstrap.bundle.min.js'; // Bootstrap JS
|
||||||
|
import 'admin-lte/dist/js/adminlte.min.js';
|
||||||
|
|
||||||
|
const rootElement = document.getElementById('root');
|
||||||
|
if (rootElement) {
|
||||||
|
createRoot(rootElement).render(
|
||||||
|
<StrictMode>
|
||||||
|
<App />
|
||||||
|
</StrictMode>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
throw new Error('Root element not found');
|
||||||
|
}
|
||||||
@ -0,0 +1,53 @@
|
|||||||
|
// App.tsx or main routing component
|
||||||
|
//import React from 'react';
|
||||||
|
import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom';
|
||||||
|
import Layout from '@/components/layouts/Layout';
|
||||||
|
import LayoutLogin from '@/components/layouts/LayoutLogin';
|
||||||
|
import Home from '@/components/pages/Home';
|
||||||
|
import Login from '@/components/pages/Login';
|
||||||
|
import Targets from '@/components/pages/Targets';
|
||||||
|
import Templates from '@/components/pages/Templates';
|
||||||
|
|
||||||
|
const App = () => {
|
||||||
|
return (
|
||||||
|
<Router basename="/">
|
||||||
|
<Routes>
|
||||||
|
<Route path="/" element={<Navigate to="/login" replace />} />
|
||||||
|
<Route
|
||||||
|
path="/home"
|
||||||
|
element={
|
||||||
|
<Layout>
|
||||||
|
<Home />
|
||||||
|
</Layout>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path="/targets"
|
||||||
|
element={
|
||||||
|
<Layout>
|
||||||
|
<Targets />
|
||||||
|
</Layout>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path="/templates"
|
||||||
|
element={
|
||||||
|
<Layout>
|
||||||
|
<Templates />
|
||||||
|
</Layout>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path="/login"
|
||||||
|
element={
|
||||||
|
<LayoutLogin>
|
||||||
|
<Login />
|
||||||
|
</LayoutLogin>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Routes>
|
||||||
|
</Router>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default App;
|
||||||
@ -0,0 +1,63 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
const Home: React.FC = () => {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{/* Content Header */}
|
||||||
|
<div className="app-content-header">
|
||||||
|
<div className="container-fluid">
|
||||||
|
<div className="row">
|
||||||
|
<div className="col-sm-6">
|
||||||
|
<h3 className="mb-0">Dashboard</h3>
|
||||||
|
</div>
|
||||||
|
<div className="col-sm-6">
|
||||||
|
<ol className="breadcrumb float-sm-end">
|
||||||
|
<li className="breadcrumb-item">
|
||||||
|
<a href="#">Home</a>
|
||||||
|
</li>
|
||||||
|
<li className="breadcrumb-item active" aria-current="page">
|
||||||
|
Dashboard
|
||||||
|
</li>
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Page-specific Content */}
|
||||||
|
<div className="app-content">
|
||||||
|
<div className="container-fluid">
|
||||||
|
<div className="row">
|
||||||
|
{/* Example: Small Box Widget 1 */}
|
||||||
|
<div className="col-lg-3 col-6">
|
||||||
|
<div className="small-box text-bg-primary">
|
||||||
|
<div className="inner">
|
||||||
|
<h3>150</h3>
|
||||||
|
<p>New Orders</p>
|
||||||
|
</div>
|
||||||
|
<svg
|
||||||
|
className="small-box-icon"
|
||||||
|
fill="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path d="M2.25 2.25a.75.75 0 000 1.5h1.386c.17 0 .318.114.362.278l2.558 9.592a3.752 3.752 0 00-2.806 3.63c0 .414.336.75.75.75h15.75a.75.75 0 000-1.5H5.378A2.25 2.25 0 017.5 15h11.218a.75.75 0 00.674-.421 60.358 60.358 0 002.96-7.228.75.75 0 00-.525-.965A60.864 60.864 0 005.68 4.509l-.232-.867A1.875 1.875 0 003.636 2.25H2.25zM3.75 20.25a1.5 1.5 0 113 0 1.5 1.5 0 01-3 0zM16.5 20.25a1.5 1.5 0 113 0 1.5 1.5 0 01-3 0z"></path>
|
||||||
|
</svg>
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
className="small-box-footer link-light link-underline-opacity-0 link-underline-opacity-50-hover"
|
||||||
|
>
|
||||||
|
More info <i className="bi bi-link-45deg"></i>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/* Additional small boxes, charts, or other widgets can be included here */}
|
||||||
|
</div>
|
||||||
|
{/* You can also add more rows for charts, direct chat, etc. */}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Home;
|
||||||
@ -0,0 +1,199 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import { Button, Form, Spinner } from 'react-bootstrap';
|
||||||
|
import { AuthResponse, AuthErrorResponse, User, isAuthErrorResponse } from '@/types/auth';
|
||||||
|
//import { Helmet, HelmetProvider } from 'react-helmet-async';
|
||||||
|
|
||||||
|
import utils from '@/ts/utils.ts';
|
||||||
|
import ForgotPasswordModal from '@/components/modals/ForgotPasswordModal';
|
||||||
|
|
||||||
|
type SpinnerState = Record<string, boolean>;
|
||||||
|
type FormErrors = Record<string, string>;
|
||||||
|
|
||||||
|
function Login() {
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [spinners, setSpinnersState] = useState<SpinnerState>({});
|
||||||
|
const [formErrors, setFormErrors] = useState<FormErrors>({});
|
||||||
|
const [username, setUsername] = useState('');
|
||||||
|
const [password, setPassword] = useState('');
|
||||||
|
const [showForgotPasswordModal, setShowForgotPasswordModal] = useState(false);
|
||||||
|
//const [user, setUser] = useState<User | null>(null);
|
||||||
|
const [loginError, setLoginError] = useState<boolean>(false);
|
||||||
|
const [loginErrorMessage, setLoginErrorMessage] = useState<string>('');
|
||||||
|
|
||||||
|
//const setSpinners = (newValues: Partial<SpinnerState>) => {
|
||||||
|
// setSpinnersState((prevSpinners) => ({
|
||||||
|
// ...prevSpinners,
|
||||||
|
// ...newValues,
|
||||||
|
// }));
|
||||||
|
//};
|
||||||
|
const setSpinners = (newValues: Partial<SpinnerState>) => {
|
||||||
|
setSpinnersState((prevSpinners) => {
|
||||||
|
const updatedSpinners: SpinnerState = { ...prevSpinners };
|
||||||
|
for (const key in newValues) {
|
||||||
|
if (newValues[key] !== undefined) {
|
||||||
|
updatedSpinners[key] = newValues[key] as boolean;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return updatedSpinners;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCloseForgotPasswordModal = () => {
|
||||||
|
setShowForgotPasswordModal(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const validateLoginForm = () => {
|
||||||
|
setFormErrors({});
|
||||||
|
|
||||||
|
const errors: FormErrors = {};
|
||||||
|
if (!username.trim()) {
|
||||||
|
errors.username = 'Username is required';
|
||||||
|
//} else if (!/\S+@\S+\.\S+/.test(email)) {
|
||||||
|
// errors.email = 'Invalid email address';
|
||||||
|
}
|
||||||
|
if (!password.trim()) {
|
||||||
|
errors.password = 'Password is required';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Object.keys(errors).length > 0) {
|
||||||
|
setFormErrors(errors);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleLogin = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setIsLoading(true);
|
||||||
|
spinners.Login = true;
|
||||||
|
setSpinners(spinners);
|
||||||
|
|
||||||
|
validateLoginForm();
|
||||||
|
|
||||||
|
if (Object.keys(formErrors).length > 0) return;
|
||||||
|
|
||||||
|
//setUser(null);
|
||||||
|
setLoginError(false);
|
||||||
|
setLoginErrorMessage('');
|
||||||
|
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 {
|
||||||
|
loggedInUser = json.user;
|
||||||
|
//setUser(loggedInUser);
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
const errorMsg: string = "Unexpected Error";
|
||||||
|
hadLoginError = true;
|
||||||
|
hadLoginErrorMessage = errorMsg;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: (err: unknown) => {
|
||||||
|
let errorMsg: string = "Unexpected Error";
|
||||||
|
if (isAuthErrorResponse(err)) {
|
||||||
|
if (err && err as AuthErrorResponse) {
|
||||||
|
if (err.data) {
|
||||||
|
if (err.data.message)
|
||||||
|
errorMsg = err.data.message;
|
||||||
|
}
|
||||||
|
console.error(errorMsg);
|
||||||
|
setLoginErrorMessage(errorMsg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
hadLoginError = true;
|
||||||
|
hadLoginErrorMessage = errorMsg;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (hadLoginError) {
|
||||||
|
setLoginErrorMessage(hadLoginErrorMessage);
|
||||||
|
setLoginError(true);
|
||||||
|
setIsLoading(false);
|
||||||
|
spinners.Login = false;
|
||||||
|
setSpinners(spinners);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (loggedInUser == null) {
|
||||||
|
setLoginError(true);
|
||||||
|
setIsLoading(false);
|
||||||
|
spinners.Login = false;
|
||||||
|
setSpinners(spinners);
|
||||||
|
} else {
|
||||||
|
await finishUserLogin(loggedInUser);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const finishUserLogin = async (loggedInUser: User) => {
|
||||||
|
setIsLoading(false);
|
||||||
|
spinners.Login = false;
|
||||||
|
spinners.LoginWithPasskey = false;
|
||||||
|
setSpinners(spinners);
|
||||||
|
|
||||||
|
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';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="container">
|
||||||
|
<div className="row text-center mt-5">
|
||||||
|
<h1>surge365 - React</h1>
|
||||||
|
</div>
|
||||||
|
<div className="row text-center" style={{ maxWidth: '400px', margin: 'auto' }}>
|
||||||
|
<h3 className="form-signin-heading mt-3 mb-1">Please sign in</h3>
|
||||||
|
<Form id="frmLogin" onSubmit={handleLogin}>
|
||||||
|
{loginError && (
|
||||||
|
<Form.Label style={{ color: 'red' }}>{loginErrorMessage ?? "Login error"}</Form.Label>
|
||||||
|
)}
|
||||||
|
<Form.Group className="mb-3" controlId="txtUsernamel">
|
||||||
|
<Form.Label className="visually-hidden">Username</Form.Label>
|
||||||
|
<Form.Control
|
||||||
|
type="username"
|
||||||
|
placeholder="Username"
|
||||||
|
value={username}
|
||||||
|
onChange={(e) => setUsername(e.target.value)}
|
||||||
|
required
|
||||||
|
autoFocus
|
||||||
|
size="sm"
|
||||||
|
/>
|
||||||
|
{spinners.Username && <Spinner animation="border" size="sm" />}
|
||||||
|
</Form.Group>
|
||||||
|
|
||||||
|
<Form.Group className="mb-3" controlId="txtPassword">
|
||||||
|
<Form.Label className="visually-hidden">Password</Form.Label>
|
||||||
|
<Form.Control
|
||||||
|
type="password"
|
||||||
|
placeholder="Password"
|
||||||
|
value={password}
|
||||||
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
|
required
|
||||||
|
size="sm"
|
||||||
|
/>
|
||||||
|
</Form.Group>
|
||||||
|
|
||||||
|
<Button className="bg-orange w-100" type="submit" disabled={isLoading}>
|
||||||
|
{spinners.Login && <Spinner animation="border" size="sm" className="me-2" />}
|
||||||
|
{isLoading && spinners.Login ? 'Signing in...' : 'Sign in'}
|
||||||
|
</Button>
|
||||||
|
<Button variant="secondary" className="w-100 mt-2" onClick={() => setShowForgotPasswordModal(true)}>
|
||||||
|
Forgot Password
|
||||||
|
</Button>
|
||||||
|
</Form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ForgotPasswordModal show={showForgotPasswordModal} onClose={handleCloseForgotPasswordModal} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Login;
|
||||||
@ -0,0 +1,63 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
const Targets: React.FC = () => {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{/* Content Header */}
|
||||||
|
<div className="app-content-header">
|
||||||
|
<div className="container-fluid">
|
||||||
|
<div className="row">
|
||||||
|
<div className="col-sm-6">
|
||||||
|
<h3 className="mb-0">Targets</h3>
|
||||||
|
</div>
|
||||||
|
<div className="col-sm-6">
|
||||||
|
<ol className="breadcrumb float-sm-end">
|
||||||
|
<li className="breadcrumb-item">
|
||||||
|
<a href="#">Home</a>
|
||||||
|
</li>
|
||||||
|
<li className="breadcrumb-item active" aria-current="page">
|
||||||
|
Targets
|
||||||
|
</li>
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Page-specific Content */}
|
||||||
|
<div className="app-content">
|
||||||
|
<div className="container-fluid">
|
||||||
|
<div className="row">
|
||||||
|
{/* Example: Small Box Widget 1 */}
|
||||||
|
<div className="col-lg-3 col-6">
|
||||||
|
<div className="small-box text-bg-primary">
|
||||||
|
<div className="inner">
|
||||||
|
<h3>150</h3>
|
||||||
|
<p>Targets</p>
|
||||||
|
</div>
|
||||||
|
<svg
|
||||||
|
className="small-box-icon"
|
||||||
|
fill="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path d="M2.25 2.25a.75.75 0 000 1.5h1.386c.17 0 .318.114.362.278l2.558 9.592a3.752 3.752 0 00-2.806 3.63c0 .414.336.75.75.75h15.75a.75.75 0 000-1.5H5.378A2.25 2.25 0 017.5 15h11.218a.75.75 0 00.674-.421 60.358 60.358 0 002.96-7.228.75.75 0 00-.525-.965A60.864 60.864 0 005.68 4.509l-.232-.867A1.875 1.875 0 003.636 2.25H2.25zM3.75 20.25a1.5 1.5 0 113 0 1.5 1.5 0 01-3 0zM16.5 20.25a1.5 1.5 0 113 0 1.5 1.5 0 01-3 0z"></path>
|
||||||
|
</svg>
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
className="small-box-footer link-light link-underline-opacity-0 link-underline-opacity-50-hover"
|
||||||
|
>
|
||||||
|
More info <i className="bi bi-link-45deg"></i>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/* Additional small boxes, charts, or other widgets can be included here */}
|
||||||
|
</div>
|
||||||
|
{/* You can also add more rows for charts, direct chat, etc. */}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Targets;
|
||||||
@ -0,0 +1,63 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
const Templates: React.FC = () => {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{/* Content Header */}
|
||||||
|
<div className="app-content-header">
|
||||||
|
<div className="container-fluid">
|
||||||
|
<div className="row">
|
||||||
|
<div className="col-sm-6">
|
||||||
|
<h3 className="mb-0">Templates</h3>
|
||||||
|
</div>
|
||||||
|
<div className="col-sm-6">
|
||||||
|
<ol className="breadcrumb float-sm-end">
|
||||||
|
<li className="breadcrumb-item">
|
||||||
|
<a href="./home">Home</a>
|
||||||
|
</li>
|
||||||
|
<li className="breadcrumb-item active" aria-current="page">
|
||||||
|
Templates
|
||||||
|
</li>
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Page-specific Content */}
|
||||||
|
<div className="app-content">
|
||||||
|
<div className="container-fluid">
|
||||||
|
<div className="row">
|
||||||
|
{/* Example: Small Box Widget 1 */}
|
||||||
|
<div className="col-lg-3 col-6">
|
||||||
|
<div className="small-box text-bg-primary">
|
||||||
|
<div className="inner">
|
||||||
|
<h3>150</h3>
|
||||||
|
<p>Templates</p>
|
||||||
|
</div>
|
||||||
|
<svg
|
||||||
|
className="small-box-icon"
|
||||||
|
fill="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path d="M2.25 2.25a.75.75 0 000 1.5h1.386c.17 0 .318.114.362.278l2.558 9.592a3.752 3.752 0 00-2.806 3.63c0 .414.336.75.75.75h15.75a.75.75 0 000-1.5H5.378A2.25 2.25 0 017.5 15h11.218a.75.75 0 00.674-.421 60.358 60.358 0 002.96-7.228.75.75 0 00-.525-.965A60.864 60.864 0 005.68 4.509l-.232-.867A1.875 1.875 0 003.636 2.25H2.25zM3.75 20.25a1.5 1.5 0 113 0 1.5 1.5 0 01-3 0zM16.5 20.25a1.5 1.5 0 113 0 1.5 1.5 0 01-3 0z"></path>
|
||||||
|
</svg>
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
className="small-box-footer link-light link-underline-opacity-0 link-underline-opacity-50-hover"
|
||||||
|
>
|
||||||
|
More info <i className="bi bi-link-45deg"></i>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/* Additional small boxes, charts, or other widgets can be included here */}
|
||||||
|
</div>
|
||||||
|
{/* You can also add more rows for charts, direct chat, etc. */}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Templates;
|
||||||
@ -0,0 +1,49 @@
|
|||||||
|
import { LineChart, lineElementClasses } from '@mui/x-charts/LineChart';
|
||||||
|
import dayjs from "dayjs";
|
||||||
|
|
||||||
|
const uData = [4000, 3000, 2000, 2780, 1890, 2390, 3490]; //This is # errors
|
||||||
|
const pData = [2400, 1398, 9800, 0, 4800, 3800, 4300]; //This is # delivered above errors (delivered - errors)
|
||||||
|
const amtData = [2400, 2210, 0, 2000, 2181, 2500, 2100]; /// This is # sent above errors and delivered (sent - delivered - sent)
|
||||||
|
const time = [
|
||||||
|
new Date(2024, 7, 0),
|
||||||
|
new Date(2024, 8, 0),
|
||||||
|
new Date(2024, 9, 0),
|
||||||
|
new Date(2024, 10, 0),
|
||||||
|
new Date(2024, 11, 0),
|
||||||
|
new Date(2024, 12, 0),
|
||||||
|
new Date(2025, 1, 0)
|
||||||
|
];
|
||||||
|
|
||||||
|
export default function LineChartSample() {
|
||||||
|
return (
|
||||||
|
<LineChart
|
||||||
|
width={500}
|
||||||
|
height={300}
|
||||||
|
series={[
|
||||||
|
{ data: uData, label: 'Error', area: true, stack: 'total', showMark: false },
|
||||||
|
{ data: pData, label: 'Delivered', area: true, stack: 'total', showMark: false },
|
||||||
|
{
|
||||||
|
data: amtData,
|
||||||
|
label: 'Sent',
|
||||||
|
area: true,
|
||||||
|
stack: 'total',
|
||||||
|
showMark: false,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
xAxis={[
|
||||||
|
{
|
||||||
|
scaleType: 'time',
|
||||||
|
data: time,
|
||||||
|
min: time[0].getTime(),
|
||||||
|
max: time[time.length - 1].getTime(),
|
||||||
|
valueFormatter: (value) => dayjs(value).format("MMM YY")
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
sx={{
|
||||||
|
[`& .${lineElementClasses.root}`]: {
|
||||||
|
display: 'none',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
86
Surge365.MassEmailReact.Web/src/context/SetupDataContext.tsx
Normal file
86
Surge365.MassEmailReact.Web/src/context/SetupDataContext.tsx
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
import { createContext, useState, useEffect, useContext } from "react";
|
||||||
|
import { Target } from "@/types/target";
|
||||||
|
import { Server } from "@/types/server";
|
||||||
|
|
||||||
|
export type SetupData = {
|
||||||
|
targets: Target[];
|
||||||
|
reloadTargets: () => void;
|
||||||
|
setTargets: (updatedTarget: Target) => void;
|
||||||
|
servers: Server[];
|
||||||
|
reloadServers: () => void;
|
||||||
|
reloadSetupData: () => void;
|
||||||
|
targetsLoading: boolean;
|
||||||
|
serversLoading: boolean;
|
||||||
|
dataLoading: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
const SetupDataContext = createContext<SetupData | undefined>(undefined);
|
||||||
|
|
||||||
|
export const SetupDataProvider = ({ children }: { children: React.ReactNode }) => {
|
||||||
|
const [servers, setServers] = useState<Server[]>([]);
|
||||||
|
const [targets, setTargets] = useState<Target[]>([]);
|
||||||
|
const [targetsLoading, setTargetsLoading] = useState<boolean>(false);
|
||||||
|
const [serversLoading, setServersLoading] = useState<boolean>(false);
|
||||||
|
const [dataLoading, setDataLoading] = useState<boolean>(false);
|
||||||
|
|
||||||
|
const reloadSetupData = async () => {
|
||||||
|
sessionStorage.removeItem("setupData");
|
||||||
|
await fetchSetupData();
|
||||||
|
}
|
||||||
|
const fetchSetupData = async () => {
|
||||||
|
try {
|
||||||
|
setDataLoading(true);
|
||||||
|
setTargetsLoading(true);
|
||||||
|
setServersLoading(true);
|
||||||
|
const cachedData = sessionStorage.getItem("setupData");
|
||||||
|
if (cachedData) { //TODO: check if data is stale
|
||||||
|
const parsedData = JSON.parse(cachedData);
|
||||||
|
setTargets(parsedData.targets);
|
||||||
|
setServers(parsedData.servers);
|
||||||
|
setDataLoading(false);
|
||||||
|
setTargetsLoading(false);
|
||||||
|
setServersLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const targetsResponse = await fetch("/api/targets/GetAll?activeOnly=false");
|
||||||
|
const targetsData = await targetsResponse.json();
|
||||||
|
setTargets(targetsData);
|
||||||
|
setTargetsLoading(false);
|
||||||
|
//const serversResponse = await fetch("/api/setup/servers"); //TODO: call once setup
|
||||||
|
//const serversData = await serversResponse.json();
|
||||||
|
|
||||||
|
const serversData: Server[] = [];
|
||||||
|
setServers(serversData);
|
||||||
|
setServersLoading(false);
|
||||||
|
setDataLoading(false);
|
||||||
|
sessionStorage.setItem("setupData", JSON.stringify({ targets: targetsData, servers: serversData }));
|
||||||
|
} catch (error) {
|
||||||
|
setDataLoading(false);
|
||||||
|
setTargetsLoading(false);
|
||||||
|
setServersLoading(false);
|
||||||
|
console.error("Failed to fetch setup data:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateTargetCache = (updatedTarget: Target) => {
|
||||||
|
setTargets((prevTargets) =>
|
||||||
|
prevTargets.map((target) => (target.id === updatedTarget.id ? updatedTarget : target))
|
||||||
|
);
|
||||||
|
sessionStorage.setItem("setupData", JSON.stringify({ servers, targets }));
|
||||||
|
};
|
||||||
|
useEffect(() => {
|
||||||
|
fetchSetupData();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SetupDataContext.Provider value={{ targets, reloadTargets: reloadSetupData, setTargets: updateTargetCache, servers, reloadServers: reloadSetupData, reloadSetupData: reloadSetupData, targetsLoading, serversLoading, dataLoading }}>
|
||||||
|
{children}
|
||||||
|
</SetupDataContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useSetupData = () => {
|
||||||
|
const context = useContext(SetupDataContext);
|
||||||
|
if (!context) throw new Error("useSetupData must be used within a SetupDataProvider");
|
||||||
|
return context;
|
||||||
|
};
|
||||||
@ -0,0 +1,2 @@
|
|||||||
|
@import url('https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap');
|
||||||
|
@import url('https://fonts.googleapis.com/icon?family=Material+Icons');
|
||||||
3
Surge365.MassEmailReact.Web/src/theme/theme.tsx
Normal file
3
Surge365.MassEmailReact.Web/src/theme/theme.tsx
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
export const ColorModeContext = React.createContext({ toggleColorMode: () => { } });
|
||||||
@ -1,3 +1,15 @@
|
|||||||
|
export class ApiError extends Error {
|
||||||
|
public status: number;
|
||||||
|
public data: unknown;
|
||||||
|
|
||||||
|
constructor(status: number, data: unknown) {
|
||||||
|
super(`HTTP error! status: ${status}`);
|
||||||
|
this.status = status;
|
||||||
|
this.data = data;
|
||||||
|
Object.setPrototypeOf(this, ApiError.prototype);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const utils = {
|
const utils = {
|
||||||
getCookie: (name: string): string | null => {
|
getCookie: (name: string): string | null => {
|
||||||
const value = `; ${document.cookie}`;
|
const value = `; ${document.cookie}`;
|
||||||
@ -6,7 +18,7 @@ const utils = {
|
|||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
getParameterByName: (name: string): string | null => {
|
getParameterByName: (name: string): string | null => {
|
||||||
const regex = new RegExp(`[\?&]${name}=([^&#]*)`);
|
const regex = new RegExp(`[?&]${name}=([^&#]*)`);
|
||||||
const results = regex.exec(window.location.search);
|
const results = regex.exec(window.location.search);
|
||||||
return results ? decodeURIComponent(results[1].replace(/\+/g, ' ')) : null;
|
return results ? decodeURIComponent(results[1].replace(/\+/g, ' ')) : null;
|
||||||
},
|
},
|
||||||
@ -29,26 +41,26 @@ const utils = {
|
|||||||
|
|
||||||
return headers;
|
return headers;
|
||||||
},
|
},
|
||||||
webMethod: async ({
|
webMethod: async <T = unknown>({
|
||||||
httpMethod = 'POST',
|
httpMethod = 'POST',
|
||||||
baseMethodPath = 'api/',
|
baseMethodPath = 'api/',
|
||||||
methodPage = '',
|
methodPage = '',
|
||||||
methodName = '',
|
methodName = '',
|
||||||
parameters = {},
|
parameters = {} as Record<string, unknown>,
|
||||||
contentType = 'application/json;',
|
contentType = 'application/json;',
|
||||||
timeout = 300000,
|
timeout = 300000,
|
||||||
success = () => { },
|
success = (_data: T) => { },
|
||||||
error = () => { },
|
error = (_err: unknown) => { },
|
||||||
}: {
|
}: {
|
||||||
httpMethod?: string;
|
httpMethod?: string;
|
||||||
baseMethodPath?: string;
|
baseMethodPath?: string;
|
||||||
methodPage?: string;
|
methodPage?: string;
|
||||||
methodName?: string;
|
methodName?: string;
|
||||||
contentType?: string;
|
contentType?: string;
|
||||||
parameters?: Record<string, any>;
|
parameters?: Record<string, unknown>;
|
||||||
timeout?: number;
|
timeout?: number;
|
||||||
success?: (data: any) => void;
|
success?: (_data: T) => void;
|
||||||
error?: (err: any) => void;
|
error?: (_err: unknown) => void;
|
||||||
}): Promise<void> => {
|
}): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
const baseUrl = window.API_BASE_URL || '';
|
const baseUrl = window.API_BASE_URL || '';
|
||||||
@ -59,19 +71,24 @@ const utils = {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const controller = new AbortController();
|
const controller = new AbortController();
|
||||||
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
//const timeoutId = setTimeout(() => controller.abort(), timeout);
|
||||||
|
setTimeout(() => controller.abort(), timeout);
|
||||||
|
|
||||||
const response = await fetch(url, {
|
const response = await fetch(url, {
|
||||||
method: httpMethod,
|
method: httpMethod,
|
||||||
headers,
|
headers,
|
||||||
body: JSON.stringify(parameters),
|
body: (httpMethod.toUpperCase() == "GET" ? null : JSON.stringify(parameters)),
|
||||||
signal: controller.signal,
|
signal: controller.signal,
|
||||||
});
|
});
|
||||||
|
|
||||||
clearTimeout(timeoutId);
|
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(`HTTP error! status: ${response.status}`);
|
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 authToken = response.headers.get('Auth-Token');
|
||||||
|
|||||||
33
Surge365.MassEmailReact.Web/src/types/auth.ts
Normal file
33
Surge365.MassEmailReact.Web/src/types/auth.ts
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
// src/types/auth.ts
|
||||||
|
|
||||||
|
import { UUID } from "crypto";
|
||||||
|
|
||||||
|
export interface User {
|
||||||
|
userKey: number,
|
||||||
|
userId: UUID;
|
||||||
|
username: string;
|
||||||
|
firstName: string;
|
||||||
|
middleInitial: string;
|
||||||
|
lastName: string;
|
||||||
|
isActive: boolean;
|
||||||
|
// Add any other properties returned by your API
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AuthResponse {
|
||||||
|
accessToken: string;
|
||||||
|
user: User;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AuthErrorResponse {
|
||||||
|
|
||||||
|
message: string;
|
||||||
|
data: { message: string };
|
||||||
|
}
|
||||||
|
export function isAuthErrorResponse(err: unknown): err is AuthErrorResponse {
|
||||||
|
return (
|
||||||
|
typeof err === 'object' &&
|
||||||
|
err !== null &&
|
||||||
|
'message' in err &&
|
||||||
|
typeof (err as any).message === 'string'
|
||||||
|
);
|
||||||
|
}
|
||||||
5
Surge365.MassEmailReact.Web/src/types/server.ts
Normal file
5
Surge365.MassEmailReact.Web/src/types/server.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
export interface Server {
|
||||||
|
id?: number;
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
11
Surge365.MassEmailReact.Web/src/types/target.ts
Normal file
11
Surge365.MassEmailReact.Web/src/types/target.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
export interface Target {
|
||||||
|
id?: number;
|
||||||
|
serverId: number;
|
||||||
|
name: string;
|
||||||
|
databaseName: string;
|
||||||
|
viewName: string;
|
||||||
|
filterQuery: string;
|
||||||
|
allowWriteBack: boolean;
|
||||||
|
isActive: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
@ -1,9 +1,10 @@
|
|||||||
{
|
{
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||||
"target": "ES2020",
|
"target": "ES2020",
|
||||||
"useDefineForClassFields": true,
|
"useDefineForClassFields": true,
|
||||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
"lib": [ "ES2020", "DOM", "DOM.Iterable" ],
|
||||||
"module": "ESNext",
|
"module": "ESNext",
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
|
|
||||||
@ -22,5 +23,5 @@
|
|||||||
"noFallthroughCasesInSwitch": true,
|
"noFallthroughCasesInSwitch": true,
|
||||||
"noUncheckedSideEffectImports": true
|
"noUncheckedSideEffectImports": true
|
||||||
},
|
},
|
||||||
"include": ["src"]
|
"include": [ "src" ]
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user