diff --git a/Surge365.MassEmailReact.API/Controllers/AuthenticationController.cs b/Surge365.MassEmailReact.API/Controllers/AuthenticationController.cs index cfa204a..b825bc5 100644 --- a/Surge365.MassEmailReact.API/Controllers/AuthenticationController.cs +++ b/Surge365.MassEmailReact.API/Controllers/AuthenticationController.cs @@ -1,13 +1,12 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; +using Surge365.MassEmailReact.API.Controllers; using Surge365.MassEmailReact.Application.DTOs; using Surge365.MassEmailReact.Application.Interfaces; -namespace Surge365.MassEmailReact.Server.Controllers +namespace Surge365.MassEmailReact.API.Controllers { - [Route("api/[controller]")] - [ApiController] - public class AuthenticationController : ControllerBase + public class AuthenticationController : BaseController { private readonly IAuthService _authService; diff --git a/Surge365.MassEmailReact.API/Controllers/BaseController.cs b/Surge365.MassEmailReact.API/Controllers/BaseController.cs new file mode 100644 index 0000000..3b69279 --- /dev/null +++ b/Surge365.MassEmailReact.API/Controllers/BaseController.cs @@ -0,0 +1,11 @@ +using Microsoft.AspNetCore.Mvc; + +namespace Surge365.MassEmailReact.API.Controllers +{ + [Route("[controller]")] + [Route("api/[controller]")] + [ApiController] + public class BaseController : ControllerBase + { + } +} diff --git a/Surge365.MassEmailReact.API/Controllers/BouncedEmailsController.cs b/Surge365.MassEmailReact.API/Controllers/BouncedEmailsController.cs index 02a0975..c075565 100644 --- a/Surge365.MassEmailReact.API/Controllers/BouncedEmailsController.cs +++ b/Surge365.MassEmailReact.API/Controllers/BouncedEmailsController.cs @@ -4,11 +4,9 @@ using Surge365.MassEmailReact.Application.Interfaces; using Surge365.MassEmailReact.Domain.Entities; using System.Net.Mail; -namespace Surge365.MassEmailReact.Server.Controllers +namespace Surge365.MassEmailReact.API.Controllers { - [Route("api/[controller]")] - [ApiController] - public class BouncedEmailsController : ControllerBase + public class BouncedEmailsController : BaseController { private readonly IBouncedEmailService _bouncedEmailService; diff --git a/Surge365.MassEmailReact.API/Controllers/EmailDomainsController.cs b/Surge365.MassEmailReact.API/Controllers/EmailDomainsController.cs index bec80dc..d67f8cb 100644 --- a/Surge365.MassEmailReact.API/Controllers/EmailDomainsController.cs +++ b/Surge365.MassEmailReact.API/Controllers/EmailDomainsController.cs @@ -3,11 +3,9 @@ using Surge365.MassEmailReact.Application.Interfaces; using Surge365.MassEmailReact.Domain.Entities; using System.Threading.Tasks; -namespace Surge365.MassEmailReact.Server.Controllers +namespace Surge365.MassEmailReact.API.Controllers { - [Route("api/[controller]")] - [ApiController] - public class EmailDomainsController : ControllerBase + public class EmailDomainsController : BaseController { private readonly IEmailDomainService _emailDomainService; @@ -34,7 +32,7 @@ namespace Surge365.MassEmailReact.Server.Controllers } [HttpPost()] - public async Task CreateTarget(int id, [FromBody] EmailDomainUpdateDto emailDomainUpdateDto) + public async Task CreateTarget([FromBody] EmailDomainUpdateDto emailDomainUpdateDto) { if (emailDomainUpdateDto.Id != null && emailDomainUpdateDto.Id > 0) return BadRequest("Id must be null or 0"); diff --git a/Surge365.MassEmailReact.API/Controllers/ServersController.cs b/Surge365.MassEmailReact.API/Controllers/ServersController.cs index 318bd14..7fdb53a 100644 --- a/Surge365.MassEmailReact.API/Controllers/ServersController.cs +++ b/Surge365.MassEmailReact.API/Controllers/ServersController.cs @@ -4,12 +4,11 @@ using Microsoft.AspNetCore.Mvc; using Surge365.MassEmailReact.Application.DTOs; using Surge365.MassEmailReact.Application.Interfaces; using Surge365.MassEmailReact.Domain.Entities; +using Surge365.MassEmailReact.Infrastructure.Services; -namespace Surge365.MassEmailReact.Server.Controllers +namespace Surge365.MassEmailReact.API.Controllers { - [Route("api/[controller]")] - [ApiController] - public class ServersController : ControllerBase + public class ServersController : BaseController { private readonly IServerService _serverService; @@ -19,6 +18,7 @@ namespace Surge365.MassEmailReact.Server.Controllers } + [HttpGet()] [HttpGet("GetAll")] public async Task GetAll([FromQuery] bool? activeOnly, bool? returnPassword = null) { @@ -35,6 +35,21 @@ namespace Surge365.MassEmailReact.Server.Controllers var server = await _serverService.GetByIdAsync(id, returnPasswordValue); return server is not null ? Ok(server) : NotFound($"Server with key '{id}' not found."); } + + [HttpPost()] + public async Task 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}")] public async Task UpdateServer(int id, [FromBody] ServerUpdateDto serverUpdateDto) { diff --git a/Surge365.MassEmailReact.API/Controllers/TargetsController.cs b/Surge365.MassEmailReact.API/Controllers/TargetsController.cs index 898e4bf..31f2cbe 100644 --- a/Surge365.MassEmailReact.API/Controllers/TargetsController.cs +++ b/Surge365.MassEmailReact.API/Controllers/TargetsController.cs @@ -5,11 +5,9 @@ using Surge365.MassEmailReact.Application.DTOs; using Surge365.MassEmailReact.Application.Interfaces; using Surge365.MassEmailReact.Domain.Entities; -namespace Surge365.MassEmailReact.Server.Controllers +namespace Surge365.MassEmailReact.API.Controllers { - [Route("api/[controller]")] - [ApiController] - public class TargetsController : ControllerBase + public class TargetsController : BaseController { 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."); } [HttpPost()] - public async Task CreateTarget(int id, [FromBody] TargetUpdateDto targetUpdateDto) + public async Task CreateTarget([FromBody] TargetUpdateDto targetUpdateDto) { if (targetUpdateDto.Id != null && targetUpdateDto.Id > 0) return BadRequest("Id must be null or 0"); var targetId = await _targetService.CreateAsync(targetUpdateDto); 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); diff --git a/Surge365.MassEmailReact.API/Controllers/TemplatesController.cs b/Surge365.MassEmailReact.API/Controllers/TemplatesController.cs index 4956017..598af35 100644 --- a/Surge365.MassEmailReact.API/Controllers/TemplatesController.cs +++ b/Surge365.MassEmailReact.API/Controllers/TemplatesController.cs @@ -3,11 +3,9 @@ using Microsoft.AspNetCore.Mvc; using Surge365.MassEmailReact.Application.Interfaces; using Surge365.MassEmailReact.Domain.Entities; -namespace Surge365.MassEmailReact.Server.Controllers +namespace Surge365.MassEmailReact.API.Controllers { - [Route("api/[controller]")] - [ApiController] - public class TemplatesController : ControllerBase + public class TemplatesController : BaseController { private readonly ITemplateService _templateService; diff --git a/Surge365.MassEmailReact.API/Controllers/TestEmailListsController.cs b/Surge365.MassEmailReact.API/Controllers/TestEmailListsController.cs index 45a97e0..297e75b 100644 --- a/Surge365.MassEmailReact.API/Controllers/TestEmailListsController.cs +++ b/Surge365.MassEmailReact.API/Controllers/TestEmailListsController.cs @@ -3,11 +3,9 @@ using Microsoft.AspNetCore.Mvc; using Surge365.MassEmailReact.Application.Interfaces; using Surge365.MassEmailReact.Domain.Entities; -namespace Surge365.MassEmailReact.Server.Controllers +namespace Surge365.MassEmailReact.API.Controllers { - [Route("api/[controller]")] - [ApiController] - public class TestEmailListsController : ControllerBase + public class TestEmailListsController : BaseController { private readonly ITestEmailListService _testEmailListService; diff --git a/Surge365.MassEmailReact.API/Controllers/UnsubscribeUrlController.cs b/Surge365.MassEmailReact.API/Controllers/UnsubscribeUrlController.cs index 3366617..4d6b8ee 100644 --- a/Surge365.MassEmailReact.API/Controllers/UnsubscribeUrlController.cs +++ b/Surge365.MassEmailReact.API/Controllers/UnsubscribeUrlController.cs @@ -4,11 +4,9 @@ using Surge365.MassEmailReact.Application.DTOs; using Surge365.MassEmailReact.Application.Interfaces; using Surge365.MassEmailReact.Domain.Entities; -namespace Surge365.MassEmailReact.Server.Controllers +namespace Surge365.MassEmailReact.API.Controllers { - [Route("api/[controller]")] - [ApiController] - public class UnsubscribeUrlsController : ControllerBase + public class UnsubscribeUrlsController : BaseController { 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."); } [HttpPost()] - public async Task Create(int id, [FromBody] UnsubscribeUrlUpdateDto unsubscribeUrlUpdateDto) + public async Task Create([FromBody] UnsubscribeUrlUpdateDto unsubscribeUrlUpdateDto) { if (unsubscribeUrlUpdateDto.Id != null && unsubscribeUrlUpdateDto.Id > 0) return BadRequest("Id must be null or 0"); diff --git a/Surge365.MassEmailReact.API/Program.cs b/Surge365.MassEmailReact.API/Program.cs index cec99e9..0142ece 100644 --- a/Surge365.MassEmailReact.API/Program.cs +++ b/Surge365.MassEmailReact.API/Program.cs @@ -46,7 +46,6 @@ app.UseAuthorization(); app.MapControllers(); -app.MapFallbackToFile("/index.html"); DapperConfiguration.ConfigureMappings(); diff --git a/Surge365.MassEmailReact.API/Surge365.MassEmailReact.Server.http b/Surge365.MassEmailReact.API/Surge365.MassEmailReact.Server.http index 318c506..5df96be 100644 --- a/Surge365.MassEmailReact.API/Surge365.MassEmailReact.Server.http +++ b/Surge365.MassEmailReact.API/Surge365.MassEmailReact.Server.http @@ -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 ### diff --git a/Surge365.MassEmailReact.Application/Interfaces/IServerRepository.cs b/Surge365.MassEmailReact.Application/Interfaces/IServerRepository.cs index c58f812..d4e4e53 100644 --- a/Surge365.MassEmailReact.Application/Interfaces/IServerRepository.cs +++ b/Surge365.MassEmailReact.Application/Interfaces/IServerRepository.cs @@ -11,6 +11,7 @@ namespace Surge365.MassEmailReact.Application.Interfaces { Task GetByIdAsync(int id, bool returnPassword = false); Task> GetAllAsync(bool activeOnly = true, bool returnPassword = false); + Task CreateAsync(Server server); Task UpdateAsync(Server server); } } diff --git a/Surge365.MassEmailReact.Application/Interfaces/IServerService.cs b/Surge365.MassEmailReact.Application/Interfaces/IServerService.cs index 5ceaef2..56f62eb 100644 --- a/Surge365.MassEmailReact.Application/Interfaces/IServerService.cs +++ b/Surge365.MassEmailReact.Application/Interfaces/IServerService.cs @@ -6,6 +6,7 @@ namespace Surge365.MassEmailReact.Application.Interfaces { Task GetByIdAsync(int id, bool returnPassword = false); Task> GetAllAsync(bool activeOnly = true, bool returnPassword = false); + Task CreateAsync(ServerUpdateDto targetDto); Task UpdateAsync(ServerUpdateDto serverDto); } } diff --git a/Surge365.MassEmailReact.Infrastructure/Repositories/ServerRepository.cs b/Surge365.MassEmailReact.Infrastructure/Repositories/ServerRepository.cs index 9b64bae..14ead4b 100644 --- a/Surge365.MassEmailReact.Infrastructure/Repositories/ServerRepository.cs +++ b/Surge365.MassEmailReact.Infrastructure/Repositories/ServerRepository.cs @@ -66,6 +66,35 @@ namespace Surge365.MassEmailReact.Infrastructure.Repositories return servers; } + public async Task 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("@success"); + + if (success) + return parameters.Get("@server_key"); + + return null; + } public async Task UpdateAsync(Server server) { ArgumentNullException.ThrowIfNull(server); diff --git a/Surge365.MassEmailReact.Infrastructure/Services/ServerService.cs b/Surge365.MassEmailReact.Infrastructure/Services/ServerService.cs index 7a0529a..9f15b7b 100644 --- a/Surge365.MassEmailReact.Infrastructure/Services/ServerService.cs +++ b/Surge365.MassEmailReact.Infrastructure/Services/ServerService.cs @@ -10,6 +10,7 @@ using Microsoft.IdentityModel.Tokens; using Microsoft.Extensions.Configuration; using Surge365.MassEmailReact.Domain.Entities; using System.Security.Cryptography; +using Surge365.MassEmailReact.Infrastructure.Repositories; namespace Surge365.MassEmailReact.Infrastructure.Services @@ -33,6 +34,22 @@ namespace Surge365.MassEmailReact.Infrastructure.Services { return await _serverRepository.GetAllAsync(activeOnly, returnPassword); } + public async Task 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 UpdateAsync(ServerUpdateDto serverDto) { ArgumentNullException.ThrowIfNull(serverDto, nameof(serverDto)); diff --git a/Surge365.MassEmailReact.Web/Surge365.MassEmailReact.Web.esproj b/Surge365.MassEmailReact.Web/Surge365.MassEmailReact.Web.esproj index d4db629..64b0dc2 100644 --- a/Surge365.MassEmailReact.Web/Surge365.MassEmailReact.Web.esproj +++ b/Surge365.MassEmailReact.Web/Surge365.MassEmailReact.Web.esproj @@ -9,7 +9,9 @@ $(MSBuildProjectDirectory)\dist + + diff --git a/Surge365.MassEmailReact.Web/package-lock.json b/Surge365.MassEmailReact.Web/package-lock.json index 8b72aba..3fe4b78 100644 --- a/Surge365.MassEmailReact.Web/package-lock.json +++ b/Surge365.MassEmailReact.Web/package-lock.json @@ -20,8 +20,6 @@ "admin-lte": "4.0.0-beta3", "bootstrap": "^5.3.3", "dayjs": "^1.11.13", - "font-awesome": "^4.7.0", - "ionicons": "^7.4.0", "react": "^19.0.0", "react-bootstrap": "^2.10.9", "react-dom": "^19.0.0", @@ -2035,19 +2033,6 @@ "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==", "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": { "version": "0.5.15", "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", @@ -3345,15 +3330,6 @@ "dev": true, "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": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -3506,15 +3482,6 @@ "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": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", diff --git a/Surge365.MassEmailReact.Web/package.json b/Surge365.MassEmailReact.Web/package.json index 766f885..eef6718 100644 --- a/Surge365.MassEmailReact.Web/package.json +++ b/Surge365.MassEmailReact.Web/package.json @@ -23,8 +23,6 @@ "admin-lte": "4.0.0-beta3", "bootstrap": "^5.3.3", "dayjs": "^1.11.13", - "font-awesome": "^4.7.0", - "ionicons": "^7.4.0", "react": "^19.0.0", "react-bootstrap": "^2.10.9", "react-dom": "^19.0.0", diff --git a/Surge365.MassEmailReact.Web/src/components/layouts/Layout.tsx b/Surge365.MassEmailReact.Web/src/components/layouts/Layout.tsx index f7e8eae..a61db02 100644 --- a/Surge365.MassEmailReact.Web/src/components/layouts/Layout.tsx +++ b/Surge365.MassEmailReact.Web/src/components/layouts/Layout.tsx @@ -228,7 +228,23 @@ const Layout = ({ children }: LayoutProps) => { { text: 'Completed Mailings', icon: , path: '/completedMailings' }, ].map((item) => ( - isMobile && handleDrawerClose()} + isMobile && handleDrawerClose()} + sx={{ + '&.Mui-selected': { + backgroundColor: 'primary.main', + color: 'primary.contrastText', + '& .MuiListItemIcon-root': { + color: 'primary.contrastText', + }, + }, + '&.Mui-selected:hover': { + backgroundColor: 'primary.dark', + }, + }} > {item.icon} diff --git a/Surge365.MassEmailReact.Web/src/components/layouts/LayoutLogin.tsx b/Surge365.MassEmailReact.Web/src/components/layouts/LayoutLogin.tsx index 34f2f6a..1ff086e 100644 --- a/Surge365.MassEmailReact.Web/src/components/layouts/LayoutLogin.tsx +++ b/Surge365.MassEmailReact.Web/src/components/layouts/LayoutLogin.tsx @@ -1,29 +1,13 @@ import { ReactNode } from 'react'; -//import { useEffect } 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 '@/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 children; -} - -LayoutLogin.propTypes = { - children: PropTypes.any }; -export default LayoutLogin; +LayoutLogin.propTypes = { + children: PropTypes.any, +}; + +export default LayoutLogin; \ No newline at end of file diff --git a/Surge365.MassEmailReact.Web/src/components/layouts/LayoutLogin_Backup.tsx b/Surge365.MassEmailReact.Web/src/components/layouts/LayoutLogin_Backup.tsx deleted file mode 100644 index 7fe9c3e..0000000 --- a/Surge365.MassEmailReact.Web/src/components/layouts/LayoutLogin_Backup.tsx +++ /dev/null @@ -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 ( - - - - {children} - - ); -} - -LayoutLogin.propTypes = { - children: PropTypes.any -}; - -export default LayoutLogin; diff --git a/Surge365.MassEmailReact.Web/src/components/layouts/LayoutOld.tsx b/Surge365.MassEmailReact.Web/src/components/layouts/LayoutOld.tsx deleted file mode 100644 index 386e4b1..0000000 --- a/Surge365.MassEmailReact.Web/src/components/layouts/LayoutOld.tsx +++ /dev/null @@ -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 ( -
- - - - - - {/*AdminLTE?*/} - - {/**/} - {/**/} - - - - {/**/} - {/**/} - {/**/} - {/**/} -
- - - Logo - - - Logo - USAHaulers - - - -
- - - - {children} - -
-
- Version 1.0.0 -
- Copyright © 2024 Surge365. All rights reserved. -
-
- ); -} - -Layout.propTypes = { - children: PropTypes.any -}; - -export default Layout; diff --git a/Surge365.MassEmailReact.Web/src/components/layouts/Layout_backup.tsx b/Surge365.MassEmailReact.Web/src/components/layouts/Layout_backup.tsx deleted file mode 100644 index 13865ad..0000000 --- a/Surge365.MassEmailReact.Web/src/components/layouts/Layout_backup.tsx +++ /dev/null @@ -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 ( - -
- - - - - - - {/*AdminLTE?*/} - - {/**/} - {/**/} - - - - {/**/} - {/**/} - {/**/} - {/**/} - -
- - - Logo - - - Logo - USAHaulers - - - -
- - - - {children} - -
-
- Version 1.0.0 -
- Copyright © 2024 Surge365. All rights reserved. -
-
-
- ); -} - -Layout.propTypes = { - children: PropTypes.any -}; - -export default Layout; diff --git a/Surge365.MassEmailReact.Web/src/components/modals/EmailDomainEdit.tsx b/Surge365.MassEmailReact.Web/src/components/modals/EmailDomainEdit.tsx index a719cea..1ba3e1f 100644 --- a/Surge365.MassEmailReact.Web/src/components/modals/EmailDomainEdit.tsx +++ b/Surge365.MassEmailReact.Web/src/components/modals/EmailDomainEdit.tsx @@ -23,7 +23,7 @@ type EmailDomainEditProps = { }; const schema = yup.object().shape({ - id: yup.number(), + id: yup.number().default(0), name: yup .string() .required("Name is required") @@ -37,7 +37,7 @@ const schema = yup.object().shape({ emailAddress: yup.string().email("Invalid email").required("Email address is required"), username: yup.string().required("Username is required"), 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; else return value.length > 0; }), @@ -60,7 +60,7 @@ const EmailDomainEdit = ({ open, emailDomain, onClose, onSave }: EmailDomainEdit const setupData: SetupData = useSetupData(); const originalEmailDomain: EmailDomain | null = emailDomain ? { ...emailDomain } : null; - const { register, trigger, control, handleSubmit, reset, formState: { errors } } = useForm({ + const { register, control, handleSubmit, reset, formState: { errors } } = useForm({ mode: "onBlur", defaultValues: emailDomain || defaultEmailDomain, resolver: yupResolver(schema) as Resolver, @@ -89,7 +89,7 @@ const EmailDomainEdit = ({ open, emailDomain, onClose, onSave }: EmailDomainEdit if (!response.ok) throw new Error(isNew ? "Failed to create" : "Failed to update"); const updatedEmailDomain = await response.json(); - onSave(updatedEmailDomain, isNew || formData.password ? formData.password : originalEmailDomain?.password); + onSave(updatedEmailDomain, isNew || formData.password ? formData.password : originalEmailDomain?.password ?? ""); onClose(); } catch (error) { console.error("Update error:", error); diff --git a/Surge365.MassEmailReact.Web/src/components/modals/ForgotPasswordModal.tsx b/Surge365.MassEmailReact.Web/src/components/modals/ForgotPasswordModal.tsx index 65e6f04..4a5cfe7 100644 --- a/Surge365.MassEmailReact.Web/src/components/modals/ForgotPasswordModal.tsx +++ b/Surge365.MassEmailReact.Web/src/components/modals/ForgotPasswordModal.tsx @@ -1,6 +1,6 @@ import { useState, FormEvent } from 'react'; -import { Modal, Button, Form } from 'react-bootstrap'; -import { FaExclamationCircle } from 'react-icons/fa'; // For optional font icon +import { Dialog, DialogTitle, DialogContent, DialogActions, Button, TextField, Typography, Box, IconButton } from '@mui/material'; +import { Close as CloseIcon } from '@mui/icons-material'; import utils from '@/ts/utils'; type FormErrors = Record; @@ -25,9 +25,8 @@ const ForgotPasswordModal: React.FC = ({ show, onClose if (Object.keys(errors).length > 0) { setFormErrors(errors); return false; - } else { - return true; } + return true; }; const handleStartPasswordRecovery = async (e: FormEvent) => { @@ -52,48 +51,51 @@ const ForgotPasswordModal: React.FC = ({ show, onClose }; return ( - - - Forgot your password? - - + { }}> + + Forgot your password? + + + + + {usernameNotFound && ( - An email has been sent to the address you provided. Please follow the instructions to reset your password. + + An email has been sent to the address you provided. Please follow the instructions to reset your password. + )} {!recoveryStarted && ( -
- - Enter your email address below and we'll send you instructions on how to reset your password... - Email - + + + Enter your username below and we'll send you instructions on how to reset your password... + + setUsername(e.target.value)} + error={!!formErrors.username} + helperText={formErrors.username} required autoFocus - size="lg" + size="small" /> - {formErrors.username && ( - - )} - {formErrors.username} - - -
+ )} -
- - - -
+ + ); }; -export default ForgotPasswordModal; - +export default ForgotPasswordModal; \ No newline at end of file diff --git a/Surge365.MassEmailReact.Web/src/components/modals/ServerEdit.tsx b/Surge365.MassEmailReact.Web/src/components/modals/ServerEdit.tsx index 2f4088f..fa8ca49 100644 --- a/Surge365.MassEmailReact.Web/src/components/modals/ServerEdit.tsx +++ b/Surge365.MassEmailReact.Web/src/components/modals/ServerEdit.tsx @@ -9,35 +9,72 @@ import { } from "@mui/material"; import Server from "@/types/server"; +import { useForm } from "react-hook-form"; +import { yupResolver } from "@hookform/resolvers/yup"; +import * as yup from "yup"; + type ServerEditProps = { open: boolean; - server: Server; + server: Server | null; 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 [formData, setFormData] = useState({ ...server }); - //const [serverError, setServerError] = useState(false); // Track validation + const isNew = !server || server.id === 0; + const originalServer: Server | null = server ? { ...server } : null; + //const [formData, setFormData] = useState({ ...server }); + const { register, handleSubmit, reset, formState: { errors } } = useForm({ + mode: "onBlur", + defaultValues: server || defaultServer, + resolver: yupResolver(schema) + }); const [loading, setLoading] = useState(false); - useEffect(() => { //Reset form to unedited state on open or server change + useEffect(() => { 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 () => { + const handleSave = async (formData: Server) => { + const apiUrl = isNew ? "/api/servers" : `/api/servers/${formData.id}`; + const method = isNew ? "POST" : "PUT"; setLoading(true); try { - const response = await fetch(`/api/servers/${formData.id}`, { - method: "PUT", + const response = await fetch(apiUrl, { + method: method, headers: { "Content-Type": "application/json" }, body: JSON.stringify(formData), }); @@ -45,7 +82,7 @@ const ServerEdit = ({ open, server, onClose, onSave }: ServerEditProps) => { if (!response.ok) throw new Error("Failed to update"); const updatedServer = await response.json(); - onSave(updatedServer); // Update UI optimistically + onSave(updatedServer, isNew || formData.password ? formData.password : originalServer?.password ?? ""); onClose(); } catch (error) { console.error("Update error:", error); @@ -56,47 +93,52 @@ const ServerEdit = ({ open, server, onClose, onSave }: ServerEditProps) => { return ( - Edit Server - id={formData.id} + {isNew ? "Add Server" : "Edit Server id=" + server?.id} handleChange("name", e.target.value)} margin="dense" + error={!!errors.name} + helperText={errors.name?.message} /> handleChange("serverName", e.target.value)} margin="dense" + error={!!errors.serverName} + helperText={errors.serverName?.message} /> handleChange("port", e.target.value)} margin="dense" + error={!!errors.port} + helperText={errors.port?.message} /> handleChange("username", e.target.value)} margin="dense" + error={!!errors.username} + helperText={errors.username?.message} /> handleChange("password", e.target.value)} margin="dense" + error={!!errors.password} + helperText={errors.password?.message} /> - diff --git a/Surge365.MassEmailReact.Web/src/components/modals/TemplateEdit.tsx b/Surge365.MassEmailReact.Web/src/components/modals/TemplateEdit.tsx index 25bbc1b..ad2bf57 100644 --- a/Surge365.MassEmailReact.Web/src/components/modals/TemplateEdit.tsx +++ b/Surge365.MassEmailReact.Web/src/components/modals/TemplateEdit.tsx @@ -94,8 +94,8 @@ const TemplateEdit = ({ open, template, onClose, onSave }: TemplateEditProps) => const handleSave = async (formData: Template) => { const domain = setupData.emailDomains.find(el => el.id === formData.domainId); - formData.fromEmail = domain.emailAddress; - formData.replyToEmail = domain.emailAddress; + formData.fromEmail = domain?.emailAddress ?? ""; + formData.replyToEmail = domain?.emailAddress ?? ""; const apiUrl = isNew ? "/api/templates" : `/api/templates/${formData.id}`; const method = isNew ? "POST" : "PUT"; setLoading(true); diff --git a/Surge365.MassEmailReact.Web/src/components/pages/BouncedEmails.tsx b/Surge365.MassEmailReact.Web/src/components/pages/BouncedEmails.tsx index e2f8470..e1c3ede 100644 --- a/Surge365.MassEmailReact.Web/src/components/pages/BouncedEmails.tsx +++ b/Surge365.MassEmailReact.Web/src/components/pages/BouncedEmails.tsx @@ -1,7 +1,9 @@ import { useState, useRef } from 'react'; import { useSetupData, SetupData } from "@/context/SetupDataContext"; 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 BouncedEmail from '@/types/bouncedEmail'; import BouncedEmailEdit from "@/components/modals/BouncedEmailEdit"; @@ -22,10 +24,10 @@ function BouncedEmails() { sortable: false, renderCell: (params: GridRenderCellParams) => (
- { e.stopPropagation(); handleEdit(params.row); }}> + { e.stopPropagation(); handleEdit(params.row); }}> - { e.stopPropagation(); handleDelete(params.row); }}> + { e.stopPropagation(); handleDelete(params.row); }}>
@@ -111,22 +113,12 @@ function BouncedEmails() { slots={{ toolbar: () => ( - - + + + + setupData.reloadBouncedEmails()} sx={{ marginLeft: 1 }}> + {setupData.bouncedEmailsLoading ? : } + diff --git a/Surge365.MassEmailReact.Web/src/components/pages/EmailDomains.tsx b/Surge365.MassEmailReact.Web/src/components/pages/EmailDomains.tsx index 9e5973d..db84b6e 100644 --- a/Surge365.MassEmailReact.Web/src/components/pages/EmailDomains.tsx +++ b/Surge365.MassEmailReact.Web/src/components/pages/EmailDomains.tsx @@ -1,6 +1,8 @@ import { useState, useRef } from 'react'; import { useSetupData, SetupData } from "@/context/SetupDataContext"; 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 { 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'; @@ -21,12 +23,13 @@ function EmailDomains() { const columns: GridColDef[] = [ { field: "actions", - headerName: "Actions", + headerName: "", sortable: false, + width: 60, renderCell: (params: GridRenderCellParams) => ( - + { e.stopPropagation(); handleEdit(params.row); }}> + + ), }, { field: "id", headerName: "ID", width: 60 }, @@ -40,7 +43,7 @@ function EmailDomains() { renderHeader: () => (
Password - { e.stopPropagation(); togglePasswordVisibility(); }} sx={{ marginLeft: 1 }}> + { e.stopPropagation(); togglePasswordVisibility(); }} sx={{ marginLeft: 1 }}> {isPasswordVisible ? : }
@@ -100,7 +103,7 @@ function EmailDomains() { : [...prev, updatedEmailDomain]; }); }; - const displayRows = isPasswordVisible ? (emailDomainsWithPasswords ?? setupData.emailDomains) : setupData.emailDomains; + const displayRows: EmailDomain[] = isPasswordVisible ? (emailDomainsWithPasswords ?? setupData.emailDomains) : setupData.emailDomains; return ( Display Order: {row.displayOrder} Active: {row.isActive ? "Yes" : "No"} - { e.stopPropagation(); handleEdit(row); }}> + { e.stopPropagation(); handleEdit(row); }}> @@ -150,26 +153,12 @@ function EmailDomains() { slots={{ toolbar: () => ( - - + + + + setupData.reloadEmailDomains()} sx={{ marginLeft: 1 }}> + {setupData.emailDomainsLoading ? : } + diff --git a/Surge365.MassEmailReact.Web/src/components/pages/Login.tsx b/Surge365.MassEmailReact.Web/src/components/pages/Login.tsx index b3b3a3a..00c5c57 100644 --- a/Surge365.MassEmailReact.Web/src/components/pages/Login.tsx +++ b/Surge365.MassEmailReact.Web/src/components/pages/Login.tsx @@ -1,10 +1,16 @@ 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 { Helmet, HelmetProvider } from 'react-helmet-async'; - import utils from '@/ts/utils.ts'; -import ForgotPasswordModal from '@/components/modals/ForgotPasswordModal'; +//import ForgotPasswordModal from '@/components/modals/ForgotPasswordModal'; type SpinnerState = Record; type FormErrors = Record; @@ -15,17 +21,10 @@ function Login() { const [formErrors, setFormErrors] = useState({}); const [username, setUsername] = useState(''); const [password, setPassword] = useState(''); - const [showForgotPasswordModal, setShowForgotPasswordModal] = useState(false); - //const [user, setUser] = useState(null); + //const [showForgotPasswordModal, setShowForgotPasswordModal] = useState(false); const [loginError, setLoginError] = useState(false); const [loginErrorMessage, setLoginErrorMessage] = useState(''); - //const setSpinners = (newValues: Partial) => { - // setSpinnersState((prevSpinners) => ({ - // ...prevSpinners, - // ...newValues, - // })); - //}; const setSpinners = (newValues: Partial) => { setSpinnersState((prevSpinners) => { const updatedSpinners: SpinnerState = { ...prevSpinners }; @@ -38,23 +37,19 @@ function Login() { }); }; - const handleCloseForgotPasswordModal = () => { - setShowForgotPasswordModal(false); - }; + //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); } @@ -63,14 +58,15 @@ function Login() { const handleLogin = async (e: React.FormEvent) => { e.preventDefault(); setIsLoading(true); - spinners.Login = true; - setSpinners(spinners); + setSpinners({ Login: true }); validateLoginForm(); + if (Object.keys(formErrors).length > 0) { + setIsLoading(false); + setSpinners({ Login: false }); + return; + } - if (Object.keys(formErrors).length > 0) return; - - //setUser(null); setLoginError(false); setLoginErrorMessage(''); let loggedInUser: User | null = null; @@ -83,45 +79,39 @@ function Login() { success: (json: AuthResponse) => { try { loggedInUser = json.user; - //setUser(loggedInUser); - } - catch { - const errorMsg: string = "Unexpected Error"; + } catch { + const errorMsg: string = 'Unexpected Error'; hadLoginError = true; - hadLoginErrorMessage = errorMsg; + hadLoginErrorMessage = errorMsg; } }, error: (err: unknown) => { - let errorMsg: string = "Unexpected Error"; + 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); + const errorResponse = err as AuthErrorResponse; + if (errorResponse.data?.message) { + errorMsg = errorResponse.data.message; } + console.error(errorMsg); + setLoginErrorMessage(errorMsg); } hadLoginError = true; - hadLoginErrorMessage = errorMsg; - } + hadLoginErrorMessage = errorMsg; + }, }); if (hadLoginError) { setLoginErrorMessage(hadLoginErrorMessage); setLoginError(true); setIsLoading(false); - spinners.Login = false; - setSpinners(spinners); + setSpinners({ Login: false }); return; } if (loggedInUser == null) { setLoginError(true); setIsLoading(false); - spinners.Login = false; - setSpinners(spinners); + setSpinners({ Login: false }); } else { await finishUserLogin(loggedInUser); } @@ -129,15 +119,13 @@ function Login() { const finishUserLogin = async (loggedInUser: User) => { setIsLoading(false); - spinners.Login = false; - spinners.LoginWithPasskey = false; - setSpinners(spinners); + setSpinners({ Login: false, LoginWithPasskey: false }); - utils.localStorage("session_currentUser", loggedInUser); + utils.localStorage('session_currentUser', loggedInUser); - const redirectUrl = utils.sessionStorage("redirect_url"); + const redirectUrl = utils.sessionStorage('redirect_url'); if (redirectUrl) { - utils.sessionStorage("redirect_url", null); + utils.sessionStorage('redirect_url', null); document.location.href = redirectUrl; } else { document.location.href = '/home'; @@ -145,55 +133,89 @@ function Login() { }; return ( -
-
-

surge365 - React

-
-
-

Please sign in

-
+ + {/* Main heading */} + + Surge 365 Mass Email 2 + + + {/* Login form */} + + + Please sign in + + + {/* Login error message */} {loginError && ( - {loginErrorMessage ?? "Login error"} + + {loginErrorMessage || 'Login error'} + )} - - Username - setUsername(e.target.value)} - required - autoFocus - size="sm" - /> - {spinners.Username && } - - - Password - setPassword(e.target.value)} - required - size="sm" - /> - + {/* Username field */} + setUsername(e.target.value)} + error={!!formErrors.username} + helperText={formErrors.username} + required + autoFocus + size="small" + /> - - - -
- -
+ {/* Forgot password button */} + {/* setShowForgotPasswordModal(true)}*/} + {/* sx={{ mt: 1 }}*/} + {/*>*/} + {/* Forgot Password*/} + {/**/} + + + + {/* Forgot password modal */} + {/**/} + ); } -export default Login; +export default Login; \ No newline at end of file diff --git a/Surge365.MassEmailReact.Web/src/components/pages/Servers.tsx b/Surge365.MassEmailReact.Web/src/components/pages/Servers.tsx index bc24942..681e369 100644 --- a/Surge365.MassEmailReact.Web/src/components/pages/Servers.tsx +++ b/Surge365.MassEmailReact.Web/src/components/pages/Servers.tsx @@ -2,7 +2,9 @@ import { useSetupData, SetupData } from "@/context/SetupDataContext"; //import Typography from '@mui/material/Typography'; 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 { Lock, LockOpen } from "@mui/icons-material"; //import utils from '@/ts/utils'; @@ -15,7 +17,7 @@ function Servers() { const setupData: SetupData = useSetupData(); const isMobile = useMediaQuery(theme.breakpoints.down("sm")); - const [servers, setServers] = useState(null); + const [serversWithPasswords, setServersWithPasswords] = useState(null); const gridContainerRef = useRef(null); const [selectedRow, setSelectedRow] = useState(null); const [open, setOpen] = useState(false); @@ -24,14 +26,14 @@ function Servers() { const togglePasswordVisibility = async () => { if (isPasswordVisible) { setIsPasswordVisible(false); - setServers(setupData.servers); + setServersWithPasswords(setupData.servers); } else { try { setIsPasswordVisible(true); const serversResponse = await fetch("/api/servers/GetAll?activeOnly=false&returnPassword=true"); const serversData = await serversResponse.json(); - setServers(serversData); + setServersWithPasswords(serversData); } catch (error) { console.error("Error fetching servers:", error); @@ -42,12 +44,13 @@ function Servers() { const columns: GridColDef[] = [ { field: "actions", - headerName: "Actions", + headerName: "", sortable: false, + width: 60, renderCell: (params: GridRenderCellParams) => ( - + { e.stopPropagation(); handleEdit(params.row); }}> + + ), }, { field: "id", headerName: "ID", width: 60 }, @@ -62,7 +65,7 @@ function Servers() { renderHeader: () => (
Password - + {isPasswordVisible ? : }
@@ -73,21 +76,49 @@ function Servers() { ]; + const handleNew = () => { + setSelectedRow(null); + setOpen(true); + }; const handleEdit = (row: GridRowModel) => { setSelectedRow(row); setOpen(true); }; - const handleUpdateRow = (updatedRow: Server) => { - setupData.setServers(updatedRow); - updateServers(updatedRow); + //const handleSaveRow = (updatedRow: Server, password: string) => { + // setupData.setServersWithPasswords(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) => { - setServers((prevServers) => { - if (prevServers == null) return null; - return prevServers.map((server) => (server.id === updatedServer.id ? updatedServer : server)) + setServersWithPasswords((prev) => { + if (prev == null) return null; + + 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 ( {isMobile ? ( - {(!isPasswordVisible ? setupData.servers : servers)?.map((row) => ( + {displayRows.map((row) => ( @@ -109,11 +140,11 @@ function Servers() { Port: {row.port} Username: {row.username} Password - + {isPasswordVisible ? : } : {row.password} - { e.stopPropagation(); handleEdit(row); }}> + { e.stopPropagation(); handleEdit(row); }}> @@ -122,7 +153,7 @@ function Servers() { ) : ( ( - + + + + setupData.reloadServers()} sx={{ marginLeft: 1 }}> + {setupData.serversLoading ? : } + @@ -164,12 +193,12 @@ function Servers() { {/* Server Edit Modal */} - {selectedRow && ( + {open && ( setOpen(false)} - onSave={handleUpdateRow} + onSave={handleSaveRow} /> )} diff --git a/Surge365.MassEmailReact.Web/src/components/pages/Targets.tsx b/Surge365.MassEmailReact.Web/src/components/pages/Targets.tsx index 41772f2..69e841c 100644 --- a/Surge365.MassEmailReact.Web/src/components/pages/Targets.tsx +++ b/Surge365.MassEmailReact.Web/src/components/pages/Targets.tsx @@ -1,7 +1,9 @@ import { useState, useRef } from 'react'; import { useSetupData, SetupData } from "@/context/SetupDataContext"; 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 Target from '@/types/target'; import TargetEdit from "@/components/modals/TargetEdit"; @@ -21,12 +23,13 @@ function Targets() { const columns: GridColDef[] = [ { field: "actions", - headerName: "Actions", + headerName: "", sortable: false, + width: 60, renderCell: (params: GridRenderCellParams) => ( - + { e.stopPropagation(); handleEdit(params.row); }}> + + ), }, { field: "id", headerName: "ID", width: 60 }, @@ -119,22 +122,12 @@ function Targets() { slots={{ toolbar: () => ( - - + + + + setupData.reloadTargets()} sx={{ marginLeft: 1 }}> + {setupData.targetsLoading ? : } + diff --git a/Surge365.MassEmailReact.Web/src/components/pages/Templates.tsx b/Surge365.MassEmailReact.Web/src/components/pages/Templates.tsx index 5d2701a..e9df032 100644 --- a/Surge365.MassEmailReact.Web/src/components/pages/Templates.tsx +++ b/Surge365.MassEmailReact.Web/src/components/pages/Templates.tsx @@ -1,7 +1,9 @@ import { useState, useRef } from 'react'; import { useSetupData, SetupData } from "@/context/SetupDataContext"; 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 Template from '@/types/template'; import TemplateEdit from "@/components/modals/TemplateEdit"; @@ -18,12 +20,13 @@ function Templates() { const columns: GridColDef