diff --git a/services/mining-service-net/src/MiningService.API/Application/Queries/GetAdminOverviewQuery.cs b/services/mining-service-net/src/MiningService.API/Application/Queries/GetAdminOverviewQuery.cs index c9a017db..9515d048 100644 --- a/services/mining-service-net/src/MiningService.API/Application/Queries/GetAdminOverviewQuery.cs +++ b/services/mining-service-net/src/MiningService.API/Application/Queries/GetAdminOverviewQuery.cs @@ -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 { - 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 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); } } diff --git a/services/order-service-net/src/OrderService.API/Application/Queries/AdminListOrdersQuery.cs b/services/order-service-net/src/OrderService.API/Application/Queries/AdminListOrdersQuery.cs new file mode 100644 index 00000000..d60ca8a9 --- /dev/null +++ b/services/order-service-net/src/OrderService.API/Application/Queries/AdminListOrdersQuery.cs @@ -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; + +/// +/// EN: Query to list all orders for admins. +/// VI: Query để liệt kê tất cả đơn hàng cho admin. +/// +public record AdminListOrdersQuery( + Guid? ShopId, + Guid? CustomerId, + string? Status, + DateTime? FromDate, + DateTime? ToDate, + decimal? MinAmount, + decimal? MaxAmount, + int Page = 1, + int PageSize = 20 +) : IRequest>; + +/// +/// EN: Handler for AdminListOrdersQuery using Dapper. +/// VI: Handler cho AdminListOrdersQuery dùng Dapper. +/// +public class AdminListOrdersQueryHandler : IRequestHandler> +{ + private readonly IDbConnection _connection; + + public AdminListOrdersQueryHandler(IDbConnection connection) + { + _connection = connection ?? throw new ArgumentNullException(nameof(connection)); + } + + public async Task> 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(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(listSql, parameters); + + return new PagedResult( + orders.ToList(), + totalCount, + request.PageSize, + request.Page); + } + + private static (string whereClause, DynamicParameters parameters) BuildWhereClause(AdminListOrdersQuery request) + { + var conditions = new List(); + 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); + } +} diff --git a/services/order-service-net/src/OrderService.API/Application/Queries/ExportOrdersQuery.cs b/services/order-service-net/src/OrderService.API/Application/Queries/ExportOrdersQuery.cs new file mode 100644 index 00000000..d517b7b9 --- /dev/null +++ b/services/order-service-net/src/OrderService.API/Application/Queries/ExportOrdersQuery.cs @@ -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; + +/// +/// 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. +/// +public record ExportOrdersQuery( + Guid? ShopId, + DateTime? FromDate, + DateTime? ToDate +) : IRequest; + +/// +/// EN: Export payload result. +/// VI: Kết quả payload export. +/// +public record ExportOrdersResult(string FileName, string CsvContent); + +/// +/// EN: Handler for ExportOrdersQuery. +/// VI: Handler cho ExportOrdersQuery. +/// +public class ExportOrdersQueryHandler : IRequestHandler +{ + private readonly IDbConnection _connection; + + public ExportOrdersQueryHandler(IDbConnection connection) + { + _connection = connection ?? throw new ArgumentNullException(nameof(connection)); + } + + public async Task 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(sql, parameters); + + var csvLines = new List + { + "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(); + 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); +} diff --git a/services/order-service-net/src/OrderService.API/Application/Queries/GetOrderStatsQuery.cs b/services/order-service-net/src/OrderService.API/Application/Queries/GetOrderStatsQuery.cs new file mode 100644 index 00000000..6a09e2f8 --- /dev/null +++ b/services/order-service-net/src/OrderService.API/Application/Queries/GetOrderStatsQuery.cs @@ -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; + +/// +/// EN: Query for order statistics. +/// VI: Query cho thống kê đơn hàng. +/// +public record GetOrderStatsQuery( + Guid? ShopId, + DateTime? FromDate, + DateTime? ToDate +) : IRequest; + +/// +/// EN: Order statistics DTO. +/// VI: DTO thống kê đơn hàng. +/// +public record OrderStatsDto( + int TotalOrders, + decimal TotalRevenue, + decimal AverageOrderValue, + Dictionary OrdersByStatus +); + +/// +/// EN: Handler for GetOrderStatsQuery. +/// VI: Handler cho GetOrderStatsQuery. +/// +public class GetOrderStatsQueryHandler : IRequestHandler +{ + private readonly IDbConnection _connection; + + public GetOrderStatsQueryHandler(IDbConnection connection) + { + _connection = connection ?? throw new ArgumentNullException(nameof(connection)); + } + + public async Task 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(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(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(); + 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); +} diff --git a/services/order-service-net/src/OrderService.API/Controllers/AdminOrdersController.cs b/services/order-service-net/src/OrderService.API/Controllers/AdminOrdersController.cs index 8487e259..800d45f4 100644 --- a/services/order-service-net/src/OrderService.API/Controllers/AdminOrdersController.cs +++ b/services/order-service-net/src/OrderService.API/Controllers/AdminOrdersController.cs @@ -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( - new List(), - 0, - pageSize, - page)); + var result = await _mediator.Send(query, cancellationToken); + return Ok(result); } /// @@ -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() - ); - + 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); } } - -/// -/// EN: Order statistics DTO. -/// VI: DTO thống kê orders. -/// -public record OrderStatsDto( - int TotalOrders, - decimal TotalRevenue, - decimal AverageOrderValue, - Dictionary OrdersByStatus -);