From 07d531727621237224fabf55c526aec94df73f7e Mon Sep 17 00:00:00 2001 From: A-Guldborg Date: Mon, 15 Apr 2024 22:31:22 +0200 Subject: [PATCH 1/7] Adds unused tickets statistics --- Shifty.App/Components/UnusedTickets.razor | 95 +++++++++++++++++++ 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 +- 10 files changed, 244 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..267b6e6 --- /dev/null +++ b/Shifty.App/Components/UnusedTickets.razor @@ -0,0 +1,95 @@ +@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 matching records found + + + +@code +{ + private IEnumerable Items; + private bool _loading = false; + private DateRange _queryDateRange = new(){ Start = DateTime.Today.AddMonths(-1), End = DateTime.Today}; + + private async Task LoadUnusedTickets(DateRange queryDateRange) + { + _loading = true; + if (queryDateRange.Start is null || queryDateRange.End is null) return; + + _queryDateRange.Start = queryDateRange.Start; + _queryDateRange.End = queryDateRange.End; + 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() + { + _queryDateRange.Start = new(DateTime.Today.Year, 1, 1); + _queryDateRange.End = new(DateTime.Today.Year, 12, 31); + 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..18b3d92 --- /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(t => UnusedTicket.FromDto(t))); + } + } +} \ No newline at end of file diff --git a/Shifty.App/Shared/NavMenu.razor b/Shifty.App/Shared/NavMenu.razor index 5fe57e7..27eb641 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 is not null && _user.IsInRole("Manager")) { Issue vouchers + } + @if (_user is not null && _user.IsInRole("Board")) + { Product Management Menu Item Management Manage users + Statistics } Logout From f8cc58d7a2dabfea2f3f800270f3ad283013b185 Mon Sep 17 00:00:00 2001 From: A-Guldborg Date: Mon, 15 Apr 2024 22:49:07 +0200 Subject: [PATCH 2/7] Add API v2 specs --- .../OpenApiSpecs/AnalogCoreV2.json | 104 ++++++++++++++++++ 1 file changed, 104 insertions(+) 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", From e844f69cd8ba6a74ac209781ae8c8f602442ed6d Mon Sep 17 00:00:00 2001 From: A-Guldborg Date: Tue, 23 Apr 2024 15:20:09 +0200 Subject: [PATCH 3/7] Fix setting dates twice on load --- Shifty.App/Components/UnusedTickets.razor | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/Shifty.App/Components/UnusedTickets.razor b/Shifty.App/Components/UnusedTickets.razor index 267b6e6..00dc65a 100644 --- a/Shifty.App/Components/UnusedTickets.razor +++ b/Shifty.App/Components/UnusedTickets.razor @@ -43,7 +43,7 @@ { private IEnumerable Items; private bool _loading = false; - private DateRange _queryDateRange = new(){ Start = DateTime.Today.AddMonths(-1), End = DateTime.Today}; + private DateRange _queryDateRange = new(){ Start = new(DateTime.Today.Year, 1, 1), End = new(DateTime.Today.Year, 12, 31)}; private async Task LoadUnusedTickets(DateRange queryDateRange) { @@ -68,8 +68,6 @@ protected override async Task OnInitializedAsync() { - _queryDateRange.Start = new(DateTime.Today.Year, 1, 1); - _queryDateRange.End = new(DateTime.Today.Year, 12, 31); await LoadUnusedTickets(_queryDateRange); } From ca7fe1ac0859b6adc78bc6daa56d10650bc857a7 Mon Sep 17 00:00:00 2001 From: A-Guldborg Date: Tue, 23 Apr 2024 15:22:31 +0200 Subject: [PATCH 4/7] Set full object and not each individual property --- Shifty.App/Components/UnusedTickets.razor | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Shifty.App/Components/UnusedTickets.razor b/Shifty.App/Components/UnusedTickets.razor index 00dc65a..1e1d14c 100644 --- a/Shifty.App/Components/UnusedTickets.razor +++ b/Shifty.App/Components/UnusedTickets.razor @@ -50,8 +50,7 @@ _loading = true; if (queryDateRange.Start is null || queryDateRange.End is null) return; - _queryDateRange.Start = queryDateRange.Start; - _queryDateRange.End = queryDateRange.End; + _queryDateRange = queryDateRange; var result = await _unusedTicketsService.GetUnusedTickets(queryDateRange.Start.Value, queryDateRange.End.Value); result.Match( From fd6de96d52fb2da26bf6456a91b2a9e4945a87b7 Mon Sep 17 00:00:00 2001 From: A-Guldborg Date: Tue, 23 Apr 2024 15:33:05 +0200 Subject: [PATCH 5/7] Replace lambda function with method signature --- Shifty.App/Services/UnusedTicketsService.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Shifty.App/Services/UnusedTicketsService.cs b/Shifty.App/Services/UnusedTicketsService.cs index 18b3d92..e043ccb 100644 --- a/Shifty.App/Services/UnusedTicketsService.cs +++ b/Shifty.App/Services/UnusedTicketsService.cs @@ -24,7 +24,7 @@ public async Task>> GetUnusedTickets(DateTimeOffse StartDate = from, EndDate = to }) - .Map(x => x.Map(t => UnusedTicket.FromDto(t))); + .Map(x => x.Map(UnusedTicket.FromDto)); } } } \ No newline at end of file From a32c244323d5d947b7af1273429bc34b1e54cb4f Mon Sep 17 00:00:00 2001 From: A-Guldborg Date: Tue, 23 Apr 2024 16:10:46 +0200 Subject: [PATCH 6/7] Remove duplicate null check --- Shifty.App/Shared/NavMenu.razor | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Shifty.App/Shared/NavMenu.razor b/Shifty.App/Shared/NavMenu.razor index 27eb641..628842d 100644 --- a/Shifty.App/Shared/NavMenu.razor +++ b/Shifty.App/Shared/NavMenu.razor @@ -3,7 +3,7 @@ Home - @if (_user is not null && _user.IsInRole("Board") || _user is not null && _user.IsInRole("Manager")) + @if (_user is not null && (_user.IsInRole("Board") || _user.IsInRole("Manager"))) { Issue vouchers } From 48b01015f6bbb88a06cc70483b21492a603c0754 Mon Sep 17 00:00:00 2001 From: A-Guldborg Date: Tue, 23 Apr 2024 17:35:31 +0200 Subject: [PATCH 7/7] Change wording of status 204 --- Shifty.App/Components/UnusedTickets.razor | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Shifty.App/Components/UnusedTickets.razor b/Shifty.App/Components/UnusedTickets.razor index 1e1d14c..e2773bf 100644 --- a/Shifty.App/Components/UnusedTickets.razor +++ b/Shifty.App/Components/UnusedTickets.razor @@ -35,7 +35,7 @@ - No matching records found + No records found for the given time period