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