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",