Skip to content

Commit

Permalink
feat: product statistics
Browse files Browse the repository at this point in the history
  • Loading branch information
losolio committed Apr 22, 2024
1 parent 6d98a35 commit aebc0c0
Show file tree
Hide file tree
Showing 16 changed files with 208 additions and 36 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ Task<Paging<Order>> ListOrdersAsync(
CancellationToken cancellationToken = default);


Task<List<ProductOrdersSummaryDto>> GetProductOrdersSummaryAsync(
Task<ProductDeliverySummaryDto> GetProductDeliverySummaryAsync(
int productId,
CancellationToken cancellationToken = default);

Expand Down
93 changes: 72 additions & 21 deletions apps/api/src/Eventuras.Services/Orders/OrderRetrievalService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,14 @@
using System.Threading.Tasks;
using Eventuras.Domain;
using Eventuras.Infrastructure;
using Eventuras.Servcies.Registrations;
using Eventuras.Services.Events;
using Eventuras.Services.Events.Products;
using Eventuras.Services.Exceptions;
using Eventuras.Services.Organizations;
using Eventuras.Services.Users;
using Microsoft.EntityFrameworkCore;
using static Eventuras.Domain.Registration;

namespace Eventuras.Services.Orders;

Expand All @@ -18,12 +21,14 @@ public class OrderRetrievalService : IOrderRetrievalService
private readonly ApplicationDbContext _context;
private readonly IOrderAccessControlService _orderAccessControlService;
private readonly IOrganizationAccessControlService _organizationAccessControlService;
private readonly IProductRetrievalService _productRetrievalService;
private readonly ICurrentOrganizationAccessorService _currentOrganizationAccessorService;

public OrderRetrievalService(
ApplicationDbContext context,
IOrderAccessControlService orderAccessControlService,
IOrganizationAccessControlService organizationAccessControlService,
IProductRetrievalService productRetrievalService,
ICurrentOrganizationAccessorService currentOrganizationAccessorService)
{
_context = context ?? throw
Expand All @@ -35,9 +40,11 @@ public OrderRetrievalService(
_organizationAccessControlService = organizationAccessControlService ?? throw
new ArgumentNullException(nameof(organizationAccessControlService));

_productRetrievalService = productRetrievalService ?? throw
new ArgumentNullException(nameof(productRetrievalService));

_currentOrganizationAccessorService = currentOrganizationAccessorService ?? throw
new ArgumentNullException(nameof(currentOrganizationAccessorService));

}

public async Task<Order> GetOrderByIdAsync(int id,
Expand Down Expand Up @@ -77,39 +84,83 @@ public async Task<Paging<Order>> ListOrdersAsync(
}


public async Task<List<ProductOrdersSummaryDto>> GetProductOrdersSummaryAsync(int productId, CancellationToken cancellationToken)
public async Task<ProductDeliverySummaryDto> GetProductDeliverySummaryAsync(int productId, CancellationToken cancellationToken)
{
var organization = await _currentOrganizationAccessorService.GetCurrentOrganizationAsync();
await _organizationAccessControlService.CheckOrganizationReadAccessAsync(organization.OrganizationId);

// Fetch OrderLines with the specified ProductId and include necessary related data
// Fetch orders and related data
var orderLines = await _context.OrderLines
.Include(ol => ol.Order)
.ThenInclude(o => o.Registration)
.ThenInclude(r => r.User)
.Where(ol => ol.ProductId == productId)
.ToListAsync(cancellationToken);

// Group OrderLines by Registration to build summary
var groupedRegistrations = orderLines
.GroupBy(ol => ol.Order.Registration)
.Select(group => new ProductOrdersSummaryDto
// Calculate ByStatus counts
var byStatus = orderLines
.GroupBy(ol => ol.Order.Registration.Status)
.Aggregate(new ByRegistrationStatus(), (acc, group) =>
{
RegistrationId = group.Key.RegistrationId,
RegistrationStatus = group.Key.Status,
User = new UserSummaryDto
var status = group.Key;

switch (status)
{
UserId = group.Key.User.Id,
Name = group.Key.User.Name,
PhoneNumber = group.Key.User.PhoneNumber,
Email = group.Key.User.Email
},
OrderIds = group.Select(ol => ol.Order.OrderId).Distinct().ToArray(),
SumQuantity = group.Sum(ol => ol.Quantity)
})
.ToList();

return groupedRegistrations;
case RegistrationStatus.Draft:
acc.Draft += group.Count();
break;
case RegistrationStatus.Cancelled:
acc.Cancelled += group.Count();
break;
case RegistrationStatus.Verified:
acc.Verified += group.Count();
break;
case RegistrationStatus.NotAttended:
acc.NotAttended += group.Count();
break;
case RegistrationStatus.Attended:
acc.Attended += group.Count();
break;
case RegistrationStatus.Finished:
acc.Finished += group.Count();
break;
case RegistrationStatus.WaitingList:
acc.WaitingList += group.Count();
break;
}

return acc;
});

// Create ProductDeliverySummaryDto with statistics
var product = await _productRetrievalService.GetProductByIdAsync(productId);
var productDeliverySummary = new ProductDeliverySummaryDto
{
Product = ProductSummaryDto.FromProduct(product),
OrderSummary = orderLines
.GroupBy(ol => ol.Order.Registration)
.Select(group => new ProductOrdersSummaryDto
{
RegistrationId = group.Key.RegistrationId,
RegistrationStatus = group.Key.Status,
User = new UserSummaryDto
{
UserId = group.Key.User.Id,
Name = group.Key.User.Name,
PhoneNumber = group.Key.User.PhoneNumber,
Email = group.Key.User.Email
},
OrderIds = group.Select(ol => ol.Order.OrderId).Distinct().ToArray(),
SumQuantity = group.Sum(ol => ol.Quantity)
})
.ToList(),
Statistics = new ProductStatisticsDto
{
ByRegistrationStatus = byStatus
}
};

return productDeliverySummary;
}


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
using Eventuras.Services.Orders;
using Eventuras.Services.Users;

namespace Eventuras.WebApi.Controllers.v3.Products;
namespace Eventuras.Services.Orders;

public class ProductSummaryDto
{
Expand Down Expand Up @@ -44,4 +44,5 @@ public class ProductDeliverySummaryDto
{
public ProductSummaryDto Product { get; set; }
public List<ProductOrdersSummaryDto> OrderSummary { get; set; }
public ProductStatisticsDto Statistics { get; set; }
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@

using Eventuras.Servcies.Registrations;
using Eventuras.Services.Users;
using static Eventuras.Domain.Registration;

Expand Down
19 changes: 19 additions & 0 deletions apps/api/src/Eventuras.Services/Orders/ProductStatisticsDto.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
using Eventuras.Servcies.Registrations;

namespace Eventuras.Services.Orders;

public class ByRegistrationStatus
{
public int Draft { get; set; }
public int Cancelled { get; set; }
public int Verified { get; set; }
public int NotAttended { get; set; }
public int Attended { get; set; }
public int Finished { get; set; }
public int WaitingList { get; set; }
}

public class ProductStatisticsDto
{
public ByRegistrationStatus ByRegistrationStatus { get; set; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
using Microsoft.AspNetCore.JsonPatch;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using Swashbuckle.AspNetCore.Annotations;

namespace Eventuras.WebApi.Controllers.v3.Events.Statistics;

Expand All @@ -37,6 +38,11 @@ public EventStatisticsController(
}

[HttpGet("statistics")]
[ProducesResponseType(typeof(EventStatisticsDto), 200)]
[SwaggerOperation(
Summary = "Event statistics",
Description = "Returns a summary of the registrations for the event."
)]
public async Task<ActionResult<EventStatisticsDto>> GetEventStatistics(int eventId, CancellationToken cancellationToken)
{
var registrationStatistics = await _registrationRetrievalService.GetRegistrationStatisticsAsync(eventId, cancellationToken);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
namespace Eventuras.WebApi.Controllers.v3.Products;

[ApiVersion("3")]
[Authorize]
[Authorize(Policy = Constants.Auth.AdministratorRole)]
[Route("v{version:apiVersion}/products")]
[ApiController]
public class ProductsController : ControllerBase
Expand All @@ -28,20 +28,18 @@ IOrderRetrievalService orderRetrievalService


[HttpGet("{productId:int}/summary")]
[ProducesResponseType(typeof(ProductDeliverySummaryDto), 200)]
public async Task<ActionResult<ProductDeliverySummaryDto>> GetProductDeliverySummary(int productId, CancellationToken cancellationToken = default)
{
var product = await _productRetrievalService.GetProductByIdAsync(productId, cancellationToken: cancellationToken);
var productDto = ProductSummaryDto.FromProduct(product);

var orderSummaries = await _orderRetrievalService.GetProductOrdersSummaryAsync(productId, cancellationToken);

var productDeliverySummaryDto = new ProductDeliverySummaryDto
// check that the product exists
var product = await _productRetrievalService.GetProductByIdAsync(productId);
if (product == null)
{
Product = productDto,
OrderSummary = orderSummaries
};
return NotFound();
}

return Ok(productDeliverySummaryDto);
var deliverySummary = await _orderRetrievalService.GetProductDeliverySummaryAsync(productId, cancellationToken);
return Ok(deliverySummary);
}


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ public ConfigureSwaggerOptions(IApiVersionDescriptionProvider provider) =>

public void Configure(SwaggerGenOptions options)
{
options.EnableAnnotations();

foreach (var description in provider.ApiVersionDescriptions)
{
options.SwaggerDoc(
Expand Down
19 changes: 18 additions & 1 deletion apps/web/src/app/admin/events/[id]/products/[productId]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Container, Heading, Section } from '@eventuras/ui';
import { Container, Heading, Section, Text } from '@eventuras/ui';
import { headers } from 'next/headers';
import createTranslation from 'next-translate/createTranslation';

Expand Down Expand Up @@ -28,6 +28,19 @@ const EventProducts: React.FC<EventProductsPage> = async ({ params }) => {
})
);

const byRegistrationStatus = productSummary.value?.statistics?.byRegistrationStatus;
const totals = {
active:
(byRegistrationStatus?.draft ?? 0) +
(byRegistrationStatus?.verified ?? 0) +
(byRegistrationStatus?.waitingList ?? 0) +
(byRegistrationStatus?.attended ?? 0) +
(byRegistrationStatus?.notAttended ?? 0) +
(byRegistrationStatus?.finished ?? 0),
cancelled: byRegistrationStatus?.cancelled ?? 0,
waitingList: byRegistrationStatus?.waitingList ?? 0,
};

return (
<>
<Section className="bg-white dark:bg-black py-10">
Expand All @@ -42,6 +55,10 @@ const EventProducts: React.FC<EventProductsPage> = async ({ params }) => {
>
{t('admin:products.labels.editProducts')}
</Link>
<Text className="py-3">
Active {totals.active} &mdash; Cancelled {totals.cancelled} &mdash; Waiting list{' '}
{totals.waitingList}.
</Text>
</Container>
</Section>
<Section className="py-10">
Expand Down
2 changes: 1 addition & 1 deletion libs/sdk/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@eventuras/sdk",
"version": "0.8.1",
"version": "0.8.2",
"description": "Eventuras Typescript SDK",
"type": "module",
"main": "src/index.ts",
Expand Down
2 changes: 2 additions & 0 deletions libs/sdk/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export { CancelablePromise, CancelError } from './core/CancelablePromise';
export { OpenAPI } from './core/OpenAPI';
export type { OpenAPIConfig } from './core/OpenAPI';

export type { ByRegistrationStatus } from './models/ByRegistrationStatus';
export type { ByStatus } from './models/ByStatus';
export type { ByType } from './models/ByType';
export type { CalendarSystem } from './models/CalendarSystem';
Expand Down Expand Up @@ -71,6 +72,7 @@ export type { ProductDto } from './models/ProductDto';
export type { ProductFormDto } from './models/ProductFormDto';
export type { ProductOrderDto } from './models/ProductOrderDto';
export type { ProductOrdersSummaryDto } from './models/ProductOrdersSummaryDto';
export type { ProductStatisticsDto } from './models/ProductStatisticsDto';
export type { ProductSummaryDto } from './models/ProductSummaryDto';
export type { ProductVariantDto } from './models/ProductVariantDto';
export { ProductVisibility } from './models/ProductVisibility';
Expand Down
14 changes: 14 additions & 0 deletions libs/sdk/src/models/ByRegistrationStatus.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type ByRegistrationStatus = {
draft?: number;
cancelled?: number;
verified?: number;
notAttended?: number;
attended?: number;
finished?: number;
waitingList?: number;
};

2 changes: 2 additions & 0 deletions libs/sdk/src/models/ProductDeliverySummaryDto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@
/* tslint:disable */
/* eslint-disable */
import type { ProductOrdersSummaryDto } from './ProductOrdersSummaryDto';
import type { ProductStatisticsDto } from './ProductStatisticsDto';
import type { ProductSummaryDto } from './ProductSummaryDto';
export type ProductDeliverySummaryDto = {
product?: ProductSummaryDto;
orderSummary?: Array<ProductOrdersSummaryDto> | null;
statistics?: ProductStatisticsDto;
};

9 changes: 9 additions & 0 deletions libs/sdk/src/models/ProductStatisticsDto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { ByRegistrationStatus } from './ByRegistrationStatus';
export type ProductStatisticsDto = {
byRegistrationStatus?: ByRegistrationStatus;
};

2 changes: 2 additions & 0 deletions libs/sdk/src/services/EventStatisticsService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import type { BaseHttpRequest } from '../core/BaseHttpRequest';
export class EventStatisticsService {
constructor(public readonly httpRequest: BaseHttpRequest) {}
/**
* Event statistics
* Returns a summary of the registrations for the event.
* @returns EventStatisticsDto Success
* @throws ApiError
*/
Expand Down
Loading

0 comments on commit aebc0c0

Please sign in to comment.