Refactor API structure and update UI components

- Changed namespaces for controllers to better organize API components.
- Updated base class for controllers to `BaseController` with standard routing.
- Simplified method signatures by removing `id` parameters in entity creation methods.
- Introduced new `CreateAsync` methods in service and repository layers.
- Removed dependencies on `font-awesome` and `ionicons` from package files.
- Migrated UI components to Material-UI, enhancing consistency and design.
- Refactored `ForgotPasswordModal` to use Material-UI's `Dialog`.
- Implemented `yup` validation in form components for stricter checks.
- Cleaned up unused styles in CSS files for a cleaner codebase.
- Updated interfaces to require `id` property for stricter type checks.
- Improved error handling and user feedback in the `Login` component.
- Added action buttons for adding and refreshing data in various components.
This commit is contained in:
David Headrick 2025-03-05 12:10:43 -06:00
parent d7b00cf335
commit ef75bdb779
41 changed files with 487 additions and 867 deletions

View File

@ -1,13 +1,12 @@
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Surge365.MassEmailReact.API.Controllers;
using Surge365.MassEmailReact.Application.DTOs; using Surge365.MassEmailReact.Application.DTOs;
using Surge365.MassEmailReact.Application.Interfaces; using Surge365.MassEmailReact.Application.Interfaces;
namespace Surge365.MassEmailReact.Server.Controllers namespace Surge365.MassEmailReact.API.Controllers
{ {
[Route("api/[controller]")] public class AuthenticationController : BaseController
[ApiController]
public class AuthenticationController : ControllerBase
{ {
private readonly IAuthService _authService; private readonly IAuthService _authService;

View File

@ -0,0 +1,11 @@
using Microsoft.AspNetCore.Mvc;
namespace Surge365.MassEmailReact.API.Controllers
{
[Route("[controller]")]
[Route("api/[controller]")]
[ApiController]
public class BaseController : ControllerBase
{
}
}

View File

@ -4,11 +4,9 @@ using Surge365.MassEmailReact.Application.Interfaces;
using Surge365.MassEmailReact.Domain.Entities; using Surge365.MassEmailReact.Domain.Entities;
using System.Net.Mail; using System.Net.Mail;
namespace Surge365.MassEmailReact.Server.Controllers namespace Surge365.MassEmailReact.API.Controllers
{ {
[Route("api/[controller]")] public class BouncedEmailsController : BaseController
[ApiController]
public class BouncedEmailsController : ControllerBase
{ {
private readonly IBouncedEmailService _bouncedEmailService; private readonly IBouncedEmailService _bouncedEmailService;

View File

@ -3,11 +3,9 @@ using Surge365.MassEmailReact.Application.Interfaces;
using Surge365.MassEmailReact.Domain.Entities; using Surge365.MassEmailReact.Domain.Entities;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace Surge365.MassEmailReact.Server.Controllers namespace Surge365.MassEmailReact.API.Controllers
{ {
[Route("api/[controller]")] public class EmailDomainsController : BaseController
[ApiController]
public class EmailDomainsController : ControllerBase
{ {
private readonly IEmailDomainService _emailDomainService; private readonly IEmailDomainService _emailDomainService;
@ -34,7 +32,7 @@ namespace Surge365.MassEmailReact.Server.Controllers
} }
[HttpPost()] [HttpPost()]
public async Task<IActionResult> CreateTarget(int id, [FromBody] EmailDomainUpdateDto emailDomainUpdateDto) public async Task<IActionResult> CreateTarget([FromBody] EmailDomainUpdateDto emailDomainUpdateDto)
{ {
if (emailDomainUpdateDto.Id != null && emailDomainUpdateDto.Id > 0) if (emailDomainUpdateDto.Id != null && emailDomainUpdateDto.Id > 0)
return BadRequest("Id must be null or 0"); return BadRequest("Id must be null or 0");

View File

@ -4,12 +4,11 @@ using Microsoft.AspNetCore.Mvc;
using Surge365.MassEmailReact.Application.DTOs; using Surge365.MassEmailReact.Application.DTOs;
using Surge365.MassEmailReact.Application.Interfaces; using Surge365.MassEmailReact.Application.Interfaces;
using Surge365.MassEmailReact.Domain.Entities; using Surge365.MassEmailReact.Domain.Entities;
using Surge365.MassEmailReact.Infrastructure.Services;
namespace Surge365.MassEmailReact.Server.Controllers namespace Surge365.MassEmailReact.API.Controllers
{ {
[Route("api/[controller]")] public class ServersController : BaseController
[ApiController]
public class ServersController : ControllerBase
{ {
private readonly IServerService _serverService; private readonly IServerService _serverService;
@ -19,6 +18,7 @@ namespace Surge365.MassEmailReact.Server.Controllers
} }
[HttpGet()]
[HttpGet("GetAll")] [HttpGet("GetAll")]
public async Task<IActionResult> GetAll([FromQuery] bool? activeOnly, bool? returnPassword = null) public async Task<IActionResult> GetAll([FromQuery] bool? activeOnly, bool? returnPassword = null)
{ {
@ -35,6 +35,21 @@ namespace Surge365.MassEmailReact.Server.Controllers
var server = await _serverService.GetByIdAsync(id, returnPasswordValue); var server = await _serverService.GetByIdAsync(id, returnPasswordValue);
return server is not null ? Ok(server) : NotFound($"Server with key '{id}' not found."); return server is not null ? Ok(server) : NotFound($"Server with key '{id}' not found.");
} }
[HttpPost()]
public async Task<IActionResult> CreateServer([FromBody] ServerUpdateDto serverUpdateDto)
{
if (serverUpdateDto.Id != null && serverUpdateDto.Id > 0)
return BadRequest("Id must be null or 0");
var serverId = await _serverService.CreateAsync(serverUpdateDto);
if (serverId == null)
return StatusCode(StatusCodes.Status500InternalServerError, "Failed to create server.");
var createdServer = await _serverService.GetByIdAsync(serverId.Value);
return Ok(createdServer);
}
[HttpPut("{id}")] [HttpPut("{id}")]
public async Task<IActionResult> UpdateServer(int id, [FromBody] ServerUpdateDto serverUpdateDto) public async Task<IActionResult> UpdateServer(int id, [FromBody] ServerUpdateDto serverUpdateDto)
{ {

View File

@ -5,11 +5,9 @@ using Surge365.MassEmailReact.Application.DTOs;
using Surge365.MassEmailReact.Application.Interfaces; using Surge365.MassEmailReact.Application.Interfaces;
using Surge365.MassEmailReact.Domain.Entities; using Surge365.MassEmailReact.Domain.Entities;
namespace Surge365.MassEmailReact.Server.Controllers namespace Surge365.MassEmailReact.API.Controllers
{ {
[Route("api/[controller]")] public class TargetsController : BaseController
[ApiController]
public class TargetsController : ControllerBase
{ {
private readonly ITargetService _targetService; private readonly ITargetService _targetService;
@ -33,14 +31,14 @@ namespace Surge365.MassEmailReact.Server.Controllers
return target is not null ? Ok(target) : NotFound($"Target with key '{id}' not found."); return target is not null ? Ok(target) : NotFound($"Target with key '{id}' not found.");
} }
[HttpPost()] [HttpPost()]
public async Task<IActionResult> CreateTarget(int id, [FromBody] TargetUpdateDto targetUpdateDto) public async Task<IActionResult> CreateTarget([FromBody] TargetUpdateDto targetUpdateDto)
{ {
if (targetUpdateDto.Id != null && targetUpdateDto.Id > 0) if (targetUpdateDto.Id != null && targetUpdateDto.Id > 0)
return BadRequest("Id must be null or 0"); return BadRequest("Id must be null or 0");
var targetId = await _targetService.CreateAsync(targetUpdateDto); var targetId = await _targetService.CreateAsync(targetUpdateDto);
if (targetId == null) if (targetId == null)
return StatusCode(StatusCodes.Status500InternalServerError, "Failed to craete target."); return StatusCode(StatusCodes.Status500InternalServerError, "Failed to create target.");
var createdTarget = await _targetService.GetByIdAsync(targetId.Value); var createdTarget = await _targetService.GetByIdAsync(targetId.Value);

View File

@ -3,11 +3,9 @@ using Microsoft.AspNetCore.Mvc;
using Surge365.MassEmailReact.Application.Interfaces; using Surge365.MassEmailReact.Application.Interfaces;
using Surge365.MassEmailReact.Domain.Entities; using Surge365.MassEmailReact.Domain.Entities;
namespace Surge365.MassEmailReact.Server.Controllers namespace Surge365.MassEmailReact.API.Controllers
{ {
[Route("api/[controller]")] public class TemplatesController : BaseController
[ApiController]
public class TemplatesController : ControllerBase
{ {
private readonly ITemplateService _templateService; private readonly ITemplateService _templateService;

View File

@ -3,11 +3,9 @@ using Microsoft.AspNetCore.Mvc;
using Surge365.MassEmailReact.Application.Interfaces; using Surge365.MassEmailReact.Application.Interfaces;
using Surge365.MassEmailReact.Domain.Entities; using Surge365.MassEmailReact.Domain.Entities;
namespace Surge365.MassEmailReact.Server.Controllers namespace Surge365.MassEmailReact.API.Controllers
{ {
[Route("api/[controller]")] public class TestEmailListsController : BaseController
[ApiController]
public class TestEmailListsController : ControllerBase
{ {
private readonly ITestEmailListService _testEmailListService; private readonly ITestEmailListService _testEmailListService;

View File

@ -4,11 +4,9 @@ using Surge365.MassEmailReact.Application.DTOs;
using Surge365.MassEmailReact.Application.Interfaces; using Surge365.MassEmailReact.Application.Interfaces;
using Surge365.MassEmailReact.Domain.Entities; using Surge365.MassEmailReact.Domain.Entities;
namespace Surge365.MassEmailReact.Server.Controllers namespace Surge365.MassEmailReact.API.Controllers
{ {
[Route("api/[controller]")] public class UnsubscribeUrlsController : BaseController
[ApiController]
public class UnsubscribeUrlsController : ControllerBase
{ {
private readonly IUnsubscribeUrlService _unsubscribeUrlService; private readonly IUnsubscribeUrlService _unsubscribeUrlService;
@ -31,7 +29,7 @@ namespace Surge365.MassEmailReact.Server.Controllers
return unsubscribeUrl is not null ? Ok(unsubscribeUrl) : NotFound($"UnsubscribeUrl with key '{id}' not found."); return unsubscribeUrl is not null ? Ok(unsubscribeUrl) : NotFound($"UnsubscribeUrl with key '{id}' not found.");
} }
[HttpPost()] [HttpPost()]
public async Task<IActionResult> Create(int id, [FromBody] UnsubscribeUrlUpdateDto unsubscribeUrlUpdateDto) public async Task<IActionResult> Create([FromBody] UnsubscribeUrlUpdateDto unsubscribeUrlUpdateDto)
{ {
if (unsubscribeUrlUpdateDto.Id != null && unsubscribeUrlUpdateDto.Id > 0) if (unsubscribeUrlUpdateDto.Id != null && unsubscribeUrlUpdateDto.Id > 0)
return BadRequest("Id must be null or 0"); return BadRequest("Id must be null or 0");

View File

@ -46,7 +46,6 @@ app.UseAuthorization();
app.MapControllers(); app.MapControllers();
app.MapFallbackToFile("/index.html");
DapperConfiguration.ConfigureMappings(); DapperConfiguration.ConfigureMappings();

View File

@ -1,6 +1,11 @@
@Surge365.MassEmailReact.Server_HostAddress = http://localhost:5065 @Surge365.MassEmailReact.API_HostAddress = http://localhost:5065/api
@Surge365.MassEmailReact.UATServer_HostAddress = https://uat.massemail2.surge365.com/api
GET {{Surge365.MassEmailReact.Server_HostAddress}}/weatherforecast/ GET {{Surge365.MassEmailReact.API_HostAddress}}/servers/
Accept: application/json
###
GET {{Surge365.MassEmailReact.UATServer_HostAddress}}/servers/get
Accept: application/json Accept: application/json
### ###

View File

@ -11,6 +11,7 @@ namespace Surge365.MassEmailReact.Application.Interfaces
{ {
Task<Server?> GetByIdAsync(int id, bool returnPassword = false); Task<Server?> GetByIdAsync(int id, bool returnPassword = false);
Task<List<Server>> GetAllAsync(bool activeOnly = true, bool returnPassword = false); Task<List<Server>> GetAllAsync(bool activeOnly = true, bool returnPassword = false);
Task<int?> CreateAsync(Server server);
Task<bool> UpdateAsync(Server server); Task<bool> UpdateAsync(Server server);
} }
} }

View File

@ -6,6 +6,7 @@ namespace Surge365.MassEmailReact.Application.Interfaces
{ {
Task<Server?> GetByIdAsync(int id, bool returnPassword = false); Task<Server?> GetByIdAsync(int id, bool returnPassword = false);
Task<List<Server>> GetAllAsync(bool activeOnly = true, bool returnPassword = false); Task<List<Server>> GetAllAsync(bool activeOnly = true, bool returnPassword = false);
Task<int?> CreateAsync(ServerUpdateDto targetDto);
Task<bool> UpdateAsync(ServerUpdateDto serverDto); Task<bool> UpdateAsync(ServerUpdateDto serverDto);
} }
} }

View File

@ -66,6 +66,35 @@ namespace Surge365.MassEmailReact.Infrastructure.Repositories
return servers; return servers;
} }
public async Task<int?> CreateAsync(Server server)
{
ArgumentNullException.ThrowIfNull(server);
if (server.Id != null && server.Id > 0)
throw new Exception("ID must be null");
using SqlConnection conn = new SqlConnection(_config.GetConnectionString(_connectionStringName));
var parameters = new DynamicParameters();
parameters.Add("@server_key", dbType: DbType.Int32, direction: ParameterDirection.Output);
parameters.Add("@name", server.Name, DbType.String);
parameters.Add("@server_name", server.ServerName, DbType.String);
parameters.Add("@port", server.Port, DbType.Int16);
parameters.Add("@username", server.Username, DbType.String);
parameters.Add("@password", server.Password, DbType.String);
// Output parameter
parameters.Add("@success", dbType: DbType.Boolean, direction: ParameterDirection.Output);
await conn.ExecuteAsync("mem_save_server", parameters, commandType: CommandType.StoredProcedure);
// Retrieve the output parameter value
bool success = parameters.Get<bool>("@success");
if (success)
return parameters.Get<int>("@server_key");
return null;
}
public async Task<bool> UpdateAsync(Server server) public async Task<bool> UpdateAsync(Server server)
{ {
ArgumentNullException.ThrowIfNull(server); ArgumentNullException.ThrowIfNull(server);

View File

@ -10,6 +10,7 @@ using Microsoft.IdentityModel.Tokens;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
using Surge365.MassEmailReact.Domain.Entities; using Surge365.MassEmailReact.Domain.Entities;
using System.Security.Cryptography; using System.Security.Cryptography;
using Surge365.MassEmailReact.Infrastructure.Repositories;
namespace Surge365.MassEmailReact.Infrastructure.Services namespace Surge365.MassEmailReact.Infrastructure.Services
@ -33,6 +34,22 @@ namespace Surge365.MassEmailReact.Infrastructure.Services
{ {
return await _serverRepository.GetAllAsync(activeOnly, returnPassword); return await _serverRepository.GetAllAsync(activeOnly, returnPassword);
} }
public async Task<int?> CreateAsync(ServerUpdateDto serverDto)
{
ArgumentNullException.ThrowIfNull(serverDto, nameof(serverDto));
if (serverDto.Id != null && serverDto.Id > 0)
throw new Exception("ID must be null");
var server = new Server();
server.Name = serverDto.Name;
server.ServerName = serverDto.ServerName;
server.Port = serverDto.Port;
server.Username = serverDto.Username;
server.Password = serverDto.Password;
return await _serverRepository.CreateAsync(server);
}
public async Task<bool> UpdateAsync(ServerUpdateDto serverDto) public async Task<bool> UpdateAsync(ServerUpdateDto serverDto)
{ {
ArgumentNullException.ThrowIfNull(serverDto, nameof(serverDto)); ArgumentNullException.ThrowIfNull(serverDto, nameof(serverDto));

View File

@ -9,7 +9,9 @@
<BuildOutputFolder>$(MSBuildProjectDirectory)\dist</BuildOutputFolder> <BuildOutputFolder>$(MSBuildProjectDirectory)\dist</BuildOutputFolder>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<None Remove="dist\**" />
<None Remove="public\content\lib\**" /> <None Remove="public\content\lib\**" />
<TypeScriptConfiguration Remove="dist\**" />
<TypeScriptConfiguration Remove="public\content\lib\**" /> <TypeScriptConfiguration Remove="public\content\lib\**" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@ -20,8 +20,6 @@
"admin-lte": "4.0.0-beta3", "admin-lte": "4.0.0-beta3",
"bootstrap": "^5.3.3", "bootstrap": "^5.3.3",
"dayjs": "^1.11.13", "dayjs": "^1.11.13",
"font-awesome": "^4.7.0",
"ionicons": "^7.4.0",
"react": "^19.0.0", "react": "^19.0.0",
"react-bootstrap": "^2.10.9", "react-bootstrap": "^2.10.9",
"react-dom": "^19.0.0", "react-dom": "^19.0.0",
@ -2035,19 +2033,6 @@
"integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==", "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/@stencil/core": {
"version": "4.26.0",
"resolved": "https://registry.npmjs.org/@stencil/core/-/core-4.26.0.tgz",
"integrity": "sha512-+0Inu+dJ9/LgWSskcZwx7v17v4GILcwIYxNgD+OuK0U+D5z61WsxWw7yHkYG5OqGPBijsJMVssYRx/Tn+e7F9A==",
"license": "MIT",
"bin": {
"stencil": "bin/stencil"
},
"engines": {
"node": ">=16.0.0",
"npm": ">=7.10.0"
}
},
"node_modules/@swc/helpers": { "node_modules/@swc/helpers": {
"version": "0.5.15", "version": "0.5.15",
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz",
@ -3345,15 +3330,6 @@
"dev": true, "dev": true,
"license": "ISC" "license": "ISC"
}, },
"node_modules/font-awesome": {
"version": "4.7.0",
"resolved": "https://registry.npmjs.org/font-awesome/-/font-awesome-4.7.0.tgz",
"integrity": "sha512-U6kGnykA/6bFmg1M/oT9EkFeIYv7JlX3bozwQJWiiLz6L0w3F5vBVPxHlwyX/vtNq1ckcpRKOB9f2Qal/VtFpg==",
"license": "(OFL-1.1 AND MIT)",
"engines": {
"node": ">=0.10.3"
}
},
"node_modules/fsevents": { "node_modules/fsevents": {
"version": "2.3.3", "version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
@ -3506,15 +3482,6 @@
"loose-envify": "^1.0.0" "loose-envify": "^1.0.0"
} }
}, },
"node_modules/ionicons": {
"version": "7.4.0",
"resolved": "https://registry.npmjs.org/ionicons/-/ionicons-7.4.0.tgz",
"integrity": "sha512-ZK94MMqgzMCPPMhmk8Ouu6goyVHFIlw/ACP6oe3FrikcI0N7CX0xcwVaEbUc0G/v3W0shI93vo+9ve/KpvcNhQ==",
"license": "MIT",
"dependencies": {
"@stencil/core": "^4.0.3"
}
},
"node_modules/is-arrayish": { "node_modules/is-arrayish": {
"version": "0.2.1", "version": "0.2.1",
"resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz",

View File

@ -23,8 +23,6 @@
"admin-lte": "4.0.0-beta3", "admin-lte": "4.0.0-beta3",
"bootstrap": "^5.3.3", "bootstrap": "^5.3.3",
"dayjs": "^1.11.13", "dayjs": "^1.11.13",
"font-awesome": "^4.7.0",
"ionicons": "^7.4.0",
"react": "^19.0.0", "react": "^19.0.0",
"react-bootstrap": "^2.10.9", "react-bootstrap": "^2.10.9",
"react-dom": "^19.0.0", "react-dom": "^19.0.0",

View File

@ -228,7 +228,23 @@ const Layout = ({ children }: LayoutProps) => {
{ text: 'Completed Mailings', icon: <CheckCircleIcon />, path: '/completedMailings' }, { text: 'Completed Mailings', icon: <CheckCircleIcon />, path: '/completedMailings' },
].map((item) => ( ].map((item) => (
<ListItem key={item.text} disablePadding> <ListItem key={item.text} disablePadding>
<ListItemButton component={RouterLink} to={item.path} onClick={() => isMobile && handleDrawerClose()} <ListItemButton
component={RouterLink}
to={item.path}
selected={location.pathname === item.path}
onClick={() => isMobile && handleDrawerClose()}
sx={{
'&.Mui-selected': {
backgroundColor: 'primary.main',
color: 'primary.contrastText',
'& .MuiListItemIcon-root': {
color: 'primary.contrastText',
},
},
'&.Mui-selected:hover': {
backgroundColor: 'primary.dark',
},
}}
> >
<ListItemIcon>{item.icon}</ListItemIcon> <ListItemIcon>{item.icon}</ListItemIcon>
<ListItemText primary={item.text} /> <ListItemText primary={item.text} />

View File

@ -1,29 +1,13 @@
import { ReactNode } from 'react'; import { ReactNode } from 'react';
//import { useEffect } from 'react';
import PropTypes from 'prop-types'; 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 '@/css/adminlte-custom.css';
import '@/css/surge365.css'; import '@/css/surge365.css';
import 'bootstrap/dist/js/bootstrap.bundle.min.js'; // Bootstrap JS
import 'admin-lte/dist/js/adminlte.min.js';
import 'admin-lte/dist/js/adminlte.min.js';
const LayoutLogin = function LayoutLogin({ children }: { children: ReactNode }) { const LayoutLogin = function LayoutLogin({ children }: { children: ReactNode }) {
return children; return children;
} };
LayoutLogin.propTypes = { LayoutLogin.propTypes = {
children: PropTypes.any children: PropTypes.any,
}; };
export default LayoutLogin; export default LayoutLogin;

View File

@ -1,37 +0,0 @@
import { ReactNode } from 'react';
//import { useEffect } from 'react';
import { Helmet, HelmetProvider } from 'react-helmet-async';
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 '@/css/adminlte-custom.css';
import '@/css/surge365.css';
import 'bootstrap/dist/js/bootstrap.bundle.min.js'; // Bootstrap JS
import 'admin-lte/dist/js/adminlte.min.js';
import 'admin-lte/dist/js/adminlte.min.js';
const LayoutLogin = function LayoutLogin({ children }: { children: ReactNode }) {
return (
<HelmetProvider>
<Helmet>
</Helmet>
{children}
</HelmetProvider>
);
}
LayoutLogin.propTypes = {
children: PropTypes.any
};
export default LayoutLogin;

View File

@ -1,120 +0,0 @@
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 &copy; 2024 <a href="https://www.surge365.com">Surge365</a>.</strong> All rights reserved.
</footer>
</div>
);
}
Layout.propTypes = {
children: PropTypes.any
};
export default Layout;

View File

@ -1,125 +0,0 @@
import { ReactNode } from 'react';
//import React from 'react';
import { Helmet, HelmetProvider } from 'react-helmet-async';
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 (
<HelmetProvider>
<div className="wrapper" style={{ overflow: 'initial' }}>
<Helmet>
<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>*/}
</Helmet>
<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 &copy; 2024 <a href="https://www.surge365.com">Surge365</a>.</strong> All rights reserved.
</footer>
</div>
</HelmetProvider>
);
}
Layout.propTypes = {
children: PropTypes.any
};
export default Layout;

View File

@ -23,7 +23,7 @@ type EmailDomainEditProps = {
}; };
const schema = yup.object().shape({ const schema = yup.object().shape({
id: yup.number(), id: yup.number().default(0),
name: yup name: yup
.string() .string()
.required("Name is required") .required("Name is required")
@ -37,7 +37,7 @@ const schema = yup.object().shape({
emailAddress: yup.string().email("Invalid email").required("Email address is required"), emailAddress: yup.string().email("Invalid email").required("Email address is required"),
username: yup.string().required("Username is required"), username: yup.string().required("Username is required"),
password: yup.string().default("") password: yup.string().default("")
.test("required-if-new", "NamePassword is required", function (value) { .test("required-if-new", "Password is required", function (value) {
if (this.parent.id > 0) return true; if (this.parent.id > 0) return true;
else return value.length > 0; else return value.length > 0;
}), }),
@ -60,7 +60,7 @@ const EmailDomainEdit = ({ open, emailDomain, onClose, onSave }: EmailDomainEdit
const setupData: SetupData = useSetupData(); const setupData: SetupData = useSetupData();
const originalEmailDomain: EmailDomain | null = emailDomain ? { ...emailDomain } : null; const originalEmailDomain: EmailDomain | null = emailDomain ? { ...emailDomain } : null;
const { register, trigger, control, handleSubmit, reset, formState: { errors } } = useForm<EmailDomain>({ const { register, control, handleSubmit, reset, formState: { errors } } = useForm<EmailDomain>({
mode: "onBlur", mode: "onBlur",
defaultValues: emailDomain || defaultEmailDomain, defaultValues: emailDomain || defaultEmailDomain,
resolver: yupResolver(schema) as Resolver<EmailDomain>, resolver: yupResolver(schema) as Resolver<EmailDomain>,
@ -89,7 +89,7 @@ const EmailDomainEdit = ({ open, emailDomain, onClose, onSave }: EmailDomainEdit
if (!response.ok) throw new Error(isNew ? "Failed to create" : "Failed to update"); if (!response.ok) throw new Error(isNew ? "Failed to create" : "Failed to update");
const updatedEmailDomain = await response.json(); const updatedEmailDomain = await response.json();
onSave(updatedEmailDomain, isNew || formData.password ? formData.password : originalEmailDomain?.password); onSave(updatedEmailDomain, isNew || formData.password ? formData.password : originalEmailDomain?.password ?? "");
onClose(); onClose();
} catch (error) { } catch (error) {
console.error("Update error:", error); console.error("Update error:", error);

View File

@ -1,6 +1,6 @@
import { useState, FormEvent } from 'react'; import { useState, FormEvent } from 'react';
import { Modal, Button, Form } from 'react-bootstrap'; import { Dialog, DialogTitle, DialogContent, DialogActions, Button, TextField, Typography, Box, IconButton } from '@mui/material';
import { FaExclamationCircle } from 'react-icons/fa'; // For optional font icon import { Close as CloseIcon } from '@mui/icons-material';
import utils from '@/ts/utils'; import utils from '@/ts/utils';
type FormErrors = Record<string, string>; type FormErrors = Record<string, string>;
@ -25,9 +25,8 @@ const ForgotPasswordModal: React.FC<ForgotPasswordModalProps> = ({ show, onClose
if (Object.keys(errors).length > 0) { if (Object.keys(errors).length > 0) {
setFormErrors(errors); setFormErrors(errors);
return false; return false;
} else {
return true;
} }
return true;
}; };
const handleStartPasswordRecovery = async (e: FormEvent) => { const handleStartPasswordRecovery = async (e: FormEvent) => {
@ -52,48 +51,51 @@ const ForgotPasswordModal: React.FC<ForgotPasswordModalProps> = ({ show, onClose
}; };
return ( return (
<Modal show={show} onHide={onClose} backdrop="static" keyboard={false} centered animation={false}> <Dialog open={show} onClose={() => { }}>
<Modal.Header closeButton> <DialogTitle>
<Modal.Title>Forgot your password?</Modal.Title> Forgot your password?
</Modal.Header> <IconButton onClick={onClose} sx={{ position: 'absolute', right: 8, top: 8 }}>
<Modal.Body> <CloseIcon />
</IconButton>
</DialogTitle>
<DialogContent>
{usernameNotFound && ( {usernameNotFound && (
<span>An email has been sent to the address you provided. Please follow the instructions to reset your password.</span> <Typography color="error" sx={{ mb: 2 }}>
An email has been sent to the address you provided. Please follow the instructions to reset your password.
</Typography>
)} )}
{!recoveryStarted && ( {!recoveryStarted && (
<Form onSubmit={handleStartPasswordRecovery}> <form onSubmit={handleStartPasswordRecovery}>
<Form.Group controlId="formForgotEmail" className="position-relative mb-3"> <Box sx={{ mb: 2 }}>
<Form.Label className="mb-4 text-center">Enter your email address below and we'll send you instructions on how to reset your password...</Form.Label> <Typography variant="body1" sx={{ mb: 2, textAlign: 'center' }}>
<Form.Label className="visually-hidden">Email</Form.Label> Enter your username below and we'll send you instructions on how to reset your password...
<Form.Control </Typography>
type="username" <TextField
placeholder="Username" label="Username"
variant="outlined"
fullWidth
value={username} value={username}
isInvalid={!!formErrors.username}
onChange={(e) => setUsername(e.target.value)} onChange={(e) => setUsername(e.target.value)}
error={!!formErrors.username}
helperText={formErrors.username}
required required
autoFocus autoFocus
size="lg" size="small"
/> />
{formErrors.username && ( </Box>
<FaExclamationCircle className="validation-icon text-danger" title={formErrors.username} /> <Button type="submit" variant="contained" color="primary" fullWidth>
)}
<Form.Control.Feedback type="invalid">{formErrors.username}</Form.Control.Feedback>
</Form.Group>
<Button variant="primary" className="bg-orange btn-flat w-100" type="submit">
Submit Submit
</Button> </Button>
</Form> </form>
)} )}
</Modal.Body> </DialogContent>
<Modal.Footer> <DialogActions>
<Button variant="secondary" onClick={onClose}> <Button onClick={onClose} color="secondary">
Close Close
</Button> </Button>
</Modal.Footer> </DialogActions>
</Modal> </Dialog>
); );
}; };
export default ForgotPasswordModal; export default ForgotPasswordModal;

View File

@ -9,35 +9,72 @@ import {
} from "@mui/material"; } from "@mui/material";
import Server from "@/types/server"; import Server from "@/types/server";
import { useForm } from "react-hook-form";
import { yupResolver } from "@hookform/resolvers/yup";
import * as yup from "yup";
type ServerEditProps = { type ServerEditProps = {
open: boolean; open: boolean;
server: Server; server: Server | null;
onClose: () => void; onClose: () => void;
onSave: (updatedServer: Server) => void; onSave: (updatedServer: Server, password: string) => void;
}; };
const schema = yup.object().shape({
id: yup.number().default(0),
name: yup
.string()
.required("Name is required")
.test("unique-name", "Name must be unique", function (value) {
const setupData = this.options.context?.setupData as { servers: Server[] };
if (!setupData) return true;
return !setupData.servers.some(
(d) => d.name.toLowerCase() === value?.toLowerCase() && (d.id === 0 || d.id !== this.parent.id)
);
}),
serverName: yup.string().required("Server name is required"),
port: yup.number().default(1433),
username: yup.string().required("Username is required"),
password: yup.string().default("")
.test("required-if-new", "Password is required", function (value) {
if (this.parent.id > 0) return true;
else return value.length > 0;
})
});
const defaultServer: Server = {
id: 0,
name: "",
serverName: "",
port: 1433,
username: "",
password: "",
};
const ServerEdit = ({ open, server, onClose, onSave }: ServerEditProps) => { const ServerEdit = ({ open, server, onClose, onSave }: ServerEditProps) => {
const [formData, setFormData] = useState<Server>({ ...server }); const isNew = !server || server.id === 0;
//const [serverError, setServerError] = useState(false); // Track validation const originalServer: Server | null = server ? { ...server } : null;
//const [formData, setFormData] = useState<Server>({ ...server });
const { register, handleSubmit, reset, formState: { errors } } = useForm<Server>({
mode: "onBlur",
defaultValues: server || defaultServer,
resolver: yupResolver(schema)
});
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
useEffect(() => { //Reset form to unedited state on open or server change useEffect(() => {
if (open) { if (open) {
setFormData(server); reset(server || defaultServer, { keepDefaultValues: true });
} }
}, [open, server]); }, [open, server, reset]);
const handleChange = (field: string, value: any) => {
setFormData((prev) => ({ ...prev, [field]: value || "" }));
//if (field === "serverId" && value) setServerError(false); const handleSave = async (formData: Server) => {
}; const apiUrl = isNew ? "/api/servers" : `/api/servers/${formData.id}`;
const method = isNew ? "POST" : "PUT";
const handleSave = async () => {
setLoading(true); setLoading(true);
try { try {
const response = await fetch(`/api/servers/${formData.id}`, { const response = await fetch(apiUrl, {
method: "PUT", method: method,
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
body: JSON.stringify(formData), body: JSON.stringify(formData),
}); });
@ -45,7 +82,7 @@ const ServerEdit = ({ open, server, onClose, onSave }: ServerEditProps) => {
if (!response.ok) throw new Error("Failed to update"); if (!response.ok) throw new Error("Failed to update");
const updatedServer = await response.json(); const updatedServer = await response.json();
onSave(updatedServer); // Update UI optimistically onSave(updatedServer, isNew || formData.password ? formData.password : originalServer?.password ?? "");
onClose(); onClose();
} catch (error) { } catch (error) {
console.error("Update error:", error); console.error("Update error:", error);
@ -56,47 +93,52 @@ const ServerEdit = ({ open, server, onClose, onSave }: ServerEditProps) => {
return ( return (
<Dialog open={open} onClose={onClose} maxWidth="sm" fullWidth> <Dialog open={open} onClose={onClose} maxWidth="sm" fullWidth>
<DialogTitle>Edit Server - id={formData.id}</DialogTitle> <DialogTitle>{isNew ? "Add Server" : "Edit Server id=" + server?.id}</DialogTitle>
<DialogContent> <DialogContent>
<TextField <TextField
{...register("name")}
label="Name" label="Name"
fullWidth fullWidth
value={formData.name}
onChange={(e) => handleChange("name", e.target.value)}
margin="dense" margin="dense"
error={!!errors.name}
helperText={errors.name?.message}
/> />
<TextField <TextField
{...register("serverName")}
label="Server Name" label="Server Name"
fullWidth fullWidth
value={formData.serverName}
onChange={(e) => handleChange("serverName", e.target.value)}
margin="dense" margin="dense"
error={!!errors.serverName}
helperText={errors.serverName?.message}
/> />
<TextField <TextField
{...register("port")}
label="Port" label="Port"
fullWidth fullWidth
value={formData.port}
onChange={(e) => handleChange("port", e.target.value)}
margin="dense" margin="dense"
error={!!errors.port}
helperText={errors.port?.message}
/> />
<TextField <TextField
{...register("username")}
label="Username" label="Username"
fullWidth fullWidth
value={formData.username}
onChange={(e) => handleChange("username", e.target.value)}
margin="dense" margin="dense"
error={!!errors.username}
helperText={errors.username?.message}
/> />
<TextField <TextField
{...register("password")}
label="Password" label="Password"
fullWidth fullWidth
value={formData.password}
onChange={(e) => handleChange("password", e.target.value)}
margin="dense" margin="dense"
error={!!errors.password}
helperText={errors.password?.message}
/> />
</DialogContent> </DialogContent>
<DialogActions> <DialogActions>
<Button onClick={onClose} disabled={loading}>Cancel</Button> <Button onClick={onClose} disabled={loading}>Cancel</Button>
<Button onClick={handleSave} color="primary" disabled={loading}> <Button onClick={handleSubmit(handleSave)} color="primary" disabled={loading}>
{loading ? "Saving..." : "Save"} {loading ? "Saving..." : "Save"}
</Button> </Button>
</DialogActions> </DialogActions>

View File

@ -94,8 +94,8 @@ const TemplateEdit = ({ open, template, onClose, onSave }: TemplateEditProps) =>
const handleSave = async (formData: Template) => { const handleSave = async (formData: Template) => {
const domain = setupData.emailDomains.find(el => el.id === formData.domainId); const domain = setupData.emailDomains.find(el => el.id === formData.domainId);
formData.fromEmail = domain.emailAddress; formData.fromEmail = domain?.emailAddress ?? "";
formData.replyToEmail = domain.emailAddress; formData.replyToEmail = domain?.emailAddress ?? "";
const apiUrl = isNew ? "/api/templates" : `/api/templates/${formData.id}`; const apiUrl = isNew ? "/api/templates" : `/api/templates/${formData.id}`;
const method = isNew ? "POST" : "PUT"; const method = isNew ? "POST" : "PUT";
setLoading(true); setLoading(true);

View File

@ -1,7 +1,9 @@
import { useState, useRef } from 'react'; import { useState, useRef } from 'react';
import { useSetupData, SetupData } from "@/context/SetupDataContext"; import { useSetupData, SetupData } from "@/context/SetupDataContext";
import EditIcon from '@mui/icons-material/Edit'; import EditIcon from '@mui/icons-material/Edit';
import { List, Card, CardContent, Typography, Box, useTheme, useMediaQuery, Button, CircularProgress, IconButton } from '@mui/material'; import AddIcon from '@mui/icons-material/Add';
import RefreshIcon from '@mui/icons-material/Refresh';
import { List, Card, CardContent, Typography, Box, useTheme, useMediaQuery, CircularProgress, IconButton } from '@mui/material';
import { DataGrid, GridColDef, GridRenderCellParams, GridRowModel, GridToolbarContainer, GridToolbarQuickFilter, GridToolbarExport, GridToolbarDensitySelector, GridToolbarColumnsButton, GridDeleteIcon } from '@mui/x-data-grid'; import { DataGrid, GridColDef, GridRenderCellParams, GridRowModel, GridToolbarContainer, GridToolbarQuickFilter, GridToolbarExport, GridToolbarDensitySelector, GridToolbarColumnsButton, GridDeleteIcon } from '@mui/x-data-grid';
import BouncedEmail from '@/types/bouncedEmail'; import BouncedEmail from '@/types/bouncedEmail';
import BouncedEmailEdit from "@/components/modals/BouncedEmailEdit"; import BouncedEmailEdit from "@/components/modals/BouncedEmailEdit";
@ -22,10 +24,10 @@ function BouncedEmails() {
sortable: false, sortable: false,
renderCell: (params: GridRenderCellParams<BouncedEmail>) => ( renderCell: (params: GridRenderCellParams<BouncedEmail>) => (
<div> <div>
<IconButton onClick={(e) => { e.stopPropagation(); handleEdit(params.row); }}> <IconButton color="primary" onClick={(e) => { e.stopPropagation(); handleEdit(params.row); }}>
<EditIcon /> <EditIcon />
</IconButton> </IconButton>
<IconButton onClick={(e) => { e.stopPropagation(); handleDelete(params.row); }}> <IconButton color="primary" onClick={(e) => { e.stopPropagation(); handleDelete(params.row); }}>
<GridDeleteIcon /> <GridDeleteIcon />
</IconButton> </IconButton>
</div> </div>
@ -111,22 +113,12 @@ function BouncedEmails() {
slots={{ slots={{
toolbar: () => ( toolbar: () => (
<GridToolbarContainer sx={{ display: "flex", alignItems: "center" }}> <GridToolbarContainer sx={{ display: "flex", alignItems: "center" }}>
<Button <IconButton size="small" color="primary" onClick={handleNew} sx={{ marginLeft: 1 }}>
variant="contained" <AddIcon />
color="primary" </IconButton>
onClick={() => handleNew()} <IconButton size="small" color="primary" onClick={() => setupData.reloadBouncedEmails()} sx={{ marginLeft: 1 }}>
sx={{ marginRight: 2 }} {setupData.bouncedEmailsLoading ? <CircularProgress size={24} color="inherit" /> : <RefreshIcon />}
> </IconButton>
{setupData.bouncedEmailsLoading ? <CircularProgress size={24} color="inherit" /> : "Add New"}
</Button>
<Button
variant="contained"
color="primary"
onClick={() => setupData.reloadBouncedEmails()}
sx={{ marginRight: 2 }}
>
{setupData.bouncedEmailsLoading ? <CircularProgress size={24} color="inherit" /> : "Refresh"}
</Button>
<GridToolbarColumnsButton /> <GridToolbarColumnsButton />
<GridToolbarDensitySelector /> <GridToolbarDensitySelector />
<GridToolbarExport /> <GridToolbarExport />

View File

@ -1,6 +1,8 @@
import { useState, useRef } from 'react'; import { useState, useRef } from 'react';
import { useSetupData, SetupData } from "@/context/SetupDataContext"; import { useSetupData, SetupData } from "@/context/SetupDataContext";
import EditIcon from '@mui/icons-material/Edit'; import EditIcon from '@mui/icons-material/Edit';
import AddIcon from '@mui/icons-material/Add';
import RefreshIcon from '@mui/icons-material/Refresh';
import { Lock, LockOpen } from "@mui/icons-material"; import { Lock, LockOpen } from "@mui/icons-material";
import { List, Card, CardContent, Typography, Box, useTheme, useMediaQuery, Button, CircularProgress, IconButton } from '@mui/material'; import { List, Card, CardContent, Typography, Box, useTheme, useMediaQuery, Button, CircularProgress, IconButton } from '@mui/material';
import { DataGrid, GridColDef, GridRenderCellParams, GridRowModel, GridToolbarContainer, GridToolbarQuickFilter, GridToolbarExport, GridToolbarDensitySelector, GridToolbarColumnsButton } from '@mui/x-data-grid'; import { DataGrid, GridColDef, GridRenderCellParams, GridRowModel, GridToolbarContainer, GridToolbarQuickFilter, GridToolbarExport, GridToolbarDensitySelector, GridToolbarColumnsButton } from '@mui/x-data-grid';
@ -21,12 +23,13 @@ function EmailDomains() {
const columns: GridColDef<EmailDomain>[] = [ const columns: GridColDef<EmailDomain>[] = [
{ {
field: "actions", field: "actions",
headerName: "Actions", headerName: "",
sortable: false, sortable: false,
width: 60,
renderCell: (params: GridRenderCellParams<EmailDomain>) => ( renderCell: (params: GridRenderCellParams<EmailDomain>) => (
<Button variant="contained" color="primary" size="small" onClick={() => handleEdit(params.row)}> <IconButton color="primary" onClick={(e) => { e.stopPropagation(); handleEdit(params.row); }}>
Edit <EditIcon />
</Button> </IconButton>
), ),
}, },
{ field: "id", headerName: "ID", width: 60 }, { field: "id", headerName: "ID", width: 60 },
@ -40,7 +43,7 @@ function EmailDomains() {
renderHeader: () => ( renderHeader: () => (
<div style={{ display: "flex", alignItems: "center" }}> <div style={{ display: "flex", alignItems: "center" }}>
Password Password
<IconButton size="small" onClick={(e) => { e.stopPropagation(); togglePasswordVisibility(); }} sx={{ marginLeft: 1 }}> <IconButton color="primary" size="small" onClick={(e) => { e.stopPropagation(); togglePasswordVisibility(); }} sx={{ marginLeft: 1 }}>
{isPasswordVisible ? <LockOpen /> : <Lock />} {isPasswordVisible ? <LockOpen /> : <Lock />}
</IconButton> </IconButton>
</div> </div>
@ -100,7 +103,7 @@ function EmailDomains() {
: [...prev, updatedEmailDomain]; : [...prev, updatedEmailDomain];
}); });
}; };
const displayRows = isPasswordVisible ? (emailDomainsWithPasswords ?? setupData.emailDomains) : setupData.emailDomains; const displayRows: EmailDomain[] = isPasswordVisible ? (emailDomainsWithPasswords ?? setupData.emailDomains) : setupData.emailDomains;
return ( return (
<Box ref={gridContainerRef} sx={{ <Box ref={gridContainerRef} sx={{
@ -133,7 +136,7 @@ function EmailDomains() {
<Typography variant="body2">Display Order: {row.displayOrder}</Typography> <Typography variant="body2">Display Order: {row.displayOrder}</Typography>
<Typography variant="body2">Active: {row.isActive ? "Yes" : "No"}</Typography> <Typography variant="body2">Active: {row.isActive ? "Yes" : "No"}</Typography>
</CardContent> </CardContent>
<IconButton onClick={(e) => { e.stopPropagation(); handleEdit(row); }}> <IconButton color="primary" onClick={(e) => { e.stopPropagation(); handleEdit(row); }}>
<EditIcon /> <EditIcon />
</IconButton> </IconButton>
</Box> </Box>
@ -150,26 +153,12 @@ function EmailDomains() {
slots={{ slots={{
toolbar: () => ( toolbar: () => (
<GridToolbarContainer sx={{ display: "flex", alignItems: "center" }}> <GridToolbarContainer sx={{ display: "flex", alignItems: "center" }}>
<Button <IconButton size="small" color="primary" onClick={handleNew} sx={{ marginLeft: 1 }}>
variant="contained" <AddIcon />
color="primary" </IconButton>
onClick={() => handleNew()} <IconButton size="small" color="primary" onClick={() => setupData.reloadEmailDomains()} sx={{ marginLeft: 1 }}>
sx={{ marginRight: 2 }} {setupData.emailDomainsLoading ? <CircularProgress size={24} color="inherit" /> : <RefreshIcon />}
> </IconButton>
Add New
</Button>
<Button
variant="contained"
color="primary"
onClick={() => {
if (isPasswordVisible)
loadEmailDomainsWithPasswords();
setupData.reloadEmailDomains();
}}
sx={{ marginRight: 2 }}
>
{setupData.emailDomainsLoading ? <CircularProgress size={24} color="inherit" /> : "Refresh"}
</Button>
<GridToolbarColumnsButton /> <GridToolbarColumnsButton />
<GridToolbarDensitySelector /> <GridToolbarDensitySelector />
<GridToolbarExport /> <GridToolbarExport />

View File

@ -1,10 +1,16 @@
import { useState } from 'react'; import { useState } from 'react';
import { Button, Form, Spinner } from 'react-bootstrap'; import {
Button,
TextField,
CircularProgress,
Container,
Typography,
Box,
Alert,
} from '@mui/material';
import { AuthResponse, AuthErrorResponse, User, isAuthErrorResponse } from '@/types/auth'; import { AuthResponse, AuthErrorResponse, User, isAuthErrorResponse } from '@/types/auth';
//import { Helmet, HelmetProvider } from 'react-helmet-async';
import utils from '@/ts/utils.ts'; import utils from '@/ts/utils.ts';
import ForgotPasswordModal from '@/components/modals/ForgotPasswordModal'; //import ForgotPasswordModal from '@/components/modals/ForgotPasswordModal';
type SpinnerState = Record<string, boolean>; type SpinnerState = Record<string, boolean>;
type FormErrors = Record<string, string>; type FormErrors = Record<string, string>;
@ -15,17 +21,10 @@ function Login() {
const [formErrors, setFormErrors] = useState<FormErrors>({}); const [formErrors, setFormErrors] = useState<FormErrors>({});
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<User | null>(null);
const [loginError, setLoginError] = useState<boolean>(false); const [loginError, setLoginError] = useState<boolean>(false);
const [loginErrorMessage, setLoginErrorMessage] = useState<string>(''); const [loginErrorMessage, setLoginErrorMessage] = useState<string>('');
//const setSpinners = (newValues: Partial<SpinnerState>) => {
// setSpinnersState((prevSpinners) => ({
// ...prevSpinners,
// ...newValues,
// }));
//};
const setSpinners = (newValues: Partial<SpinnerState>) => { const setSpinners = (newValues: Partial<SpinnerState>) => {
setSpinnersState((prevSpinners) => { setSpinnersState((prevSpinners) => {
const updatedSpinners: SpinnerState = { ...prevSpinners }; const updatedSpinners: SpinnerState = { ...prevSpinners };
@ -38,23 +37,19 @@ function Login() {
}); });
}; };
const handleCloseForgotPasswordModal = () => { //const handleCloseForgotPasswordModal = () => {
setShowForgotPasswordModal(false); // setShowForgotPasswordModal(false);
}; //};
const validateLoginForm = () => { const validateLoginForm = () => {
setFormErrors({}); setFormErrors({});
const errors: FormErrors = {}; const errors: FormErrors = {};
if (!username.trim()) { if (!username.trim()) {
errors.username = 'Username is required'; errors.username = 'Username is required';
//} else if (!/\S+@\S+\.\S+/.test(email)) {
// errors.email = 'Invalid email address';
} }
if (!password.trim()) { if (!password.trim()) {
errors.password = 'Password is required'; errors.password = 'Password is required';
} }
if (Object.keys(errors).length > 0) { if (Object.keys(errors).length > 0) {
setFormErrors(errors); setFormErrors(errors);
} }
@ -63,14 +58,15 @@ function Login() {
const handleLogin = async (e: React.FormEvent) => { const handleLogin = async (e: React.FormEvent) => {
e.preventDefault(); e.preventDefault();
setIsLoading(true); setIsLoading(true);
spinners.Login = true; setSpinners({ Login: true });
setSpinners(spinners);
validateLoginForm(); validateLoginForm();
if (Object.keys(formErrors).length > 0) {
setIsLoading(false);
setSpinners({ Login: false });
return;
}
if (Object.keys(formErrors).length > 0) return;
//setUser(null);
setLoginError(false); setLoginError(false);
setLoginErrorMessage(''); setLoginErrorMessage('');
let loggedInUser: User | null = null; let loggedInUser: User | null = null;
@ -83,45 +79,39 @@ function Login() {
success: (json: AuthResponse) => { success: (json: AuthResponse) => {
try { try {
loggedInUser = json.user; loggedInUser = json.user;
//setUser(loggedInUser); } catch {
} const errorMsg: string = 'Unexpected Error';
catch {
const errorMsg: string = "Unexpected Error";
hadLoginError = true; hadLoginError = true;
hadLoginErrorMessage = errorMsg; hadLoginErrorMessage = errorMsg;
} }
}, },
error: (err: unknown) => { error: (err: unknown) => {
let errorMsg: string = "Unexpected Error"; let errorMsg: string = 'Unexpected Error';
if (isAuthErrorResponse(err)) { if (isAuthErrorResponse(err)) {
if (err && err as AuthErrorResponse) { const errorResponse = err as AuthErrorResponse;
if (err.data) { if (errorResponse.data?.message) {
if (err.data.message) errorMsg = errorResponse.data.message;
errorMsg = err.data.message;
}
console.error(errorMsg);
setLoginErrorMessage(errorMsg);
} }
console.error(errorMsg);
setLoginErrorMessage(errorMsg);
} }
hadLoginError = true; hadLoginError = true;
hadLoginErrorMessage = errorMsg; hadLoginErrorMessage = errorMsg;
} },
}); });
if (hadLoginError) { if (hadLoginError) {
setLoginErrorMessage(hadLoginErrorMessage); setLoginErrorMessage(hadLoginErrorMessage);
setLoginError(true); setLoginError(true);
setIsLoading(false); setIsLoading(false);
spinners.Login = false; setSpinners({ Login: false });
setSpinners(spinners);
return; return;
} }
if (loggedInUser == null) { if (loggedInUser == null) {
setLoginError(true); setLoginError(true);
setIsLoading(false); setIsLoading(false);
spinners.Login = false; setSpinners({ Login: false });
setSpinners(spinners);
} else { } else {
await finishUserLogin(loggedInUser); await finishUserLogin(loggedInUser);
} }
@ -129,15 +119,13 @@ function Login() {
const finishUserLogin = async (loggedInUser: User) => { const finishUserLogin = async (loggedInUser: User) => {
setIsLoading(false); setIsLoading(false);
spinners.Login = false; setSpinners({ Login: false, LoginWithPasskey: false });
spinners.LoginWithPasskey = false;
setSpinners(spinners);
utils.localStorage("session_currentUser", loggedInUser); 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 = '/home'; document.location.href = '/home';
@ -145,54 +133,88 @@ function Login() {
}; };
return ( return (
<div className="container"> <Container maxWidth="sm">
<div className="row text-center mt-5"> {/* Main heading */}
<h1>surge365 - React</h1> <Box sx={{ textAlign: 'center', mt: 5 }}>
</div> <Typography variant="h4">Surge 365 Mass Email 2</Typography>
<div className="row text-center" style={{ maxWidth: '400px', margin: 'auto' }}> </Box>
<h3 className="form-signin-heading mt-3 mb-1">Please sign in</h3>
<Form id="frmLogin" onSubmit={handleLogin}> {/* Login form */}
<Box sx={{ maxWidth: 400, margin: 'auto', mt: 3 }}>
<Typography variant="h5" align="center">
Please sign in
</Typography>
<form onSubmit={handleLogin}>
{/* Login error message */}
{loginError && ( {loginError && (
<Form.Label style={{ color: 'red' }}>{loginErrorMessage ?? "Login error"}</Form.Label> <Alert severity="error" sx={{ mb: 2 }}>
{loginErrorMessage || 'Login error'}
</Alert>
)} )}
<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"> {/* Username field */}
<Form.Label className="visually-hidden">Password</Form.Label> <TextField
<Form.Control label="Username"
type="password" variant="outlined"
placeholder="Password" fullWidth
value={password} margin="normal"
onChange={(e) => setPassword(e.target.value)} value={username}
required onChange={(e) => setUsername(e.target.value)}
size="sm" error={!!formErrors.username}
/> helperText={formErrors.username}
</Form.Group> required
autoFocus
size="small"
/>
<Button className="bg-orange w-100" type="submit" disabled={isLoading}> {/* Password field */}
{spinners.Login && <Spinner animation="border" size="sm" className="me-2" />} <TextField
label="Password"
type="password"
variant="outlined"
fullWidth
margin="normal"
value={password}
onChange={(e) => setPassword(e.target.value)}
error={!!formErrors.password}
helperText={formErrors.password}
required
size="small"
/>
{/* Sign in button */}
<Button
type="submit"
variant="contained"
color="primary"
fullWidth
disabled={isLoading}
startIcon={
isLoading && spinners.Login ? <CircularProgress size={24} color="inherit" /> : null
}
sx={{ mt: 2 }}
>
{isLoading && spinners.Login ? 'Signing in...' : 'Sign in'} {isLoading && spinners.Login ? 'Signing in...' : 'Sign in'}
</Button> </Button>
<Button variant="secondary" className="w-100 mt-2" onClick={() => setShowForgotPasswordModal(true)}>
Forgot Password
</Button>
</Form>
</div>
<ForgotPasswordModal show={showForgotPasswordModal} onClose={handleCloseForgotPasswordModal} /> {/* Forgot password button */}
</div> {/*<Button*/}
{/* variant="outlined"*/}
{/* fullWidth*/}
{/* onClick={() => setShowForgotPasswordModal(true)}*/}
{/* sx={{ mt: 1 }}*/}
{/*>*/}
{/* Forgot Password*/}
{/*</Button>*/}
</form>
</Box>
{/* Forgot password modal */}
{/*<ForgotPasswordModal*/}
{/* show={showForgotPasswordModal}*/}
{/* onClose={handleCloseForgotPasswordModal}*/}
{/*/>*/}
</Container>
); );
} }

View File

@ -2,7 +2,9 @@
import { useSetupData, SetupData } from "@/context/SetupDataContext"; import { useSetupData, SetupData } from "@/context/SetupDataContext";
//import Typography from '@mui/material/Typography'; //import Typography from '@mui/material/Typography';
import EditIcon from '@mui/icons-material/Edit'; import EditIcon from '@mui/icons-material/Edit';
import { List, Card, CardContent, Typography, Box, useTheme, useMediaQuery, Button, CircularProgress, IconButton } from '@mui/material'; import AddIcon from '@mui/icons-material/Add';
import RefreshIcon from '@mui/icons-material/Refresh';
import { List, Card, CardContent, Typography, Box, useTheme, useMediaQuery, CircularProgress, IconButton } from '@mui/material';
import { DataGrid, GridColDef, GridRenderCellParams, GridRowModel, GridToolbarContainer, GridToolbarQuickFilter, GridToolbarExport, GridToolbarDensitySelector, GridToolbarColumnsButton } from '@mui/x-data-grid'; import { DataGrid, GridColDef, GridRenderCellParams, GridRowModel, GridToolbarContainer, GridToolbarQuickFilter, GridToolbarExport, GridToolbarDensitySelector, GridToolbarColumnsButton } from '@mui/x-data-grid';
import { Lock, LockOpen } from "@mui/icons-material"; import { Lock, LockOpen } from "@mui/icons-material";
//import utils from '@/ts/utils'; //import utils from '@/ts/utils';
@ -15,7 +17,7 @@ function Servers() {
const setupData: SetupData = useSetupData(); const setupData: SetupData = useSetupData();
const isMobile = useMediaQuery(theme.breakpoints.down("sm")); const isMobile = useMediaQuery(theme.breakpoints.down("sm"));
const [servers, setServers] = useState<Server[] | null>(null); const [serversWithPasswords, setServersWithPasswords] = useState<Server[] | null>(null);
const gridContainerRef = useRef<HTMLDivElement | null>(null); const gridContainerRef = useRef<HTMLDivElement | null>(null);
const [selectedRow, setSelectedRow] = useState<Server | null>(null); const [selectedRow, setSelectedRow] = useState<Server | null>(null);
const [open, setOpen] = useState<boolean>(false); const [open, setOpen] = useState<boolean>(false);
@ -24,14 +26,14 @@ function Servers() {
const togglePasswordVisibility = async () => { const togglePasswordVisibility = async () => {
if (isPasswordVisible) { if (isPasswordVisible) {
setIsPasswordVisible(false); setIsPasswordVisible(false);
setServers(setupData.servers); setServersWithPasswords(setupData.servers);
} }
else { else {
try { try {
setIsPasswordVisible(true); setIsPasswordVisible(true);
const serversResponse = await fetch("/api/servers/GetAll?activeOnly=false&returnPassword=true"); const serversResponse = await fetch("/api/servers/GetAll?activeOnly=false&returnPassword=true");
const serversData = await serversResponse.json(); const serversData = await serversResponse.json();
setServers(serversData); setServersWithPasswords(serversData);
} }
catch (error) { catch (error) {
console.error("Error fetching servers:", error); console.error("Error fetching servers:", error);
@ -42,12 +44,13 @@ function Servers() {
const columns: GridColDef<Server>[] = [ const columns: GridColDef<Server>[] = [
{ {
field: "actions", field: "actions",
headerName: "Actions", headerName: "",
sortable: false, sortable: false,
width: 60,
renderCell: (params: GridRenderCellParams<Server>) => ( renderCell: (params: GridRenderCellParams<Server>) => (
<Button variant="contained" color="primary" size="small" onClick={() => handleEdit(params.row)}> <IconButton color="primary" onClick={(e) => { e.stopPropagation(); handleEdit(params.row); }}>
Edit <EditIcon />
</Button> </IconButton>
), ),
}, },
{ field: "id", headerName: "ID", width: 60 }, { field: "id", headerName: "ID", width: 60 },
@ -62,7 +65,7 @@ function Servers() {
renderHeader: () => ( renderHeader: () => (
<div style={{ display: "flex", alignItems: "center" }}> <div style={{ display: "flex", alignItems: "center" }}>
Password Password
<IconButton size="small" onClick={togglePasswordVisibility} sx={{ marginLeft: 1 }}> <IconButton size="small" color="primary" onClick={togglePasswordVisibility} sx={{ marginLeft: 1 }}>
{isPasswordVisible ? <LockOpen /> : <Lock />} {isPasswordVisible ? <LockOpen /> : <Lock />}
</IconButton> </IconButton>
</div> </div>
@ -73,21 +76,49 @@ function Servers() {
]; ];
const handleNew = () => {
setSelectedRow(null);
setOpen(true);
};
const handleEdit = (row: GridRowModel<Server>) => { const handleEdit = (row: GridRowModel<Server>) => {
setSelectedRow(row); setSelectedRow(row);
setOpen(true); setOpen(true);
}; };
const handleUpdateRow = (updatedRow: Server) => { //const handleSaveRow = (updatedRow: Server, password: string) => {
setupData.setServers(updatedRow); // setupData.setServersWithPasswords(updatedRow);
updateServers(updatedRow); // if (isPasswordVisible) {
// updateServers
// }
// updateServers(updatedRow);
//};
//const updateServers = (updatedServer: Server) => {
// setServersWithPasswords((prevServers) => {
// if (prevServers == null) return null;
// return prevServers.map((server) => (server.id === updatedServer.id ? updatedServer : server))
// });
//};
const handleSaveRow = (savedRow: Server, password: string) => {
setupData.setServers(savedRow);
if (isPasswordVisible) {
if (password)
updateServers({ ...savedRow, password: password }); // Update local state
else
updateServers({ ...savedRow });
}
}; };
const updateServers = (updatedServer: Server) => { const updateServers = (updatedServer: Server) => {
setServers((prevServers) => { setServersWithPasswords((prev) => {
if (prevServers == null) return null; if (prev == null) return null;
return prevServers.map((server) => (server.id === updatedServer.id ? updatedServer : server))
const exists = prev.some((e) => e.id === updatedServer.id);
return exists
? prev.map((server) => (server.id === updatedServer.id ? updatedServer : server))
: [...prev, updatedServer];
}); });
}; };
const displayRows: Server[] = isPasswordVisible ? (serversWithPasswords ?? setupData.servers) : setupData.servers;
return ( return (
<Box ref={gridContainerRef} sx={{ <Box ref={gridContainerRef} sx={{
position: 'relative', left: 0, right: 0, height: "calc(100vh - 124px)", overflow: "hidden", position: 'relative', left: 0, right: 0, height: "calc(100vh - 124px)", overflow: "hidden",
@ -99,7 +130,7 @@ function Servers() {
<Box sx={{ position: 'absolute', inset: 0 }}> <Box sx={{ position: 'absolute', inset: 0 }}>
{isMobile ? ( {isMobile ? (
<List> <List>
{(!isPasswordVisible ? setupData.servers : servers)?.map((row) => ( {displayRows.map((row) => (
<Card key={row.id} sx={{ marginBottom: 2 }}> <Card key={row.id} sx={{ marginBottom: 2 }}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}> <Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<CardContent> <CardContent>
@ -109,11 +140,11 @@ function Servers() {
<Typography variant="body2">Port: {row.port}</Typography> <Typography variant="body2">Port: {row.port}</Typography>
<Typography variant="body2">Username: {row.username}</Typography> <Typography variant="body2">Username: {row.username}</Typography>
<Typography variant="body2">Password <Typography variant="body2">Password
<IconButton size="small" onClick={togglePasswordVisibility} sx={{ marginLeft: 1 }}> <IconButton color="primary" size="small" onClick={togglePasswordVisibility} sx={{ marginLeft: 1 }}>
{isPasswordVisible ? <LockOpen /> : <Lock />} {isPasswordVisible ? <LockOpen /> : <Lock />}
</IconButton>: {row.password}</Typography> </IconButton>: {row.password}</Typography>
</CardContent> </CardContent>
<IconButton onClick={(e) => { e.stopPropagation(); handleEdit(row); }}> <IconButton color="primary" onClick={(e) => { e.stopPropagation(); handleEdit(row); }}>
<EditIcon /> <EditIcon />
</IconButton> </IconButton>
</Box> </Box>
@ -122,7 +153,7 @@ function Servers() {
</List> </List>
) : ( ) : (
<DataGrid <DataGrid
rows={(!isPasswordVisible ? setupData.servers : servers)!} rows={displayRows}
columns={columns} columns={columns}
autoPageSize autoPageSize
sx={{ minWidth: "600px" }} sx={{ minWidth: "600px" }}
@ -130,14 +161,12 @@ function Servers() {
slots={{ slots={{
toolbar: () => ( toolbar: () => (
<GridToolbarContainer sx={{ display: "flex", alignItems: "center" }}> <GridToolbarContainer sx={{ display: "flex", alignItems: "center" }}>
<Button <IconButton size="small" color="primary" onClick={handleNew} sx={{ marginLeft: 1 }}>
variant="contained" <AddIcon />
color="primary" </IconButton>
onClick={() => setupData.reloadServers()} // Refresh only active servers <IconButton size="small" color="primary" onClick={() => setupData.reloadServers()} sx={{ marginLeft: 1 }}>
sx={{ marginRight: 2 }} {setupData.serversLoading ? <CircularProgress size={24} color="inherit" /> : <RefreshIcon />}
> </IconButton>
{setupData.serversLoading ? <CircularProgress size={24} color="inherit" /> : "Refresh"}
</Button>
<GridToolbarColumnsButton /> <GridToolbarColumnsButton />
<GridToolbarDensitySelector /> <GridToolbarDensitySelector />
<GridToolbarExport /> <GridToolbarExport />
@ -164,12 +193,12 @@ function Servers() {
</Box> </Box>
{/* Server Edit Modal */} {/* Server Edit Modal */}
{selectedRow && ( {open && (
<ServerEdit <ServerEdit
open={open} open={open}
server={selectedRow} server={selectedRow}
onClose={() => setOpen(false)} onClose={() => setOpen(false)}
onSave={handleUpdateRow} onSave={handleSaveRow}
/> />
)} )}
</Box> </Box>

View File

@ -1,7 +1,9 @@
import { useState, useRef } from 'react'; import { useState, useRef } from 'react';
import { useSetupData, SetupData } from "@/context/SetupDataContext"; import { useSetupData, SetupData } from "@/context/SetupDataContext";
import EditIcon from '@mui/icons-material/Edit'; import EditIcon from '@mui/icons-material/Edit';
import { List, Card, CardContent, Typography, Box, useTheme, useMediaQuery, Button, CircularProgress, IconButton } from '@mui/material'; import AddIcon from '@mui/icons-material/Add';
import RefreshIcon from '@mui/icons-material/Refresh';
import { List, Card, CardContent, Typography, Box, useTheme, useMediaQuery, CircularProgress, IconButton } from '@mui/material';
import { DataGrid, GridColDef, GridRenderCellParams, GridRowModel, GridToolbarContainer, GridToolbarQuickFilter, GridToolbarExport, GridToolbarDensitySelector, GridToolbarColumnsButton } from '@mui/x-data-grid'; import { DataGrid, GridColDef, GridRenderCellParams, GridRowModel, GridToolbarContainer, GridToolbarQuickFilter, GridToolbarExport, GridToolbarDensitySelector, GridToolbarColumnsButton } from '@mui/x-data-grid';
import Target from '@/types/target'; import Target from '@/types/target';
import TargetEdit from "@/components/modals/TargetEdit"; import TargetEdit from "@/components/modals/TargetEdit";
@ -21,12 +23,13 @@ function Targets() {
const columns: GridColDef<Target>[] = [ const columns: GridColDef<Target>[] = [
{ {
field: "actions", field: "actions",
headerName: "Actions", headerName: "",
sortable: false, sortable: false,
width: 60,
renderCell: (params: GridRenderCellParams<Target>) => ( renderCell: (params: GridRenderCellParams<Target>) => (
<Button variant="contained" color="primary" size="small" onClick={() => handleEdit(params.row)}> <IconButton color="primary" onClick={(e) => { e.stopPropagation(); handleEdit(params.row); }}>
Edit <EditIcon />
</Button> </IconButton>
), ),
}, },
{ field: "id", headerName: "ID", width: 60 }, { field: "id", headerName: "ID", width: 60 },
@ -119,22 +122,12 @@ function Targets() {
slots={{ slots={{
toolbar: () => ( toolbar: () => (
<GridToolbarContainer sx={{ display: "flex", alignItems: "center" }}> <GridToolbarContainer sx={{ display: "flex", alignItems: "center" }}>
<Button <IconButton size="small" color="primary" onClick={handleNew} sx={{ marginLeft: 1 }}>
variant="contained" <AddIcon />
color="primary" </IconButton>
onClick={() => handleNew()} <IconButton size="small" color="primary" onClick={() => setupData.reloadTargets()} sx={{ marginLeft: 1 }}>
sx={{ marginRight: 2 }} {setupData.targetsLoading ? <CircularProgress size={24} color="inherit" /> : <RefreshIcon />}
> </IconButton>
{setupData.targetsLoading ? <CircularProgress size={24} color="inherit" /> : "Add New"}
</Button>
<Button
variant="contained"
color="primary"
onClick={() => setupData.reloadTargets()}
sx={{ marginRight: 2 }}
>
{setupData.targetsLoading ? <CircularProgress size={24} color="inherit" /> : "Refresh"}
</Button>
<GridToolbarColumnsButton /> <GridToolbarColumnsButton />
<GridToolbarDensitySelector /> <GridToolbarDensitySelector />
<GridToolbarExport /> <GridToolbarExport />

View File

@ -1,7 +1,9 @@
import { useState, useRef } from 'react'; import { useState, useRef } from 'react';
import { useSetupData, SetupData } from "@/context/SetupDataContext"; import { useSetupData, SetupData } from "@/context/SetupDataContext";
import EditIcon from '@mui/icons-material/Edit'; import EditIcon from '@mui/icons-material/Edit';
import { List, Card, CardContent, Typography, Box, useTheme, useMediaQuery, Button, CircularProgress, IconButton } from '@mui/material'; import AddIcon from '@mui/icons-material/Add';
import RefreshIcon from '@mui/icons-material/Refresh';
import { List, Card, CardContent, Typography, Box, useTheme, useMediaQuery, CircularProgress, IconButton } from '@mui/material';
import { DataGrid, GridColDef, GridRenderCellParams, GridRowModel, GridToolbarContainer, GridToolbarQuickFilter, GridToolbarExport, GridToolbarDensitySelector, GridToolbarColumnsButton } from '@mui/x-data-grid'; import { DataGrid, GridColDef, GridRenderCellParams, GridRowModel, GridToolbarContainer, GridToolbarQuickFilter, GridToolbarExport, GridToolbarDensitySelector, GridToolbarColumnsButton } from '@mui/x-data-grid';
import Template from '@/types/template'; import Template from '@/types/template';
import TemplateEdit from "@/components/modals/TemplateEdit"; import TemplateEdit from "@/components/modals/TemplateEdit";
@ -18,12 +20,13 @@ function Templates() {
const columns: GridColDef<Template>[] = [ const columns: GridColDef<Template>[] = [
{ {
field: "actions", field: "actions",
headerName: "Actions", headerName: "",
sortable: false, sortable: false,
width:60,
renderCell: (params: GridRenderCellParams<Template>) => ( renderCell: (params: GridRenderCellParams<Template>) => (
<Button variant="contained" color="primary" size="small" onClick={() => handleEdit(params.row)}> <IconButton color="primary" onClick={(e) => { e.stopPropagation(); handleEdit(params.row); }}>
Edit <EditIcon />
</Button> </IconButton>
), ),
}, },
{ field: "id", headerName: "ID", width: 60 }, { field: "id", headerName: "ID", width: 60 },
@ -75,7 +78,7 @@ function Templates() {
<Typography variant="body2">Description: {row.description}</Typography> <Typography variant="body2">Description: {row.description}</Typography>
<Typography variant="body2">Active: {row.isActive ? "Yes" : "No"}</Typography> <Typography variant="body2">Active: {row.isActive ? "Yes" : "No"}</Typography>
</CardContent> </CardContent>
<IconButton onClick={(e) => { e.stopPropagation(); handleEdit(row); }}> <IconButton color="primary" onClick={(e) => { e.stopPropagation(); handleEdit(row); }}>
<EditIcon /> <EditIcon />
</IconButton> </IconButton>
</Box> </Box>
@ -91,23 +94,13 @@ function Templates() {
minWidth: "600px", maxWidth: getMaxWidth() }} minWidth: "600px", maxWidth: getMaxWidth() }}
slots={{ slots={{
toolbar: () => ( toolbar: () => (
<GridToolbarContainer sx={{ display: "flex", alignItems: "center" }}> <GridToolbarContainer sx={{ display: "flex", alignItems: "center", }}>
<Button <IconButton size="small" color="primary" onClick={handleNew} sx={{ marginLeft: 1 }}>
variant="contained" <AddIcon />
color="primary" </IconButton>
onClick={() => handleNew()} <IconButton size="small" color="primary" onClick={() => setupData.reloadTemplates()} sx={{ marginLeft: 1 }}>
sx={{ marginRight: 2 }} {setupData.templatesLoading ? <CircularProgress size={24} color="inherit" /> : <RefreshIcon />}
> </IconButton>
{"Add New"}
</Button>
<Button
variant="contained"
color="primary"
onClick={() => setupData.reloadTemplates()}
sx={{ marginRight: 2 }}
>
{setupData.templatesLoading ? <CircularProgress size={24} color="inherit" /> : "Refresh"}
</Button>
<GridToolbarColumnsButton /> <GridToolbarColumnsButton />
<GridToolbarDensitySelector /> <GridToolbarDensitySelector />
<GridToolbarExport /> <GridToolbarExport />

View File

@ -1,7 +1,9 @@
import { useState, useRef } from 'react'; import { useState, useRef } from 'react';
import { useSetupData, SetupData } from "@/context/SetupDataContext"; import { useSetupData, SetupData } from "@/context/SetupDataContext";
import EditIcon from '@mui/icons-material/Edit'; import EditIcon from '@mui/icons-material/Edit';
import { List, Card, CardContent, Typography, Box, useTheme, useMediaQuery, Button, CircularProgress, IconButton } from '@mui/material'; import AddIcon from '@mui/icons-material/Add';
import RefreshIcon from '@mui/icons-material/Refresh';
import { List, Card, CardContent, Typography, Box, useTheme, useMediaQuery, CircularProgress, IconButton } from '@mui/material';
import { DataGrid, GridColDef, GridRenderCellParams, GridRowModel, GridToolbarContainer, GridToolbarQuickFilter, GridToolbarExport, GridToolbarDensitySelector, GridToolbarColumnsButton } from '@mui/x-data-grid'; import { DataGrid, GridColDef, GridRenderCellParams, GridRowModel, GridToolbarContainer, GridToolbarQuickFilter, GridToolbarExport, GridToolbarDensitySelector, GridToolbarColumnsButton } from '@mui/x-data-grid';
import TestEmailList from '@/types/testEmailList'; import TestEmailList from '@/types/testEmailList';
import TestEmailListEdit from "@/components/modals/TestEmailListEdit"; import TestEmailListEdit from "@/components/modals/TestEmailListEdit";
@ -18,12 +20,13 @@ function TestEmailLists() {
const columns: GridColDef<TestEmailList>[] = [ const columns: GridColDef<TestEmailList>[] = [
{ {
field: "actions", field: "actions",
headerName: "Actions", headerName: "",
sortable: false, sortable: false,
width: 60,
renderCell: (params: GridRenderCellParams<TestEmailList>) => ( renderCell: (params: GridRenderCellParams<TestEmailList>) => (
<Button variant="contained" color="primary" size="small" onClick={() => handleEdit(params.row)}> <IconButton color="primary" onClick={(e) => { e.stopPropagation(); handleEdit(params.row); }}>
Edit <EditIcon />
</Button> </IconButton>
), ),
}, },
{ field: "id", headerName: "ID", width: 60 }, { field: "id", headerName: "ID", width: 60 },
@ -70,7 +73,7 @@ function TestEmailLists() {
<Typography variant="body2">ID: {row.id}</Typography> <Typography variant="body2">ID: {row.id}</Typography>
<Typography variant="body2">Email List: {row.emails}</Typography> {/*TODO: Format properly*/} <Typography variant="body2">Email List: {row.emails}</Typography> {/*TODO: Format properly*/}
</CardContent> </CardContent>
<IconButton onClick={(e) => { e.stopPropagation(); handleEdit(row); }}> <IconButton color="primary" onClick={(e) => { e.stopPropagation(); handleEdit(row); }}>
<EditIcon /> <EditIcon />
</IconButton> </IconButton>
</Box> </Box>
@ -86,22 +89,12 @@ function TestEmailLists() {
slots={{ slots={{
toolbar: () => ( toolbar: () => (
<GridToolbarContainer sx={{ display: "flex", alignItems: "center" }}> <GridToolbarContainer sx={{ display: "flex", alignItems: "center" }}>
<Button <IconButton size="small" color="primary" onClick={handleNew} sx={{ marginLeft: 1 }}>
variant="contained" <AddIcon />
color="primary" </IconButton>
onClick={() => handleNew()} <IconButton size="small" color="primary" onClick={() => setupData.reloadTestEmailLists()} sx={{ marginLeft: 1 }}>
sx={{ marginRight: 2 }} {setupData.testEmailListsLoading ? <CircularProgress size={24} color="inherit" /> : <RefreshIcon />}
> </IconButton>
{setupData.testEmailListsLoading ? <CircularProgress size={24} color="inherit" /> : "Add New"}
</Button>
<Button
variant="contained"
color="primary"
onClick={() => setupData.reloadTestEmailLists()}
sx={{ marginRight: 2 }}
>
{setupData.testEmailListsLoading ? <CircularProgress size={24} color="inherit" /> : "Refresh"}
</Button>
<GridToolbarColumnsButton /> <GridToolbarColumnsButton />
<GridToolbarDensitySelector /> <GridToolbarDensitySelector />
<GridToolbarExport /> <GridToolbarExport />

View File

@ -18,7 +18,7 @@ function UnsubscribeUrls() {
const columns: GridColDef<UnsubscribeUrl>[] = [ const columns: GridColDef<UnsubscribeUrl>[] = [
{ {
field: "actions", field: "actions",
headerName: "Actions", headerName: "",
sortable: false, sortable: false,
renderCell: (params: GridRenderCellParams<UnsubscribeUrl>) => ( renderCell: (params: GridRenderCellParams<UnsubscribeUrl>) => (
<Button variant="contained" color="primary" size="small" onClick={() => handleEdit(params.row)}> <Button variant="contained" color="primary" size="small" onClick={() => handleEdit(params.row)}>

View File

@ -1,78 +0,0 @@
.has-feedback .form-control {
padding-right: 30.5px;
}
.notesStyle {
text-overflow: ellipsis;
overflow: hidden;
max-width: 200px;
min-width: 200px;
white-space: nowrap;
}
.table > thead > tr > th, .table > tbody > tr > th, .table > tfoot > tr > th, .table > thead > tr > td, .table > tbody > tr > td, .table > tfoot > tr > td {
padding: 3px;
line-height: 1.42857143;
vertical-align: top;
border-top: 1px solid #ddd;
}
.dataTables_wrapper {
padding: 20px;
}
.dataTables_wrapper .sorting_icon {
width: 0px;
}
.no-sort {
padding-right: 3px !important;
}
.drop-zone {
width: 300px;
height: 200px;
border: 2px dashed #ccc;
border-radius: 10px;
display: flex;
justify-content: center;
align-items: center;
text-align: center;
margin: 20px auto;
cursor: pointer;
}
.drop-zone.dragover {
background-color: #f0f0f0;
}
.preview {
display: flex;
flex-wrap: wrap;
margin-top: 20px;
}
.preview div {
position: relative;
margin: 10px;
}
.preview img {
max-width: 100px;
display: block;
}
.preview button {
position: absolute;
top: 0;
right: 0;
background: red;
color: white;
border: none;
border-radius: 50%;
cursor: pointer;
}
.form-group {
margin-bottom: 5px;
}

View File

@ -1,107 +0,0 @@
/* Custom CSS to restore old AdminLTE color classes in AdminLTE 4.x */
/* Orange Background */
.bg-orange {
background-color: #ff851b !important;
color: #ffffff;
}
/* Blue Background */
.bg-blue {
background-color: #0073b7 !important;
color: #ffffff;
}
/* Green Background */
.bg-green {
background-color: #00a65a !important;
color: #ffffff;
}
/* Red Background */
.bg-red {
background-color: #dd4b39 !important;
color: #ffffff;
}
/* Yellow Background */
.bg-yellow {
background-color: #f39c12 !important;
color: #ffffff;
}
/* Purple Background */
.bg-purple {
background-color: #605ca8 !important;
color: #ffffff;
}
/* Light Blue Background */
.bg-light-blue {
background-color: #3c8dbc !important;
color: #ffffff;
}
/* Navy Background */
.bg-navy {
background-color: #001f3f !important;
color: #ffffff;
}
/* Teal Background */
.bg-teal {
background-color: #39cccc !important;
color: #ffffff;
}
/* Maroon Background */
.bg-maroon {
background-color: #d81b60 !important;
color: #ffffff;
}
/* Black Background */
.bg-black {
background-color: #111 !important;
color: #ffffff;
}
/* Olive Background */
.bg-olive {
background-color: #3d9970 !important;
color: #ffffff;
}
/* Lime Background */
.bg-lime {
background-color: #01ff70 !important;
color: #ffffff;
}
/* Fuchsia Background */
.bg-fuchsia {
background-color: #f012be !important;
color: #ffffff;
}
/* Aqua Background */
.bg-aqua {
background-color: #00c0ef !important;
color: #ffffff;
}
/* Gray Background */
.bg-gray {
background-color: #d2d6de !important;
color: #ffffff;
}
/* Flat Button */
.btn-flat {
box-shadow: none;
border-radius: 0;
border: none;
padding: 10px 15px;
font-size: 14px;
transition: background-color 0.3s ease;
}

View File

@ -1,5 +1,5 @@
export interface EmailDomain { export interface EmailDomain {
id?: number; id: number;
name: string; name: string;
emailAddress: string; emailAddress: string;
username: string; username: string;

View File

@ -1,5 +1,5 @@
export interface Server { export interface Server {
id?: number; id: number;
name: string; name: string;
serverName: string; serverName: string;
port: number; port: number;

View File

@ -1,5 +1,5 @@
export interface Target { export interface Target {
id?: number; id: number;
serverId: number; serverId: number;
name: string; name: string;
databaseName: string; databaseName: string;

View File

@ -1,5 +1,5 @@
export interface UnsubscribeUrl { export interface UnsubscribeUrl {
id?: number; id: number;
name: string; name: string;
url: string; url: string;
} }