From 7cfd6fa0ad504307d9053aa5ee71cf6592aaab63 Mon Sep 17 00:00:00 2001 From: Andreas Guldborg Hansen Date: Tue, 23 Apr 2024 18:47:51 +0200 Subject: [PATCH] Adds unused tickets statistics (#34) * Adds unused tickets statistics * Add API v2 specs * Fix setting dates twice on load * Set full object and not each individual property * Replace lambda function with method signature * Remove duplicate null check * Change wording of status 204 --- Shifty.App/Components/UnusedTickets.razor | 92 ++++++++++++++++ Shifty.App/Components/UserTable.razor | 2 +- Shifty.App/DomainModels/UnusedTicket.cs | 24 ++++ Shifty.App/Pages/Statistics.razor | 24 ++++ Shifty.App/Program.cs | 2 + .../Repositories/IUnusedTicketRepository.cs | 15 +++ .../Repositories/UnusedTicketsRepository.cs | 25 +++++ Shifty.App/Services/IUnusedTicketsService.cs | 23 ++++ Shifty.App/Services/UnusedTicketsService.cs | 30 +++++ Shifty.App/Shared/NavMenu.razor | 6 +- .../OpenApiSpecs/AnalogCoreV2.json | 104 ++++++++++++++++++ 11 files changed, 345 insertions(+), 2 deletions(-) create mode 100644 Shifty.App/Components/UnusedTickets.razor create mode 100644 Shifty.App/DomainModels/UnusedTicket.cs create mode 100644 Shifty.App/Pages/Statistics.razor create mode 100644 Shifty.App/Repositories/IUnusedTicketRepository.cs create mode 100644 Shifty.App/Repositories/UnusedTicketsRepository.cs create mode 100644 Shifty.App/Services/IUnusedTicketsService.cs create mode 100644 Shifty.App/Services/UnusedTicketsService.cs diff --git a/Shifty.App/Components/UnusedTickets.razor b/Shifty.App/Components/UnusedTickets.razor new file mode 100644 index 0000000..e2773bf --- /dev/null +++ b/Shifty.App/Components/UnusedTickets.razor @@ -0,0 +1,92 @@ +@namespace Components +@using System.ComponentModel.DataAnnotations +@using Shifty.App.Services +@using Shifty.Api.Generated.AnalogCoreV1 +@using Shifty.Api.Generated.AnalogCoreV2 +@using Shifty.App.DomainModels +@using Shifty.App.Shared +@using Shared +@using LanguageExt.UnsafeValueAccess +@inject IUnusedTicketsService _unusedTicketsService +@inject ISnackbar Snackbar + + + + + Unused Tickets + + @if (_loading) + { + + } + + + + + + + + + @context.Item.UnusedPurchasesValue.ToString("0.00") + + + + No records found for the given time period + + + +@code +{ + private IEnumerable Items; + private bool _loading = false; + private DateRange _queryDateRange = new(){ Start = new(DateTime.Today.Year, 1, 1), End = new(DateTime.Today.Year, 12, 31)}; + + private async Task LoadUnusedTickets(DateRange queryDateRange) + { + _loading = true; + if (queryDateRange.Start is null || queryDateRange.End is null) return; + + _queryDateRange = queryDateRange; + var result = await _unusedTicketsService.GetUnusedTickets(queryDateRange.Start.Value, queryDateRange.End.Value); + + result.Match( + Succ: res => { + Items = res; + }, + Fail: error => { + Snackbar.Add(error.Message, Severity.Error); + Items = new List(); + } + ); + _loading = false; + } + + protected override async Task OnInitializedAsync() + { + await LoadUnusedTickets(_queryDateRange); + } + + private AggregateDefinition _ticketsLeftSum = new() + { + Type = AggregateType.Sum + }; + + private AggregateDefinition _valueLeftSum = new() + { + CustomAggregate = x => { + var sum = x.Sum(t => t.UnusedPurchasesValue); + return sum.ToString("0.00"); + }, + Type = AggregateType.Custom, + }; + + private AggregateDefinition _footerLabel = new() + { + CustomAggregate = x => "Total", + Type = AggregateType.Custom, + }; +} \ No newline at end of file diff --git a/Shifty.App/Components/UserTable.razor b/Shifty.App/Components/UserTable.razor index fd6d41a..d757983 100644 --- a/Shifty.App/Components/UserTable.razor +++ b/Shifty.App/Components/UserTable.razor @@ -76,7 +76,7 @@ return result.Match( Succ: res => { - return new TableData(){ Items = res.Users.AsEnumerable(), TotalItems = res.TotalUsers};; + return new TableData(){ Items = res.Users.AsEnumerable(), TotalItems = res.TotalUsers}; }, Fail: error => { Snackbar.Add(error.Message, Severity.Error); diff --git a/Shifty.App/DomainModels/UnusedTicket.cs b/Shifty.App/DomainModels/UnusedTicket.cs new file mode 100644 index 0000000..e85dda3 --- /dev/null +++ b/Shifty.App/DomainModels/UnusedTicket.cs @@ -0,0 +1,24 @@ +using Components; +using Shifty.Api.Generated.AnalogCoreV2; + +namespace Shifty.App.DomainModels +{ + public class UnusedTicket + { + public int ProductId { get; set; } + public string ProductName { get; set; } + public int TicketsLeft { get; set; } + public decimal UnusedPurchasesValue { get; set; } + + public static UnusedTicket FromDto(UnusedClipsResponse ticket) + { + return new UnusedTicket() + { + ProductId = ticket.ProductId, + ProductName = ticket.ProductName, + TicketsLeft = ticket.TicketsLeft, + UnusedPurchasesValue = ticket.UnusedPurchasesValue, + }; + } + } +} \ No newline at end of file diff --git a/Shifty.App/Pages/Statistics.razor b/Shifty.App/Pages/Statistics.razor new file mode 100644 index 0000000..33553d6 --- /dev/null +++ b/Shifty.App/Pages/Statistics.razor @@ -0,0 +1,24 @@ +@page "/Statistics" +@using Components +@inject NavigationManager NavManager + +@if (_user is not null && _user.IsInRole("Board")) +{ + +} + +@code { + [CascadingParameter] public Task AuthTask { get; set; } + private System.Security.Claims.ClaimsPrincipal _user; + + protected override async Task OnInitializedAsync() + { + var authState = await AuthTask; + _user = authState.User; + + if (_user is null || !_user.IsInRole("Board")) + { + NavManager.NavigateTo("/"); + } + } +} \ No newline at end of file diff --git a/Shifty.App/Program.cs b/Shifty.App/Program.cs index f38a6d0..9798e67 100644 --- a/Shifty.App/Program.cs +++ b/Shifty.App/Program.cs @@ -58,6 +58,7 @@ public static void ConfigureServices(IServiceCollection services, IConfiguration services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); services.AddScoped(); services.AddScoped(s => s.GetService()); services.AddScoped(); @@ -66,6 +67,7 @@ public static void ConfigureServices(IServiceCollection services, IConfiguration services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); services.AddScoped(); services.AddMudServices(config => diff --git a/Shifty.App/Repositories/IUnusedTicketRepository.cs b/Shifty.App/Repositories/IUnusedTicketRepository.cs new file mode 100644 index 0000000..dd81d9c --- /dev/null +++ b/Shifty.App/Repositories/IUnusedTicketRepository.cs @@ -0,0 +1,15 @@ +using System.Collections; +using System.Collections.Generic; +using System.Threading.Tasks; +using LanguageExt; +using LanguageExt.Common; +using Shifty.Api.Generated.AnalogCoreV1; +using Shifty.Api.Generated.AnalogCoreV2; + +namespace Shifty.App.Repositories +{ + public interface IUnusedTicketRepository + { + Task>> GetTickets(UnusedClipsRequest unusedClipsRequest); + } +} \ No newline at end of file diff --git a/Shifty.App/Repositories/UnusedTicketsRepository.cs b/Shifty.App/Repositories/UnusedTicketsRepository.cs new file mode 100644 index 0000000..8b76bb0 --- /dev/null +++ b/Shifty.App/Repositories/UnusedTicketsRepository.cs @@ -0,0 +1,25 @@ +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using LanguageExt; +using Shifty.Api.Generated.AnalogCoreV2; +using static LanguageExt.Prelude; + +namespace Shifty.App.Repositories +{ + public class UnusedTicketRepository : IUnusedTicketRepository + { + private readonly AnalogCoreV2 _client; + + public UnusedTicketRepository(AnalogCoreV2 client) + { + _client = client; + } + + public async Task>> GetTickets(UnusedClipsRequest unusedClipsRequest) + { + return await TryAsync(async () => (await _client.ApiV2StatisticsUnusedClipsAsync(unusedClipsRequest)).AsEnumerable()); + } + } +} \ No newline at end of file diff --git a/Shifty.App/Services/IUnusedTicketsService.cs b/Shifty.App/Services/IUnusedTicketsService.cs new file mode 100644 index 0000000..ad13391 --- /dev/null +++ b/Shifty.App/Services/IUnusedTicketsService.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using LanguageExt; +using LanguageExt.Common; +using MudBlazor; +using Shifty.Api.Generated.AnalogCoreV1; +using Shifty.Api.Generated.AnalogCoreV2; +using Shifty.App.DomainModels; + +namespace Shifty.App.Services +{ + public interface IUnusedTicketsService + { + /// + /// Queries unused tickets + /// + /// The first date to retrieve unused tickets from + /// The last date to retrieve unused tickets to + /// A list of unused tickets grouped by product + Task>> GetUnusedTickets(DateTimeOffset from, DateTimeOffset to); + } +} \ No newline at end of file diff --git a/Shifty.App/Services/UnusedTicketsService.cs b/Shifty.App/Services/UnusedTicketsService.cs new file mode 100644 index 0000000..e043ccb --- /dev/null +++ b/Shifty.App/Services/UnusedTicketsService.cs @@ -0,0 +1,30 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using LanguageExt; +using Shifty.Api.Generated.AnalogCoreV2; +using Shifty.App.DomainModels; +using Shifty.App.Repositories; + +namespace Shifty.App.Services +{ + public class UnusedTicketsService : IUnusedTicketsService + { + private readonly IUnusedTicketRepository _unusedTicketRepository; + + public UnusedTicketsService(IUnusedTicketRepository unusedTicketRepository) + { + _unusedTicketRepository = unusedTicketRepository; + } + + public async Task>> GetUnusedTickets(DateTimeOffset from, DateTimeOffset to) + { + return await _unusedTicketRepository + .GetTickets(new UnusedClipsRequest(){ + StartDate = from, + EndDate = to + }) + .Map(x => x.Map(UnusedTicket.FromDto)); + } + } +} \ No newline at end of file diff --git a/Shifty.App/Shared/NavMenu.razor b/Shifty.App/Shared/NavMenu.razor index 5fe57e7..628842d 100644 --- a/Shifty.App/Shared/NavMenu.razor +++ b/Shifty.App/Shared/NavMenu.razor @@ -3,12 +3,16 @@ Home - @if (_user is not null && _user.IsInRole("Board")) + @if (_user is not null && (_user.IsInRole("Board") || _user.IsInRole("Manager"))) { Issue vouchers + } + @if (_user is not null && _user.IsInRole("Board")) + { Product Management Menu Item Management Manage users + Statistics } Logout diff --git a/Shifty.Generated.ApiClient/OpenApiSpecs/AnalogCoreV2.json b/Shifty.Generated.ApiClient/OpenApiSpecs/AnalogCoreV2.json index 94a53fc..53463f8 100644 --- a/Shifty.Generated.ApiClient/OpenApiSpecs/AnalogCoreV2.json +++ b/Shifty.Generated.ApiClient/OpenApiSpecs/AnalogCoreV2.json @@ -384,6 +384,61 @@ ] } }, + "/api/v2/statistics/unused-clips": { + "post": { + "tags": [ + "AdminStatistics" + ], + "summary": "Sum unused clip cards within a given period per productId", + "operationId": "AdminStatistics_GetUnusedClips", + "requestBody": { + "x-name": "unusedClipsRequest", + "description": "Request object containing start and end date of the query", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnusedClipsRequest" + } + } + }, + "required": true, + "x-position": 1 + }, + "responses": { + "200": { + "description": " Products with tickets that match the criteria ", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/UnusedClipsResponse" + } + } + } + } + }, + "401": { + "description": " Invalid credentials ", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + } + }, + "security": [ + { + "jwt": [] + }, + { + "apikey": [] + } + ] + } + }, "/api/v2/appconfig": { "get": { "tags": ["AppConfig"], @@ -1737,6 +1792,55 @@ "x-enumNames": ["Active", "Deleted", "PendingActivition"], "enum": ["Active", "Deleted", "PendingActivition"] }, + "UnusedClipsResponse": { + "type": "object", + "description": "Initialize a response with unused clips data", + "additionalProperties": false, + "properties": { + "productId": { + "type": "integer", + "description": "The id of the product", + "format": "int32", + "example": 1 + }, + "productName": { + "type": "string", + "description": "The name of the product", + "example": "Americano " + }, + "ticketsLeft": { + "type": "integer", + "description": "The number of tickets unused in a purchase", + "format": "int32", + "example": 8 + }, + "unusedPurchasesValue": { + "type": "number", + "description": "The value of the unused purchases of a given product", + "format": "decimal", + "example": 40.2 + } + } + }, + "UnusedClipsRequest": { + "type": "object", + "description": "Initialize a request for data with unused clips.", + "additionalProperties": false, + "properties": { + "startDate": { + "type": "string", + "description": "The start date of unused tickets query.", + "format": "date-time", + "example": "2021-02-08" + }, + "endDate": { + "type": "string", + "description": "The end date of unused tickets query.", + "format": "date-time", + "example": "2024-02-08" + } + } + }, "AppConfig": { "type": "object", "description": "App Configuration",