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