diff --git a/src/AdminUI/Pages/NewUsers.razor b/src/AdminUI/Pages/NewUsers.razor
new file mode 100644
index 000000000..eb491e5a4
--- /dev/null
+++ b/src/AdminUI/Pages/NewUsers.razor
@@ -0,0 +1,131 @@
+@page "/NewUsers"
+@using SSW.Rewards.ApiClient.Services
+@using SSW.Rewards.Enums
+@using SSW.Rewards.Shared.DTOs.Users
+@using Microsoft.AspNetCore.Components.WebAssembly.Authentication
+@using Newtonsoft.Json
+@using Icons = MudBlazor.Icons
+@using System.Text
+@* @using Microsoft.AspNetCore.Authorization *@
+@* @attribute [Authorize] *@
+@inject IUserAdminService UserAdminService
+@inject IJSRuntime JSRuntime
+
+New Users
+View new users
+
+
+ New Users
+
+
+
+ @foreach (var icon in Enum.GetValues())
+ {
+ @icon.ToString()
+ }
+
+
+ Hide Staff
+
+
+ Search
+
+
+ Copy All
+
+
+
+
+
+
+
+
+
+ Id
+ Name
+ Email
+
+
+ @context.UserId
+ @context.Name
+
+
+ @context.Email
+
+
+
+
+
+
+
+
+
+
+@code {
+
+ NewUsersViewModel _vm = new ();
+
+ private string _infoFormat = "{first_item}-{last_item} of {all_items}";
+ private LeaderboardFilter _timeFrame = LeaderboardFilter.ThisMonth;
+ private bool _hideStaff = true;
+ private bool _loading = true;
+
+ protected override async Task OnInitializedAsync()
+ {
+ try
+ {
+ await FetchData();
+ }
+ catch (AccessTokenNotAvailableException exception)
+ {
+ exception.Redirect();
+ }
+ }
+
+ private async Task FetchData()
+ {
+ _loading = true;
+ _vm = await UserAdminService.GetNewUsers(_timeFrame, _hideStaff);
+ _loading = false;
+ }
+
+ private void CopyToClipboard(string text)
+ {
+ JSRuntime.InvokeVoidAsync("navigator.clipboard.writeText", text);
+ }
+
+ private void CopyAllUsers()
+ {
+ var serializer = JsonSerializer.Create(new JsonSerializerSettings
+ {
+ Formatting = Formatting.Indented,
+ });
+ var stringBuilder = new StringBuilder();
+ using (var writer = new JsonTextWriter(new StringWriter(stringBuilder)))
+ {
+ serializer.Serialize(writer, _vm.NewUsers);
+ }
+
+ var jsonText = stringBuilder.ToString();
+
+ CopyToClipboard(jsonText);
+ }
+}
\ No newline at end of file
diff --git a/src/AdminUI/Shared/NavMenu.razor b/src/AdminUI/Shared/NavMenu.razor
index 7cef1d0e2..b64b32657 100644
--- a/src/AdminUI/Shared/NavMenu.razor
+++ b/src/AdminUI/Shared/NavMenu.razor
@@ -9,5 +9,6 @@
Skills
Quizzes
Prize Draw
+ New Users
diff --git a/src/ApiClient/ConfigureServices.cs b/src/ApiClient/ConfigureServices.cs
index 1cc21e3c5..63d6c7152 100644
--- a/src/ApiClient/ConfigureServices.cs
+++ b/src/ApiClient/ConfigureServices.cs
@@ -28,6 +28,7 @@ public static IServiceCollection AddApiClientServices(this IServiceCol
services.AddSingleton();
services.AddSingleton();
services.AddSingleton();
+ services.AddSingleton();
}
services.AddSingleton();
diff --git a/src/ApiClient/Services/IPrizeDrawService.cs b/src/ApiClient/Services/IPrizeDrawService.cs
index 7817a7f3c..fbadb4e2e 100644
--- a/src/ApiClient/Services/IPrizeDrawService.cs
+++ b/src/ApiClient/Services/IPrizeDrawService.cs
@@ -24,7 +24,7 @@ public async Task GetEligibleUsers(GetEligibleUsersFilte
{
var result = await _httpClient.GetAsync($"{_baseRoute}GetEligibleUsers?filter={filter.Filter}&achievementId={filter.AchievementId}&filterStaff={filter.FilterStaff}", cancellationToken);
- if (result.IsSuccessStatusCode)
+ if (result.IsSuccessStatusCode)
{
var response = await result.Content.ReadFromJsonAsync(cancellationToken: cancellationToken);
diff --git a/src/ApiClient/Services/IUserAdminService.cs b/src/ApiClient/Services/IUserAdminService.cs
new file mode 100644
index 000000000..3ed835abb
--- /dev/null
+++ b/src/ApiClient/Services/IUserAdminService.cs
@@ -0,0 +1,40 @@
+using System.Net.Http.Json;
+using SSW.Rewards.Shared.DTOs.Users;
+
+namespace SSW.Rewards.ApiClient.Services;
+
+public interface IUserAdminService
+{
+ Task GetNewUsers(LeaderboardFilter filter, bool filterStaff, CancellationToken cancellationToken = default);
+}
+
+public class UserAdminService : IUserAdminService
+{
+ private readonly HttpClient _httpClient;
+
+ private const string _baseRoute = "api/User/";
+
+ public UserAdminService(IHttpClientFactory httpClientFactory)
+ {
+ _httpClient = httpClientFactory.CreateClient(Constants.AuthenticatedClient);
+ }
+
+ public async Task GetNewUsers(LeaderboardFilter filter, bool filterStaff, CancellationToken cancellationToken = default)
+ {
+ var result = await _httpClient.GetAsync($"{_baseRoute}GetNewUsers?filter={filter}&filterStaff={filterStaff}", cancellationToken);
+
+ if (result.IsSuccessStatusCode)
+ {
+ var response = await result.Content.ReadFromJsonAsync(cancellationToken: cancellationToken);
+
+ if (response is not null)
+ {
+ return response;
+ }
+ }
+
+ var responseContent = await result.Content.ReadAsStringAsync(cancellationToken);
+
+ throw new Exception($"Failed to get new users: {responseContent}");
+ }
+}
\ No newline at end of file
diff --git a/src/ApiClient/Services/IUserService.cs b/src/ApiClient/Services/IUserService.cs
index 69f0245cc..af322cfe5 100644
--- a/src/ApiClient/Services/IUserService.cs
+++ b/src/ApiClient/Services/IUserService.cs
@@ -161,23 +161,23 @@ public async Task GetUserRewards(int userId, CancellationT
throw new Exception($"Failed to get user rewards: {responseContent}");
}
+
+ public async Task GetNewUsers(LeaderboardFilter filter, bool filterStaff, CancellationToken cancellationToken = default)
+ {
+ var result = await _httpClient.GetAsync($"{_baseRoute}GetNewUsers?filter={filter}&filterStaff={filterStaff}", cancellationToken);
+
+ if (result.IsSuccessStatusCode)
+ {
+ var response = await result.Content.ReadFromJsonAsync(cancellationToken: cancellationToken);
- // public async Task GetNewUsers(LeaderboardFilter filter, bool filterStaff = false, CancellationToken cancellationToken = default)
- // {
- // var result = await _httpClient.GetAsync($"{_baseRoute}/NewUsers?filter={filter}&filterStaff={filterStaff}");
- //
- // if (result.IsSuccessStatusCode)
- // {
- // var response = await result.Content.ReadFromJsonAsync(cancellationToken: cancellationToken);
- //
- // if (response is not null)
- // {
- // return response;
- // }
- // }
- //
- // var responseContent = await result.Content.ReadAsStringAsync(cancellationToken);
- //
- // throw new Exception($"Failed to get user rewards: {responseContent}");
- // }
+ if (response is not null)
+ {
+ return response;
+ }
+ }
+
+ var responseContent = await result.Content.ReadAsStringAsync(cancellationToken);
+
+ throw new Exception($"Failed to get new users: {responseContent}");
+ }
}
\ No newline at end of file
diff --git a/src/Application/Users/Queries/GetNewUsers/GetNewUsersQuery.cs b/src/Application/Users/Queries/GetNewUsers/GetNewUsersQuery.cs
new file mode 100644
index 000000000..78db04930
--- /dev/null
+++ b/src/Application/Users/Queries/GetNewUsers/GetNewUsersQuery.cs
@@ -0,0 +1,73 @@
+using AutoMapper.QueryableExtensions;
+using SSW.Rewards.Application.Common.Extensions;
+using SSW.Rewards.Shared.DTOs.Users;
+
+namespace SSW.Rewards.Application.Users.Queries.GetNewUsers;
+
+public class GetNewUsersQuery : IRequest
+{
+ public LeaderboardFilter Filter { get; set; }
+ public bool FilterStaff { get; set; }
+}
+
+public class GetNewUsersHandler(
+ IApplicationDbContext context,
+ IMapper mapper,
+ IDateTime dateTime)
+ : IRequestHandler
+{
+ public async Task Handle(GetNewUsersQuery request, CancellationToken cancellationToken)
+ {
+ var eligibleUsers = context.Users.AsNoTracking().Where(u => u.Activated == true);
+
+ switch (request.Filter)
+ {
+ case LeaderboardFilter.ThisYear:
+ eligibleUsers = eligibleUsers
+ .TagWith("NewUsersThisYear")
+ .Where(u => u.CreatedUtc.Year == dateTime.Now.Year);
+ break;
+ case LeaderboardFilter.ThisMonth:
+ eligibleUsers = eligibleUsers
+ .TagWith("NewUsersThisMonth")
+ .Where(u => u.CreatedUtc.Year == dateTime.Now.Year && u.CreatedUtc.Month == dateTime.Now.Month);
+ break;
+ case LeaderboardFilter.Today:
+ eligibleUsers = eligibleUsers
+ .TagWith("NewUsersToday")
+ .Where(u => u.CreatedUtc.Year == dateTime.Now.Year && u.CreatedUtc.Month == dateTime.Now.Month && u.CreatedUtc.Day == dateTime.Now.Day);
+ break;
+ case LeaderboardFilter.ThisWeek:
+ {
+ var start = dateTime.Now.FirstDayOfWeek();
+ var end = start.AddDays(7);
+
+ eligibleUsers = eligibleUsers
+ .TagWith("NewUsersThisWeek")
+ .Where(u => start <= u.CreatedUtc && u.CreatedUtc <= end);
+ break;
+ }
+ case LeaderboardFilter.Forever:
+ default:
+ // no action
+ break;
+ }
+
+ var users = await eligibleUsers
+ .ProjectTo(mapper.ConfigurationProvider)
+ .ToListAsync(cancellationToken);
+
+ // filter out any @ssw.com.au users (.ends with didn't translate with EF Core :(
+ if (request.FilterStaff)
+ {
+ users = users.Where(u => (u.Email != null && !u.Email.EndsWith("@ssw.com.au", StringComparison.OrdinalIgnoreCase))).ToList();
+ }
+
+ NewUsersViewModel vm = new()
+ {
+ NewUsers = users
+ };
+
+ return vm;
+ }
+}
\ No newline at end of file
diff --git a/src/Application/Users/Queries/GetNewUsers/Mapping.cs b/src/Application/Users/Queries/GetNewUsers/Mapping.cs
new file mode 100644
index 000000000..9a48ef19d
--- /dev/null
+++ b/src/Application/Users/Queries/GetNewUsers/Mapping.cs
@@ -0,0 +1,14 @@
+using SSW.Rewards.Shared.DTOs.Users;
+
+namespace SSW.Rewards.Application.Users.Queries;
+
+public class Mapping : Profile
+{
+ public Mapping()
+ {
+ CreateMap()
+ .ForMember(dst => dst.UserId, opt => opt.MapFrom(src => src.Id))
+ .ForMember(dst => dst.Name, opt => opt.MapFrom(src => src.FullName))
+ .ForMember(dst => dst.Email, opt => opt.MapFrom(src => src.Email));
+ }
+}
\ No newline at end of file
diff --git a/src/Common/DTOs/Users/NewUserDto.cs b/src/Common/DTOs/Users/NewUserDto.cs
new file mode 100644
index 000000000..00f97b1f1
--- /dev/null
+++ b/src/Common/DTOs/Users/NewUserDto.cs
@@ -0,0 +1,10 @@
+namespace SSW.Rewards.Shared.DTOs.Users;
+
+public class NewUserDto
+{
+ public int? UserId { get; set; }
+
+ public string? Name { get; set; }
+
+ public string? Email { get; set; }
+}
\ No newline at end of file
diff --git a/src/Common/DTOs/Users/NewUsersViewModel.cs b/src/Common/DTOs/Users/NewUsersViewModel.cs
new file mode 100644
index 000000000..88556bd60
--- /dev/null
+++ b/src/Common/DTOs/Users/NewUsersViewModel.cs
@@ -0,0 +1,6 @@
+namespace SSW.Rewards.Shared.DTOs.Users;
+
+public class NewUsersViewModel
+{
+ public IEnumerable NewUsers { get; set; }
+}
\ No newline at end of file
diff --git a/src/WebAPI/Controllers/UserController.cs b/src/WebAPI/Controllers/UserController.cs
index 31331d6f4..d17a87469 100644
--- a/src/WebAPI/Controllers/UserController.cs
+++ b/src/WebAPI/Controllers/UserController.cs
@@ -1,4 +1,5 @@
-using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Mvc;
using SSW.Rewards.Shared.DTOs.Users;
using SSW.Rewards.Application.Achievements.Commands.ClaimSocialMediaAchievementForUser;
using SSW.Rewards.Application.Users.Commands.DeleteMyProfile;
@@ -7,10 +8,13 @@
using SSW.Rewards.Application.Users.Commands.UpsertUserSocialMediaId;
using SSW.Rewards.Application.Users.Queries.GetCurrentUser;
using SSW.Rewards.Application.Users.Queries.GetCurrentUserRoles;
+using SSW.Rewards.Application.Users.Queries.GetNewUsers;
using SSW.Rewards.Application.Users.Queries.GetProfileAchievements;
using SSW.Rewards.Application.Users.Queries.GetUser;
using SSW.Rewards.Application.Users.Queries.GetUserAchievements;
using SSW.Rewards.Application.Users.Queries.GetUserRewards;
+using SSW.Rewards.Enums;
+using SSW.Rewards.WebAPI.Authorisation;
namespace SSW.Rewards.WebAPI.Controllers;
@@ -93,4 +97,11 @@ public async Task DeleteMyProfile()
return Ok();
}
+
+ [HttpGet]
+ [Authorize(Roles = AuthorizationRoles.Admin)]
+ public async Task> GetNewUsers([FromQuery] LeaderboardFilter filter, bool filterStaff)
+ {
+ return await Mediator.Send(new GetNewUsersQuery { Filter = filter, FilterStaff = filterStaff });
+ }
}
\ No newline at end of file