feat: implement admin order stats export and mining overview
Co-authored-by: Velik <hongochai10@users.noreply.github.com>
This commit is contained in:
@@ -1,5 +1,8 @@
|
||||
using MediatR;
|
||||
using MiningService.Domain.AggregatesModel.MinerAggregate;
|
||||
using MiningService.Domain.AggregatesModel.CircleAggregate;
|
||||
using MiningService.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace MiningService.API.Application.Queries;
|
||||
|
||||
@@ -21,25 +24,39 @@ public record AdminOverviewDto(
|
||||
|
||||
public class GetAdminOverviewQueryHandler : IRequestHandler<GetAdminOverviewQuery, AdminOverviewDto>
|
||||
{
|
||||
private readonly IMinerRepository _minerRepository;
|
||||
private readonly MiningServiceContext _context;
|
||||
|
||||
public GetAdminOverviewQueryHandler(IMinerRepository minerRepository)
|
||||
public GetAdminOverviewQueryHandler(MiningServiceContext context)
|
||||
{
|
||||
_minerRepository = minerRepository;
|
||||
_context = context ?? throw new ArgumentNullException(nameof(context));
|
||||
}
|
||||
|
||||
public async Task<AdminOverviewDto> Handle(GetAdminOverviewQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
// Note: In production, use Dapper for read-optimized queries
|
||||
// This is a simplified version
|
||||
var totalMiners = await _context.Miners.CountAsync(cancellationToken);
|
||||
var activeMiners = await _context.Miners
|
||||
.CountAsync(miner => miner.Status == MinerStatus.Active, cancellationToken);
|
||||
var minersWithActiveSession = await _context.Miners
|
||||
.CountAsync(miner => miner.ActiveSession != null && miner.ActiveSession.Status == MiningSessionStatus.Active, cancellationToken);
|
||||
var totalPointsMined = await _context.Miners
|
||||
.SumAsync(miner => (decimal?)miner.TotalMinedPoints, cancellationToken) ?? 0m;
|
||||
|
||||
var totalCircles = await _context.Circles.CountAsync(cancellationToken);
|
||||
var validCircles = await _context.Circles
|
||||
.CountAsync(circle => circle.Status == CircleStatus.Active, cancellationToken);
|
||||
|
||||
var totalReferrals = await _context.Referrals.CountAsync(cancellationToken);
|
||||
var activeReferrals = await _context.Referrals
|
||||
.CountAsync(referral => referral.IsActive, cancellationToken);
|
||||
|
||||
return new AdminOverviewDto(
|
||||
TotalMiners: 0, // TODO: Implement with Dapper
|
||||
ActiveMiners: 0,
|
||||
MinersWithActiveSession: 0,
|
||||
TotalPointsMined: 0,
|
||||
TotalCircles: 0,
|
||||
ValidCircles: 0,
|
||||
TotalReferrals: 0,
|
||||
ActiveReferrals: 0);
|
||||
TotalMiners: totalMiners,
|
||||
ActiveMiners: activeMiners,
|
||||
MinersWithActiveSession: minersWithActiveSession,
|
||||
TotalPointsMined: totalPointsMined,
|
||||
TotalCircles: totalCircles,
|
||||
ValidCircles: validCircles,
|
||||
TotalReferrals: totalReferrals,
|
||||
ActiveReferrals: activeReferrals);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
// EN: Query to list all orders for admin with advanced filtering.
|
||||
// VI: Query liệt kê tất cả đơn hàng cho admin với bộ lọc nâng cao.
|
||||
|
||||
using System.Data;
|
||||
using Dapper;
|
||||
using MediatR;
|
||||
using OrderService.API.Application.DTOs;
|
||||
|
||||
namespace OrderService.API.Application.Queries;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Query to list all orders for admins.
|
||||
/// VI: Query để liệt kê tất cả đơn hàng cho admin.
|
||||
/// </summary>
|
||||
public record AdminListOrdersQuery(
|
||||
Guid? ShopId,
|
||||
Guid? CustomerId,
|
||||
string? Status,
|
||||
DateTime? FromDate,
|
||||
DateTime? ToDate,
|
||||
decimal? MinAmount,
|
||||
decimal? MaxAmount,
|
||||
int Page = 1,
|
||||
int PageSize = 20
|
||||
) : IRequest<PagedResult<OrderSummaryDto>>;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Handler for AdminListOrdersQuery using Dapper.
|
||||
/// VI: Handler cho AdminListOrdersQuery dùng Dapper.
|
||||
/// </summary>
|
||||
public class AdminListOrdersQueryHandler : IRequestHandler<AdminListOrdersQuery, PagedResult<OrderSummaryDto>>
|
||||
{
|
||||
private readonly IDbConnection _connection;
|
||||
|
||||
public AdminListOrdersQueryHandler(IDbConnection connection)
|
||||
{
|
||||
_connection = connection ?? throw new ArgumentNullException(nameof(connection));
|
||||
}
|
||||
|
||||
public async Task<PagedResult<OrderSummaryDto>> Handle(
|
||||
AdminListOrdersQuery request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var (whereClause, parameters) = BuildWhereClause(request);
|
||||
|
||||
var countSql = $@"
|
||||
SELECT COUNT(*)
|
||||
FROM orders o
|
||||
INNER JOIN order_statuses os ON o.status_id = os.id
|
||||
{whereClause}";
|
||||
|
||||
var totalCount = await _connection.ExecuteScalarAsync<int>(countSql, parameters);
|
||||
|
||||
var listSql = $@"
|
||||
SELECT
|
||||
o.id AS Id,
|
||||
o.shop_id AS ShopId,
|
||||
o.customer_id AS CustomerId,
|
||||
os.name AS Status,
|
||||
o.total_amount AS TotalAmount,
|
||||
(SELECT COUNT(*) FROM order_items oi WHERE oi.order_id = o.id) AS ItemCount,
|
||||
o.created_at AS CreatedAt
|
||||
FROM orders o
|
||||
INNER JOIN order_statuses os ON o.status_id = os.id
|
||||
{whereClause}
|
||||
ORDER BY o.created_at DESC
|
||||
LIMIT @PageSize OFFSET @Offset";
|
||||
|
||||
parameters.Add("PageSize", request.PageSize);
|
||||
parameters.Add("Offset", (request.Page - 1) * request.PageSize);
|
||||
|
||||
var orders = await _connection.QueryAsync<OrderSummaryDto>(listSql, parameters);
|
||||
|
||||
return new PagedResult<OrderSummaryDto>(
|
||||
orders.ToList(),
|
||||
totalCount,
|
||||
request.PageSize,
|
||||
request.Page);
|
||||
}
|
||||
|
||||
private static (string whereClause, DynamicParameters parameters) BuildWhereClause(AdminListOrdersQuery request)
|
||||
{
|
||||
var conditions = new List<string>();
|
||||
var parameters = new DynamicParameters();
|
||||
|
||||
if (request.ShopId.HasValue)
|
||||
{
|
||||
conditions.Add("o.shop_id = @ShopId");
|
||||
parameters.Add("ShopId", request.ShopId.Value);
|
||||
}
|
||||
|
||||
if (request.CustomerId.HasValue)
|
||||
{
|
||||
conditions.Add("o.customer_id = @CustomerId");
|
||||
parameters.Add("CustomerId", request.CustomerId.Value);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(request.Status))
|
||||
{
|
||||
conditions.Add("os.name = @Status");
|
||||
parameters.Add("Status", request.Status);
|
||||
}
|
||||
|
||||
if (request.FromDate.HasValue)
|
||||
{
|
||||
conditions.Add("o.created_at >= @FromDate");
|
||||
parameters.Add("FromDate", request.FromDate.Value);
|
||||
}
|
||||
|
||||
if (request.ToDate.HasValue)
|
||||
{
|
||||
conditions.Add("o.created_at <= @ToDate");
|
||||
parameters.Add("ToDate", request.ToDate.Value);
|
||||
}
|
||||
|
||||
if (request.MinAmount.HasValue)
|
||||
{
|
||||
conditions.Add("o.total_amount >= @MinAmount");
|
||||
parameters.Add("MinAmount", request.MinAmount.Value);
|
||||
}
|
||||
|
||||
if (request.MaxAmount.HasValue)
|
||||
{
|
||||
conditions.Add("o.total_amount <= @MaxAmount");
|
||||
parameters.Add("MaxAmount", request.MaxAmount.Value);
|
||||
}
|
||||
|
||||
var whereClause = conditions.Count == 0
|
||||
? string.Empty
|
||||
: "WHERE " + string.Join(" AND ", conditions);
|
||||
|
||||
return (whereClause, parameters);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
// EN: Query to export orders for admin.
|
||||
// VI: Query xuất đơn hàng cho admin.
|
||||
|
||||
using System.Data;
|
||||
using Dapper;
|
||||
using MediatR;
|
||||
|
||||
namespace OrderService.API.Application.Queries;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Query to export orders in CSV-friendly structure.
|
||||
/// VI: Query để xuất đơn hàng theo cấu trúc phù hợp CSV.
|
||||
/// </summary>
|
||||
public record ExportOrdersQuery(
|
||||
Guid? ShopId,
|
||||
DateTime? FromDate,
|
||||
DateTime? ToDate
|
||||
) : IRequest<ExportOrdersResult>;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Export payload result.
|
||||
/// VI: Kết quả payload export.
|
||||
/// </summary>
|
||||
public record ExportOrdersResult(string FileName, string CsvContent);
|
||||
|
||||
/// <summary>
|
||||
/// EN: Handler for ExportOrdersQuery.
|
||||
/// VI: Handler cho ExportOrdersQuery.
|
||||
/// </summary>
|
||||
public class ExportOrdersQueryHandler : IRequestHandler<ExportOrdersQuery, ExportOrdersResult>
|
||||
{
|
||||
private readonly IDbConnection _connection;
|
||||
|
||||
public ExportOrdersQueryHandler(IDbConnection connection)
|
||||
{
|
||||
_connection = connection ?? throw new ArgumentNullException(nameof(connection));
|
||||
}
|
||||
|
||||
public async Task<ExportOrdersResult> Handle(
|
||||
ExportOrdersQuery request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var (whereClause, parameters) = BuildWhereClause(request.ShopId, request.FromDate, request.ToDate);
|
||||
|
||||
var sql = $@"
|
||||
SELECT
|
||||
o.id AS Id,
|
||||
o.shop_id AS ShopId,
|
||||
o.customer_id AS CustomerId,
|
||||
os.name AS Status,
|
||||
o.total_amount AS TotalAmount,
|
||||
o.created_at AS CreatedAt
|
||||
FROM orders o
|
||||
INNER JOIN order_statuses os ON o.status_id = os.id
|
||||
{whereClause}
|
||||
ORDER BY o.created_at DESC";
|
||||
|
||||
var rows = await _connection.QueryAsync<ExportOrderRow>(sql, parameters);
|
||||
|
||||
var csvLines = new List<string>
|
||||
{
|
||||
"OrderId,ShopId,CustomerId,Status,TotalAmount,CreatedAt",
|
||||
};
|
||||
|
||||
csvLines.AddRange(rows.Select(row =>
|
||||
$"{row.Id},{row.ShopId},{row.CustomerId},{EscapeCsv(row.Status)},{row.TotalAmount},{row.CreatedAt:O}"));
|
||||
|
||||
var fileName = $"orders_{DateTime.UtcNow:yyyyMMdd_HHmmss}.csv";
|
||||
var csvContent = string.Join(Environment.NewLine, csvLines);
|
||||
return new ExportOrdersResult(fileName, csvContent);
|
||||
}
|
||||
|
||||
private static (string whereClause, DynamicParameters parameters) BuildWhereClause(
|
||||
Guid? shopId,
|
||||
DateTime? fromDate,
|
||||
DateTime? toDate)
|
||||
{
|
||||
var conditions = new List<string>();
|
||||
var parameters = new DynamicParameters();
|
||||
|
||||
if (shopId.HasValue)
|
||||
{
|
||||
conditions.Add("o.shop_id = @ShopId");
|
||||
parameters.Add("ShopId", shopId.Value);
|
||||
}
|
||||
|
||||
if (fromDate.HasValue)
|
||||
{
|
||||
conditions.Add("o.created_at >= @FromDate");
|
||||
parameters.Add("FromDate", fromDate.Value);
|
||||
}
|
||||
|
||||
if (toDate.HasValue)
|
||||
{
|
||||
conditions.Add("o.created_at <= @ToDate");
|
||||
parameters.Add("ToDate", toDate.Value);
|
||||
}
|
||||
|
||||
var whereClause = conditions.Count == 0
|
||||
? string.Empty
|
||||
: "WHERE " + string.Join(" AND ", conditions);
|
||||
|
||||
return (whereClause, parameters);
|
||||
}
|
||||
|
||||
private static string EscapeCsv(string value)
|
||||
{
|
||||
if (string.IsNullOrEmpty(value))
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
if (value.Contains(',') || value.Contains('"') || value.Contains('\n'))
|
||||
{
|
||||
return $"\"{value.Replace("\"", "\"\"")}\"";
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
private record ExportOrderRow(
|
||||
Guid Id,
|
||||
Guid ShopId,
|
||||
Guid? CustomerId,
|
||||
string Status,
|
||||
decimal TotalAmount,
|
||||
DateTime CreatedAt);
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
// EN: Query to get aggregate order statistics for admin.
|
||||
// VI: Query lấy thống kê tổng hợp đơn hàng cho admin.
|
||||
|
||||
using System.Data;
|
||||
using Dapper;
|
||||
using MediatR;
|
||||
|
||||
namespace OrderService.API.Application.Queries;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Query for order statistics.
|
||||
/// VI: Query cho thống kê đơn hàng.
|
||||
/// </summary>
|
||||
public record GetOrderStatsQuery(
|
||||
Guid? ShopId,
|
||||
DateTime? FromDate,
|
||||
DateTime? ToDate
|
||||
) : IRequest<OrderStatsDto>;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Order statistics DTO.
|
||||
/// VI: DTO thống kê đơn hàng.
|
||||
/// </summary>
|
||||
public record OrderStatsDto(
|
||||
int TotalOrders,
|
||||
decimal TotalRevenue,
|
||||
decimal AverageOrderValue,
|
||||
Dictionary<string, int> OrdersByStatus
|
||||
);
|
||||
|
||||
/// <summary>
|
||||
/// EN: Handler for GetOrderStatsQuery.
|
||||
/// VI: Handler cho GetOrderStatsQuery.
|
||||
/// </summary>
|
||||
public class GetOrderStatsQueryHandler : IRequestHandler<GetOrderStatsQuery, OrderStatsDto>
|
||||
{
|
||||
private readonly IDbConnection _connection;
|
||||
|
||||
public GetOrderStatsQueryHandler(IDbConnection connection)
|
||||
{
|
||||
_connection = connection ?? throw new ArgumentNullException(nameof(connection));
|
||||
}
|
||||
|
||||
public async Task<OrderStatsDto> Handle(GetOrderStatsQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
var (whereClause, parameters) = BuildWhereClause(request.ShopId, request.FromDate, request.ToDate);
|
||||
|
||||
var aggregateSql = $@"
|
||||
SELECT
|
||||
COUNT(*) AS TotalOrders,
|
||||
COALESCE(SUM(o.total_amount), 0) AS TotalRevenue,
|
||||
COALESCE(AVG(o.total_amount), 0) AS AverageOrderValue
|
||||
FROM orders o
|
||||
INNER JOIN order_statuses os ON o.status_id = os.id
|
||||
{whereClause}";
|
||||
|
||||
var aggregate = await _connection.QuerySingleAsync<OrderStatsAggregateRow>(aggregateSql, parameters);
|
||||
|
||||
var statusSql = $@"
|
||||
SELECT
|
||||
os.name AS Status,
|
||||
COUNT(*) AS Total
|
||||
FROM orders o
|
||||
INNER JOIN order_statuses os ON o.status_id = os.id
|
||||
{whereClause}
|
||||
GROUP BY os.name";
|
||||
|
||||
var statusRows = await _connection.QueryAsync<OrderStatusCountRow>(statusSql, parameters);
|
||||
var byStatus = statusRows.ToDictionary(row => row.Status, row => row.Total, StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
return new OrderStatsDto(
|
||||
aggregate.TotalOrders,
|
||||
aggregate.TotalRevenue,
|
||||
aggregate.AverageOrderValue,
|
||||
byStatus);
|
||||
}
|
||||
|
||||
private static (string whereClause, DynamicParameters parameters) BuildWhereClause(
|
||||
Guid? shopId,
|
||||
DateTime? fromDate,
|
||||
DateTime? toDate)
|
||||
{
|
||||
var conditions = new List<string>();
|
||||
var parameters = new DynamicParameters();
|
||||
|
||||
if (shopId.HasValue)
|
||||
{
|
||||
conditions.Add("o.shop_id = @ShopId");
|
||||
parameters.Add("ShopId", shopId.Value);
|
||||
}
|
||||
|
||||
if (fromDate.HasValue)
|
||||
{
|
||||
conditions.Add("o.created_at >= @FromDate");
|
||||
parameters.Add("FromDate", fromDate.Value);
|
||||
}
|
||||
|
||||
if (toDate.HasValue)
|
||||
{
|
||||
conditions.Add("o.created_at <= @ToDate");
|
||||
parameters.Add("ToDate", toDate.Value);
|
||||
}
|
||||
|
||||
var whereClause = conditions.Count == 0
|
||||
? string.Empty
|
||||
: "WHERE " + string.Join(" AND ", conditions);
|
||||
|
||||
return (whereClause, parameters);
|
||||
}
|
||||
|
||||
private record OrderStatsAggregateRow(int TotalOrders, decimal TotalRevenue, decimal AverageOrderValue);
|
||||
private record OrderStatusCountRow(string Status, int Total);
|
||||
}
|
||||
@@ -47,38 +47,19 @@ public class AdminOrdersController : ControllerBase
|
||||
[FromQuery] int pageSize = 20,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
// EN: For now, if shopId is provided, use ListOrdersByShopQuery
|
||||
// VI: Tạm thời, nếu shopId được cung cấp, dùng ListOrdersByShopQuery
|
||||
// TODO: Create AdminListOrdersQuery with more filters
|
||||
if (shopId.HasValue)
|
||||
{
|
||||
var query = new ListOrdersByShopQuery(
|
||||
shopId.Value,
|
||||
status,
|
||||
fromDate,
|
||||
toDate,
|
||||
page,
|
||||
pageSize);
|
||||
var result = await _mediator.Send(query, cancellationToken);
|
||||
return Ok(result);
|
||||
}
|
||||
var query = new AdminListOrdersQuery(
|
||||
shopId,
|
||||
customerId,
|
||||
status,
|
||||
fromDate,
|
||||
toDate,
|
||||
minAmount,
|
||||
maxAmount,
|
||||
page,
|
||||
pageSize);
|
||||
|
||||
// EN: If customerId is provided
|
||||
// VI: Nếu customerId được cung cấp
|
||||
if (customerId.HasValue)
|
||||
{
|
||||
var query = new GetOrdersByCustomerQuery(customerId.Value, page, pageSize);
|
||||
var result = await _mediator.Send(query, cancellationToken);
|
||||
return Ok(result);
|
||||
}
|
||||
|
||||
// EN: Return empty for now
|
||||
// VI: Trả về rỗng tạm thời
|
||||
return Ok(new PagedResult<OrderSummaryDto>(
|
||||
new List<OrderSummaryDto>(),
|
||||
0,
|
||||
pageSize,
|
||||
page));
|
||||
var result = await _mediator.Send(query, cancellationToken);
|
||||
return Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -95,16 +76,8 @@ public class AdminOrdersController : ControllerBase
|
||||
{
|
||||
_logger.LogInformation("EN: Getting order stats / VI: Lấy thống kê orders");
|
||||
|
||||
// TODO: Implement GetOrderStatsQuery
|
||||
// EN: Return mock data for now
|
||||
// VI: Trả về dữ liệu giả tạm thời
|
||||
var stats = new OrderStatsDto(
|
||||
TotalOrders: 0,
|
||||
TotalRevenue: 0,
|
||||
AverageOrderValue: 0,
|
||||
OrdersByStatus: new Dictionary<string, int>()
|
||||
);
|
||||
|
||||
var query = new GetOrderStatsQuery(shopId, fromDate, toDate);
|
||||
var stats = await _mediator.Send(query, cancellationToken);
|
||||
return Ok(stats);
|
||||
}
|
||||
|
||||
@@ -122,23 +95,10 @@ public class AdminOrdersController : ControllerBase
|
||||
{
|
||||
_logger.LogInformation("EN: Exporting orders / VI: Xuất orders");
|
||||
|
||||
// TODO: Implement export functionality
|
||||
// EN: Return empty CSV for now
|
||||
// VI: Trả về CSV rỗng tạm thời
|
||||
var csv = "OrderId,ShopId,CustomerId,Status,TotalAmount,CreatedAt\n";
|
||||
var bytes = System.Text.Encoding.UTF8.GetBytes(csv);
|
||||
var query = new ExportOrdersQuery(shopId, fromDate, toDate);
|
||||
var export = await _mediator.Send(query, cancellationToken);
|
||||
var bytes = System.Text.Encoding.UTF8.GetBytes(export.CsvContent);
|
||||
|
||||
return File(bytes, "text/csv", $"orders_{DateTime.UtcNow:yyyyMMdd}.csv");
|
||||
return File(bytes, "text/csv", export.FileName);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EN: Order statistics DTO.
|
||||
/// VI: DTO thống kê orders.
|
||||
/// </summary>
|
||||
public record OrderStatsDto(
|
||||
int TotalOrders,
|
||||
decimal TotalRevenue,
|
||||
decimal AverageOrderValue,
|
||||
Dictionary<string, int> OrdersByStatus
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user