feat(pos): implement table-based ordering, kitchen ticket workflow, and table floor plan management

This commit is contained in:
Ho Ngoc Hai
2026-03-05 07:53:00 +07:00
parent 7102b89ef1
commit 0901e91673
22 changed files with 614 additions and 54 deletions

View File

@@ -15,7 +15,8 @@ public record CreateOrderCommand(
List<OrderItemRequest> Items,
decimal? DiscountAmount = null,
string? DiscountType = null,
string? DiscountReference = null
string? DiscountReference = null,
Guid? TableId = null
) : IRequest<CreateOrderResult>;
/// <summary>

View File

@@ -37,7 +37,7 @@ public class CreateOrderCommandHandler : IRequestHandler<CreateOrderCommand, Cre
// EN: Create order aggregate
// VI: Tạo order aggregate
var order = new Order(request.ShopId, request.CustomerId);
var order = new Order(request.ShopId, request.CustomerId, request.TableId);
// EN: Add items to order
// VI: Thêm items vào order

View File

@@ -42,6 +42,7 @@ public record OrderSummaryDto
public Guid Id { get; init; }
public Guid ShopId { get; init; }
public Guid? CustomerId { get; init; }
public Guid? TableId { get; init; }
public string Status { get; init; } = string.Empty;
public decimal TotalAmount { get; init; }
public long ItemCount { get; init; }

View File

@@ -0,0 +1,81 @@
// EN: Query to get active (unpaid) orders for a shop, grouped by table — includes items.
// VI: Query lấy orders chưa thanh toán của shop, nhóm theo bàn — bao gồm items.
using System.Data;
using Dapper;
using MediatR;
using OrderService.API.Application.DTOs;
namespace OrderService.API.Application.Queries;
public record GetActiveTableOrdersQuery(Guid ShopId) : IRequest<List<ActiveTableOrderDto>>;
public record ActiveTableOrderDto
{
public Guid OrderId { get; init; }
public Guid? TableId { get; init; }
public decimal TotalAmount { get; init; }
public DateTime CreatedAt { get; init; }
public List<ActiveTableOrderItemDto> Items { get; init; } = new();
}
public record ActiveTableOrderItemDto
{
public Guid ProductId { get; init; }
public string ProductName { get; init; } = string.Empty;
public int Quantity { get; init; }
public decimal UnitPrice { get; init; }
}
public class GetActiveTableOrdersQueryHandler : IRequestHandler<GetActiveTableOrdersQuery, List<ActiveTableOrderDto>>
{
private readonly IDbConnection _connection;
public GetActiveTableOrdersQueryHandler(IDbConnection connection)
{
_connection = connection ?? throw new ArgumentNullException(nameof(connection));
}
public async Task<List<ActiveTableOrderDto>> Handle(
GetActiveTableOrdersQuery request,
CancellationToken cancellationToken)
{
// EN: Get active orders (status = Validated=2) with table_id for this shop
// VI: Lấy orders đang active (status = Validated=2) có table_id cho shop này
var ordersSql = @"
SELECT o.id AS OrderId, o.table_id AS TableId, o.total_amount AS TotalAmount, o.created_at AS CreatedAt
FROM orders o
WHERE o.shop_id = @ShopId AND o.status_id = 2 AND o.table_id IS NOT NULL
ORDER BY o.created_at";
var orders = (await _connection.QueryAsync<ActiveTableOrderDto>(ordersSql, new { request.ShopId })).ToList();
if (!orders.Any()) return orders;
// EN: Get items for all active orders in one query
// VI: Lấy items cho tất cả active orders trong 1 query
var orderIds = orders.Select(o => o.OrderId).ToArray();
var itemsSql = @"
SELECT oi.order_id AS OrderId, oi.product_id AS ProductId, oi.product_name AS ProductName,
oi.quantity AS Quantity, oi.unit_price AS UnitPrice
FROM order_items oi
WHERE oi.order_id = ANY(@OrderIds)";
var items = await _connection.QueryAsync<(Guid OrderId, Guid ProductId, string ProductName, int Quantity, decimal UnitPrice)>(
itemsSql, new { OrderIds = orderIds });
var itemsByOrder = items.GroupBy(i => i.OrderId).ToDictionary(g => g.Key, g => g.ToList());
return orders.Select(o => o with
{
Items = itemsByOrder.TryGetValue(o.OrderId, out var orderItems)
? orderItems.Select(i => new ActiveTableOrderItemDto
{
ProductId = i.ProductId,
ProductName = i.ProductName,
Quantity = i.Quantity,
UnitPrice = i.UnitPrice
}).ToList()
: new()
}).ToList();
}
}

View File

@@ -44,6 +44,7 @@ public class ListOrdersByShopQueryHandler : IRequestHandler<ListOrdersByShopQuer
o.id AS Id,
o.shop_id AS ShopId,
o.customer_id AS CustomerId,
o.table_id AS TableId,
os.name AS Status,
o.total_amount AS TotalAmount,
o.created_at AS CreatedAt,

View File

@@ -194,6 +194,21 @@ public class OrdersController : ControllerBase
return Ok(result);
}
/// <summary>
/// EN: Get active (unpaid) orders grouped by table for a shop.
/// VI: Lấy orders chưa thanh toán nhóm theo bàn cho shop.
/// </summary>
[HttpGet("active-by-table")]
[ProducesResponseType(typeof(List<ActiveTableOrderDto>), StatusCodes.Status200OK)]
public async Task<ActionResult<List<ActiveTableOrderDto>>> GetActiveTableOrders(
[FromQuery] Guid shopId,
CancellationToken cancellationToken = default)
{
var query = new GetActiveTableOrdersQuery(shopId);
var result = await _mediator.Send(query, cancellationToken);
return Ok(result);
}
/// <summary>
/// EN: Get orders by customer.
/// VI: Lấy orders theo khách hàng.