feat: Implement kitchen ticket and session management in FnbEngine, add booking-related controllers and a generic API response in BookingService, and update Dockerfiles.

This commit is contained in:
Ho Ngoc Hai
2026-01-18 02:56:43 +07:00
parent 83a8db2942
commit 593457a9e3
21 changed files with 969 additions and 5 deletions

View File

@@ -0,0 +1,95 @@
// EN: Admin Appointments Controller - Admin management and analytics APIs.
// VI: Controller Admin Appointments - APIs quản trị và thống kê.
using Asp.Versioning;
using BookingService.API.Application.DTOs;
using BookingService.API.Application.Queries;
using BookingService.API.Models.Responses;
using MediatR;
using Microsoft.AspNetCore.Mvc;
namespace BookingService.API.Controllers.Admin;
[ApiController]
[ApiVersion("1.0")]
[Route("api/v{version:apiVersion}/admin/appointments")]
[Produces("application/json")]
// [Authorize(Roles = "Admin,ShopOwner")] // TODO: Add authorization
public class AdminAppointmentsController : ControllerBase
{
private readonly IMediator _mediator;
private readonly ILogger<AdminAppointmentsController> _logger;
public AdminAppointmentsController(IMediator mediator, ILogger<AdminAppointmentsController> logger)
{
_mediator = mediator ?? throw new ArgumentNullException(nameof(mediator));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
/// <summary>
/// EN: Get all appointments with advanced filtering for admin.
/// VI: Lấy tất cả cuộc hẹn với bộ lọc nâng cao cho admin.
/// </summary>
[HttpGet]
[ProducesResponseType(typeof(ApiResponse<PaginatedList<AppointmentDto>>), StatusCodes.Status200OK)]
public async Task<ActionResult<ApiResponse<PaginatedList<AppointmentDto>>>> GetAllAppointments(
[FromQuery] Guid? shopId,
[FromQuery] DateTime? startDate,
[FromQuery] DateTime? endDate,
[FromQuery] string? status,
[FromQuery] int page = 1,
[FromQuery] int pageSize = 50,
CancellationToken cancellationToken = default)
{
if (!shopId.HasValue)
{
return BadRequest(ApiResponse<PaginatedList<AppointmentDto>>.Fail("ShopId is required"));
}
var query = new GetAppointmentsByShopQuery(shopId.Value, startDate, endDate, status, page, pageSize);
var result = await _mediator.Send(query, cancellationToken);
return Ok(ApiResponse<PaginatedList<AppointmentDto>>.Ok(result));
}
/// <summary>
/// EN: Get appointment statistics for a date range.
/// VI: Lấy thống kê cuộc hẹn theo khoảng thời gian.
/// </summary>
[HttpGet("statistics")]
[ProducesResponseType(typeof(ApiResponse<AppointmentStatisticsDto>), StatusCodes.Status200OK)]
public async Task<ActionResult<ApiResponse<AppointmentStatisticsDto>>> GetStatistics(
[FromQuery] Guid shopId,
[FromQuery] DateTime startDate,
[FromQuery] DateTime endDate,
CancellationToken cancellationToken = default)
{
// EN: Query appointments for date range
// VI: Query appointments trong khoảng thời gian
var query = new GetAppointmentsByShopQuery(shopId, startDate, endDate, null, 1, int.MaxValue);
var appointments = await _mediator.Send(query, cancellationToken);
// EN: Calculate statistics
// VI: Tính toán thống kê
var total = appointments.TotalCount;
var pending = appointments.Items.Count(a => a.Status == "Pending");
var confirmed = appointments.Items.Count(a => a.Status == "Confirmed");
var completed = appointments.Items.Count(a => a.Status == "Completed");
var cancelled = appointments.Items.Count(a => a.Status == "Cancelled");
var noShow = appointments.Items.Count(a => a.Status == "NoShow");
var statistics = new AppointmentStatisticsDto
{
TotalAppointments = total,
PendingAppointments = pending,
ConfirmedAppointments = confirmed,
CompletedAppointments = completed,
CancelledAppointments = cancelled,
NoShowAppointments = noShow,
CompletionRate = total > 0 ? (decimal)completed / total * 100 : 0,
CancellationRate = total > 0 ? (decimal)cancelled / total * 100 : 0
};
return Ok(ApiResponse<AppointmentStatisticsDto>.Ok(statistics));
}
}

View File

@@ -0,0 +1,44 @@
// EN: Admin Resources Controller - Admin resource management APIs.
// VI: Controller Admin Resources - APIs quản trị tài nguyên.
using Asp.Versioning;
using BookingService.API.Application.DTOs;
using BookingService.API.Application.Queries;
using BookingService.API.Models.Responses;
using MediatR;
using Microsoft.AspNetCore.Mvc;
namespace BookingService.API.Controllers.Admin;
[ApiController]
[ApiVersion("1.0")]
[Route("api/v{version:apiVersion}/admin/resources")]
[Produces("application/json")]
// [Authorize(Roles = "Admin,ShopOwner")] // TODO: Add authorization
public class AdminResourcesController : ControllerBase
{
private readonly IMediator _mediator;
private readonly ILogger<AdminResourcesController> _logger;
public AdminResourcesController(IMediator mediator, ILogger<AdminResourcesController> logger)
{
_mediator = mediator ?? throw new ArgumentNullException(nameof(mediator));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
/// <summary>
/// EN: Get all resources for a shop (including inactive).
/// VI: Lấy tất cả tài nguyên của shop (bao gồm cả inactive).
/// </summary>
[HttpGet("{shopId:guid}")]
[ProducesResponseType(typeof(ApiResponse<List<ResourceDto>>), StatusCodes.Status200OK)]
public async Task<ActionResult<ApiResponse<List<ResourceDto>>>> GetAllResources(
Guid shopId,
CancellationToken cancellationToken = default)
{
var query = new GetResourcesByShopQuery(shopId, null); // null = get all (active + inactive)
var result = await _mediator.Send(query, cancellationToken);
return Ok(ApiResponse<List<ResourceDto>>.Ok(result));
}
}

View File

@@ -0,0 +1,152 @@
// EN: Appointments Controller - Customer-facing appointment APIs.
// VI: Controller Appointments - APIs đặt lịch hẹn cho khách hàng.
using Asp.Versioning;
using BookingService.API.Application.Commands;
using BookingService.API.Application.DTOs;
using BookingService.API.Application.Queries;
using BookingService.API.Models.Requests;
using BookingService.API.Models.Responses;
using MediatR;
using Microsoft.AspNetCore.Mvc;
namespace BookingService.API.Controllers;
[ApiController]
[ApiVersion("1.0")]
[Route("api/v{version:apiVersion}/appointments")]
[Produces("application/json")]
public class AppointmentsController : ControllerBase
{
private readonly IMediator _mediator;
private readonly ILogger<AppointmentsController> _logger;
public AppointmentsController(IMediator mediator, ILogger<AppointmentsController> logger)
{
_mediator = mediator ?? throw new ArgumentNullException(nameof(mediator));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
/// <summary>
/// EN: Get appointments by shop or customer with filtering.
/// VI: Lấy danh sách cuộc hẹn theo shop hoặc customer với lọc.
/// </summary>
[HttpGet]
[ProducesResponseType(typeof(ApiResponse<PaginatedList<AppointmentDto>>), StatusCodes.Status200OK)]
public async Task<ActionResult<ApiResponse<PaginatedList<AppointmentDto>>>> GetAppointments(
[FromQuery] Guid? shopId,
[FromQuery] Guid? customerId,
[FromQuery] DateTime? startDate,
[FromQuery] DateTime? endDate,
[FromQuery] string? status,
[FromQuery] int page = 1,
[FromQuery] int pageSize = 20,
CancellationToken cancellationToken = default)
{
PaginatedList<AppointmentDto> result;
if (shopId.HasValue)
{
var query = new GetAppointmentsByShopQuery(shopId.Value, startDate, endDate, status, page, pageSize);
result = await _mediator.Send(query, cancellationToken);
}
else if (customerId.HasValue)
{
var query = new GetAppointmentsByCustomerQuery(customerId.Value, page, pageSize);
result = await _mediator.Send(query, cancellationToken);
}
else
{
return BadRequest(ApiResponse<PaginatedList<AppointmentDto>>.Fail("Either shopId or customerId must be provided"));
}
return Ok(ApiResponse<PaginatedList<AppointmentDto>>.Ok(result));
}
/// <summary>
/// EN: Get appointment by ID.
/// VI: Lấy chi tiết cuộc appointment theo ID.
/// </summary>
[HttpGet("{id:guid}")]
[ProducesResponseType(typeof(ApiResponse<AppointmentDto>), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult<ApiResponse<AppointmentDto>>> GetAppointment(
Guid id,
CancellationToken cancellationToken = default)
{
var query = new GetAppointmentQuery(id);
var result = await _mediator.Send(query, cancellationToken);
if (result == null)
{
return NotFound(ApiResponse<AppointmentDto>.Fail($"Appointment {id} not found"));
}
return Ok(ApiResponse<AppointmentDto>.Ok(result));
}
/// <summary>
/// EN: Create a new appointment.
/// VI: Tạo cuộc hẹn mới.
/// </summary>
[HttpPost]
[ProducesResponseType(typeof(ApiResponse<AppointmentDto>), StatusCodes.Status201Created)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public async Task<ActionResult<ApiResponse<AppointmentDto>>> CreateAppointment(
[FromBody] CreateAppointmentRequest request,
CancellationToken cancellationToken = default)
{
var command = new CreateAppointmentCommand(
request.ShopId,
request.ServiceId,
request.StartTime,
request.EndTime,
request.CustomerId,
request.StaffId,
request.ResourceId
);
var result = await _mediator.Send(command, cancellationToken);
return CreatedAtAction(
nameof(GetAppointment),
new { id = result.Id },
ApiResponse<AppointmentDto>.Ok(result, "Appointment created successfully"));
}
/// <summary>
/// EN: Update appointment status.
/// VI: Cập nhật trạng thái cuộc hẹn.
/// </summary>
[HttpPatch("{id:guid}/status")]
[ProducesResponseType(typeof(ApiResponse<AppointmentDto>), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult<ApiResponse<AppointmentDto>>> UpdateStatus(
Guid id,
[FromBody] UpdateStatusRequest request,
CancellationToken cancellationToken = default)
{
var command = new UpdateAppointmentStatusCommand(id, request.Action);
var result = await _mediator.Send(command, cancellationToken);
return Ok(ApiResponse<AppointmentDto>.Ok(result, "Status updated successfully"));
}
/// <summary>
/// EN: Cancel an appointment.
/// VI: Hủy cuộc hẹn.
/// </summary>
[HttpDelete("{id:guid}")]
[ProducesResponseType(typeof(ApiResponse<bool>), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult<ApiResponse<bool>>> CancelAppointment(
Guid id,
[FromBody] CancelRequest request,
CancellationToken cancellationToken = default)
{
var command = new CancelAppointmentCommand(id, request.Reason);
var result = await _mediator.Send(command, cancellationToken);
return Ok(ApiResponse<bool>.Ok(result, "Appointment cancelled successfully"));
}
}

View File

@@ -0,0 +1,95 @@
// EN: Resources Controller - Resource management APIs.
// VI: Controller Resources - APIs quản lý tài nguyên.
using Asp.Versioning;
using BookingService.API.Application.Commands;
using BookingService.API.Application.DTOs;
using BookingService.API.Application.Queries;
using BookingService.API.Models.Requests;
using BookingService.API.Models.Responses;
using MediatR;
using Microsoft.AspNetCore.Mvc;
namespace BookingService.API.Controllers;
[ApiController]
[ApiVersion("1.0")]
[Route("api/v{version:apiVersion}/resources")]
[Produces("application/json")]
public class ResourcesController : ControllerBase
{
private readonly IMediator _mediator;
private readonly ILogger<ResourcesController> _logger;
public ResourcesController(IMediator mediator, ILogger<ResourcesController> logger)
{
_mediator = mediator ?? throw new ArgumentNullException(nameof(mediator));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
/// <summary>
/// EN: Get resources by shop.
/// VI: Lấy danh sách tài nguyên theo shop.
/// </summary>
[HttpGet]
[ProducesResponseType(typeof(ApiResponse<List<ResourceDto>>), StatusCodes.Status200OK)]
public async Task<ActionResult<ApiResponse<List<ResourceDto>>>> GetResources(
[FromQuery] Guid shopId,
[FromQuery] bool? isActive = null,
CancellationToken cancellationToken = default)
{
var query = new GetResourcesByShopQuery(shopId, isActive);
var result = await _mediator.Send(query, cancellationToken);
return Ok(ApiResponse<List<ResourceDto>>.Ok(result));
}
/// <summary>
/// EN: Create a new resource.
/// VI: Tạo tài nguyên mới.
/// </summary>
[HttpPost]
[ProducesResponseType(typeof(ApiResponse<ResourceDto>), StatusCodes.Status201Created)]
public async Task<ActionResult<ApiResponse<ResourceDto>>> CreateResource(
[FromBody] CreateResourceRequest request,
CancellationToken cancellationToken = default)
{
var command = new CreateResourceCommand(
request.ShopId,
request.Name,
request.ResourceType,
request.Capacity
);
var result = await _mediator.Send(command, cancellationToken);
return CreatedAtAction(
nameof(GetResources),
new { shopId = result.ShopId },
ApiResponse<ResourceDto>.Ok(result, "Resource created successfully"));
}
/// <summary>
/// EN: Update a resource.
/// VI: Cập nhật tài nguyên.
/// </summary>
[HttpPut("{id:guid}")]
[ProducesResponseType(typeof(ApiResponse<ResourceDto>), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult<ApiResponse<ResourceDto>>> UpdateResource(
Guid id,
[FromBody] UpdateResourceRequest request,
CancellationToken cancellationToken = default)
{
var command = new UpdateResourceCommand(
id,
request.Name,
request.Capacity,
request.IsActive
);
var result = await _mediator.Send(command, cancellationToken);
return Ok(ApiResponse<ResourceDto>.Ok(result, "Resource updated successfully"));
}
}

View File

@@ -0,0 +1,54 @@
// EN: Slots Controller - Available slot finding APIs.
// VI: Controller Slots - APIs tìm slot thời gian khả dụng.
using Asp.Versioning;
using BookingService.API.Application.DTOs;
using BookingService.API.Application.Queries;
using BookingService.API.Models.Requests;
using BookingService.API.Models.Responses;
using MediatR;
using Microsoft.AspNetCore.Mvc;
namespace BookingService.API.Controllers;
[ApiController]
[ApiVersion("1.0")]
[Route("api/v{version:apiVersion}/slots")]
[Produces("application/json")]
public class SlotsController : ControllerBase
{
private readonly IMediator _mediator;
private readonly ILogger<SlotsController> _logger;
public SlotsController(IMediator mediator, ILogger<SlotsController> logger)
{
_mediator = mediator ?? throw new ArgumentNullException(nameof(mediator));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
/// <summary>
/// EN: Find available time slots based on staff schedule, resource availability, and existing appointments.
/// VI: Tìm các slot thời gian khả dụng dựa trên lịch nhân viên, tài nguyên và cuộc hẹn hiện tại.
/// </summary>
[HttpPost("find")]
[ProducesResponseType(typeof(ApiResponse<List<TimeSlotDto>>), StatusCodes.Status200OK)]
public async Task<ActionResult<ApiResponse<List<TimeSlotDto>>>> FindAvailableSlots(
[FromBody] FindSlotsRequest request,
CancellationToken cancellationToken = default)
{
var query = new FindAvailableSlotsQuery(
request.ShopId,
request.ServiceId,
request.Date,
request.ServiceDurationMinutes,
request.StaffId,
request.ResourceId
);
var result = await _mediator.Send(query, cancellationToken);
return Ok(ApiResponse<List<TimeSlotDto>>.Ok(
result,
$"Found {result.Count} available slots"));
}
}

View File

@@ -0,0 +1,68 @@
// EN: Staff Schedules Controller - Staff schedule management APIs.
// VI: Controller StaffSchedules - APIs quản lý lịch làm việc nhân viên.
using Asp.Versioning;
using BookingService.API.Application.Commands;
using BookingService.API.Application.DTOs;
using BookingService.API.Application.Queries;
using BookingService.API.Models.Requests;
using BookingService.API.Models.Responses;
using MediatR;
using Microsoft.AspNetCore.Mvc;
namespace BookingService.API.Controllers;
[ApiController]
[ApiVersion("1.0")]
[Route("api/v{version:apiVersion}/staff/{staffId:guid}/schedule")]
[Produces("application/json")]
public class StaffSchedulesController : ControllerBase
{
private readonly IMediator _mediator;
private readonly ILogger<StaffSchedulesController> _logger;
public StaffSchedulesController(IMediator mediator, ILogger<StaffSchedulesController> logger)
{
_mediator = mediator ?? throw new ArgumentNullException(nameof(mediator));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
/// <summary>
/// EN: Get staff schedule.
/// VI: Lấy lịch làm việc của nhân viên.
/// </summary>
[HttpGet]
[ProducesResponseType(typeof(ApiResponse<List<StaffScheduleDto>>), StatusCodes.Status200OK)]
public async Task<ActionResult<ApiResponse<List<StaffScheduleDto>>>> GetSchedule(
Guid staffId,
[FromQuery] Guid shopId,
CancellationToken cancellationToken = default)
{
var query = new GetStaffScheduleQuery(staffId, shopId);
var result = await _mediator.Send(query, cancellationToken);
return Ok(ApiResponse<List<StaffScheduleDto>>.Ok(result));
}
/// <summary>
/// EN: Update staff schedule.
/// VI: Cập nhật lịch làm việc nhân viên.
/// </summary>
[HttpPut]
[ProducesResponseType(typeof(ApiResponse<List<StaffScheduleDto>>), StatusCodes.Status200OK)]
public async Task<ActionResult<ApiResponse<List<StaffScheduleDto>>>> UpdateSchedule(
Guid staffId,
[FromBody] UpdateScheduleRequest request,
CancellationToken cancellationToken = default)
{
var command = new UpdateStaffScheduleCommand(
staffId,
request.ShopId,
request.Schedule
);
var result = await _mediator.Send(command, cancellationToken);
return Ok(ApiResponse<List<StaffScheduleDto>>.Ok(result, "Schedule updated successfully"));
}
}

View File

@@ -0,0 +1,32 @@
// EN: Generic API response wrapper.
// VI: Wrapper response API generic.
namespace BookingService.API.Models.Responses;
public record ApiResponse<T>
{
public bool Success { get; init; }
public T? Data { get; init; }
public string? Message { get; init; }
public List<string>? Errors { get; init; }
public static ApiResponse<T> Ok(T data, string? message = null)
{
return new ApiResponse<T>
{
Success = true,
Data = data,
Message = message
};
}
public static ApiResponse<T> Fail(string message, List<string>? errors = null)
{
return new ApiResponse<T>
{
Success = false,
Message = message,
Errors = errors
};
}
}

View File

@@ -6,8 +6,9 @@ using BookingService.Domain.SeedWork;
namespace BookingService.Infrastructure.Repositories;
public interface IStaffScheduleRepository : IRepository<StaffSchedule>
public interface IStaffScheduleRepository
{
IUnitOfWork UnitOfWork { get; }
StaffSchedule Add(StaffSchedule schedule);
void Update(StaffSchedule schedule);
void Remove(StaffSchedule schedule);

View File

@@ -20,11 +20,11 @@ COPY src/ ./src/
# EN: Build the application
# VI: Build ứng dụng
WORKDIR "/src/src/FnbEngine.API"
RUN dotnet build "FnbEngine.API.csproj" -c Release -o /app/build --no-restore
RUN dotnet build "FnbEngine.API.csproj" -c Release -o /app/build
# Publish stage / Giai đoạn publish
FROM build AS publish
RUN dotnet publish "FnbEngine.API.csproj" -c Release -o /app/publish /p:UseAppHost=false --no-restore
RUN dotnet publish "FnbEngine.API.csproj" -c Release -o /app/publish /p:UseAppHost=false
# Runtime stage / Giai đoạn runtime
FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS final

View File

@@ -0,0 +1,58 @@
// EN: Handler for CloseSessionCommand.
// VI: Handler cho CloseSessionCommand.
using MediatR;
using FnbEngine.Domain.AggregatesModel.SessionAggregate;
using FnbEngine.Domain.AggregatesModel.TableAggregate;
namespace FnbEngine.API.Application.Commands;
/// <summary>
/// EN: Handler for closing a session.
/// VI: Handler đóng phiên.
/// </summary>
public class CloseSessionCommandHandler : IRequestHandler<CloseSessionCommand, bool>
{
private readonly ISessionRepository _sessionRepository;
private readonly ITableRepository _tableRepository;
private readonly ILogger<CloseSessionCommandHandler> _logger;
public CloseSessionCommandHandler(
ISessionRepository sessionRepository,
ITableRepository tableRepository,
ILogger<CloseSessionCommandHandler> logger)
{
_sessionRepository = sessionRepository ?? throw new ArgumentNullException(nameof(sessionRepository));
_tableRepository = tableRepository ?? throw new ArgumentNullException(nameof(tableRepository));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
public async Task<bool> Handle(CloseSessionCommand request, CancellationToken cancellationToken)
{
var session = await _sessionRepository.GetByIdAsync(request.SessionId, cancellationToken);
if (session == null)
{
throw new InvalidOperationException($"Session {request.SessionId} not found");
}
// EN: Close session through domain model
// VI: Đóng phiên thông qua domain model
session.Close();
// EN: Mark table as available
// VI: Đánh dấu bàn còn trống
var table = await _tableRepository.GetByIdAsync(session.TableId, cancellationToken);
if (table != null)
{
table.MarkAsAvailable();
_tableRepository.Update(table);
}
_sessionRepository.Update(session);
await _sessionRepository.UnitOfWork.SaveEntitiesAsync(cancellationToken);
_logger.LogInformation("Closed session {SessionId}", request.SessionId);
return true;
}
}

View File

@@ -0,0 +1,14 @@
// EN: Command to create a kitchen ticket.
// VI: Command tạo phiếu bếp.
using MediatR;
namespace FnbEngine.API.Application.Commands;
public record CreateKitchenTicketCommand(
Guid SessionId,
Guid OrderItemId,
string ItemName,
string? Station = null,
int Priority = 0
) : IRequest<Guid>;

View File

@@ -0,0 +1,32 @@
// EN: Handler for CreateKitchenTicketCommand.
// VI: Handler cho CreateKitchenTicketCommand.
using MediatR;
using FnbEngine.Domain.AggregatesModel.KitchenAggregate;
namespace FnbEngine.API.Application.Commands;
public class CreateKitchenTicketCommandHandler : IRequestHandler<CreateKitchenTicketCommand, Guid>
{
private readonly IKitchenTicketRepository _repository;
public CreateKitchenTicketCommandHandler(IKitchenTicketRepository repository)
{
_repository = repository ?? throw new ArgumentNullException(nameof(repository));
}
public async Task<Guid> Handle(CreateKitchenTicketCommand request, CancellationToken cancellationToken)
{
var ticket = new KitchenTicket(
request.SessionId,
request.OrderItemId,
request.ItemName,
request.Station,
request.Priority);
await _repository.AddAsync(ticket, cancellationToken);
await _repository.UnitOfWork.SaveEntitiesAsync(cancellationToken);
return ticket.Id;
}
}

View File

@@ -0,0 +1,11 @@
// EN: Command to update kitchen ticket status.
// VI: Command cập nhật trạng thái phiếu bếp.
using MediatR;
namespace FnbEngine.API.Application.Commands;
public record UpdateTicketStatusCommand(
Guid TicketId,
string Status // "InProgress", "Ready", "Served"
) : IRequest<bool>;

View File

@@ -0,0 +1,46 @@
// EN: Handler for UpdateTicketStatusCommand.
// VI: Handler cho UpdateTicketStatusCommand.
using MediatR;
using FnbEngine.Domain.AggregatesModel.KitchenAggregate;
namespace FnbEngine.API.Application.Commands;
public class UpdateTicketStatusCommandHandler : IRequestHandler<UpdateTicketStatusCommand, bool>
{
private readonly IKitchenTicketRepository _repository;
public UpdateTicketStatusCommandHandler(IKitchenTicketRepository repository)
{
_repository = repository ?? throw new ArgumentNullException(nameof(repository));
}
public async Task<bool> Handle(UpdateTicketStatusCommand request, CancellationToken cancellationToken)
{
var ticket = await _repository.GetByIdAsync(request.TicketId, cancellationToken);
if (ticket == null)
{
throw new InvalidOperationException($"Ticket {request.TicketId} not found");
}
switch (request.Status.ToLowerInvariant())
{
case "inprogress":
ticket.MarkAsInProgress();
break;
case "ready":
ticket.MarkAsReady();
break;
case "served":
ticket.MarkAsServed();
break;
default:
throw new ArgumentException($"Invalid status: {request.Status}");
}
_repository.Update(ticket);
await _repository.UnitOfWork.SaveEntitiesAsync(cancellationToken);
return true;
}
}

View File

@@ -0,0 +1,19 @@
// EN: Query to get pending kitchen tickets.
// VI: Query lấy danh sách phiếu bếp chờ.
using MediatR;
namespace FnbEngine.API.Application.Queries;
public record GetPendingTicketsQuery(string? Station = null) : IRequest<IEnumerable<KitchenTicketDto>>;
public record KitchenTicketDto(
Guid Id,
Guid SessionId,
Guid OrderItemId,
string ItemName,
string? Station,
int Priority,
string Status,
DateTime CreatedAt
);

View File

@@ -0,0 +1,33 @@
// EN: Handler for GetPendingTicketsQuery.
// VI: Handler cho GetPendingTicketsQuery.
using MediatR;
using FnbEngine.Domain.AggregatesModel.KitchenAggregate;
namespace FnbEngine.API.Application.Queries;
public class GetPendingTicketsQueryHandler : IRequestHandler<GetPendingTicketsQuery, IEnumerable<KitchenTicketDto>>
{
private readonly IKitchenTicketRepository _repository;
public GetPendingTicketsQueryHandler(IKitchenTicketRepository repository)
{
_repository = repository ?? throw new ArgumentNullException(nameof(repository));
}
public async Task<IEnumerable<KitchenTicketDto>> Handle(GetPendingTicketsQuery request, CancellationToken cancellationToken)
{
var tickets = await _repository.GetPendingByStationAsync(request.Station, cancellationToken);
return tickets.Select(t => new KitchenTicketDto(
t.Id,
t.SessionId,
t.OrderItemId,
t.ItemName,
t.Station,
t.Priority,
t.Status,
t.CreatedAt
));
}
}

View File

@@ -0,0 +1,26 @@
// EN: Query to get session details.
// VI: Query lấy thông tin phiên.
using MediatR;
namespace FnbEngine.API.Application.Queries;
/// <summary>
/// EN: Query to get session by ID.
/// VI: Query lấy phiên theo ID.
/// </summary>
public record GetSessionQuery(Guid SessionId) : IRequest<SessionDto?>;
/// <summary>
/// EN: Session data transfer object.
/// VI: Data transfer object cho Session.
/// </summary>
public record SessionDto(
Guid Id,
Guid TableId,
Guid ShopId,
int GuestCount,
DateTime StartedAt,
DateTime? ClosedAt,
string Status
);

View File

@@ -0,0 +1,39 @@
// EN: Handler for GetSessionQuery.
// VI: Handler cho GetSessionQuery.
using MediatR;
using FnbEngine.Domain.AggregatesModel.SessionAggregate;
namespace FnbEngine.API.Application.Queries;
/// <summary>
/// EN: Handler for getting session details.
/// VI: Handler lấy thông tin phiên.
/// </summary>
public class GetSessionQueryHandler : IRequestHandler<GetSessionQuery, SessionDto?>
{
private readonly ISessionRepository _sessionRepository;
public GetSessionQueryHandler(ISessionRepository sessionRepository)
{
_sessionRepository = sessionRepository ?? throw new ArgumentNullException(nameof(sessionRepository));
}
public async Task<SessionDto?> Handle(GetSessionQuery request, CancellationToken cancellationToken)
{
var session = await _sessionRepository.GetByIdAsync(request.SessionId, cancellationToken);
if (session == null)
return null;
return new SessionDto(
session.Id,
session.TableId,
session.ShopId,
session.GuestCount,
session.StartedAt,
session.ClosedAt,
session.Status
);
}
}

View File

@@ -0,0 +1,55 @@
// EN: Controller for kitchen display system.
// VI: Controller cho hệ thống hiển thị bếp.
using Asp.Versioning;
using MediatR;
using Microsoft.AspNetCore.Mvc;
using FnbEngine.API.Application.Commands;
using FnbEngine.API.Application.Queries;
namespace FnbEngine.API.Controllers;
[ApiController]
[ApiVersion("1.0")]
[Route("api/v{version:apiVersion}/kitchen")]
public class KitchenController : ControllerBase
{
private readonly IMediator _mediator;
public KitchenController(IMediator mediator)
{
_mediator = mediator ?? throw new ArgumentNullException(nameof(mediator));
}
/// <summary>
/// EN: Get pending kitchen tickets.
/// VI: Lấy danh sách phiếu bếp chờ.
/// </summary>
[HttpGet("tickets")]
[ProducesResponseType(typeof(ApiResponse<IEnumerable<KitchenTicketDto>>), 200)]
public async Task<ActionResult<ApiResponse<IEnumerable<KitchenTicketDto>>>> GetPendingTickets(
[FromQuery] string? station = null,
CancellationToken ct = default)
{
var result = await _mediator.Send(new GetPendingTicketsQuery(station), ct);
return Ok(new ApiResponse<IEnumerable<KitchenTicketDto>> { Success = true, Data = result });
}
/// <summary>
/// EN: Update ticket status.
/// VI: Cập nhật trạng thái phiếu.
/// </summary>
[HttpPatch("tickets/{id}/status")]
[ProducesResponseType(typeof(ApiResponse<bool>), 200)]
[ProducesResponseType(404)]
public async Task<ActionResult<ApiResponse<bool>>> UpdateStatus(
Guid id,
[FromBody] UpdateStatusRequest request,
CancellationToken ct = default)
{
var result = await _mediator.Send(new UpdateTicketStatusCommand(id, request.Status), ct);
return Ok(new ApiResponse<bool> { Success = true, Data = result });
}
}
public record UpdateStatusRequest(string Status);

View File

@@ -0,0 +1,90 @@
// EN: Controller for session management.
// VI: Controller quản lý phiên.
using Asp.Versioning;
using MediatR;
using Microsoft.AspNetCore.Mvc;
using FnbEngine.API.Application.Commands;
using FnbEngine.API.Application.Queries;
namespace FnbEngine.API.Controllers;
/// <summary>
/// EN: Controller for session management.
/// VI: Controller quản lý phiên.
/// </summary>
[ApiController]
[ApiVersion("1.0")]
[Route("api/v{version:apiVersion}/sessions")]
public class SessionsController : ControllerBase
{
private readonly IMediator _mediator;
private readonly ILogger<SessionsController> _logger;
public SessionsController(IMediator mediator, ILogger<SessionsController> logger)
{
_mediator = mediator ?? throw new ArgumentNullException(nameof(mediator));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
/// <summary>
/// EN: Open a new session.
/// VI: Mở phiên mới.
/// </summary>
[HttpPost]
[ProducesResponseType(typeof(ApiResponse<OpenSessionResult>), 201)]
[ProducesResponseType(400)]
public async Task<ActionResult<ApiResponse<OpenSessionResult>>> OpenSession(
[FromBody] OpenSessionRequest request,
CancellationToken ct = default)
{
var command = new OpenSessionCommand(request.TableId, request.ShopId, request.GuestCount);
var result = await _mediator.Send(command, ct);
return CreatedAtAction(nameof(GetSession), new { id = result.SessionId },
new ApiResponse<OpenSessionResult> { Success = true, Data = result });
}
/// <summary>
/// EN: Get session details.
/// VI: Lấy thông tin phiên.
/// </summary>
[HttpGet("{id}")]
[ProducesResponseType(typeof(ApiResponse<SessionDto>), 200)]
[ProducesResponseType(404)]
public async Task<ActionResult<ApiResponse<SessionDto>>> GetSession(
Guid id,
CancellationToken ct = default)
{
var result = await _mediator.Send(new GetSessionQuery(id), ct);
if (result == null)
return NotFound(new ApiResponse<SessionDto> { Success = false, Error = "Session not found" });
return Ok(new ApiResponse<SessionDto> { Success = true, Data = result });
}
/// <summary>
/// EN: Close a session.
/// VI: Đóng phiên.
/// </summary>
[HttpPost("{id}/close")]
[ProducesResponseType(typeof(ApiResponse<bool>), 200)]
[ProducesResponseType(404)]
public async Task<ActionResult<ApiResponse<bool>>> CloseSession(
Guid id,
CancellationToken ct = default)
{
var result = await _mediator.Send(new CloseSessionCommand(id), ct);
return Ok(new ApiResponse<bool> { Success = true, Data = result });
}
}
/// <summary>
/// EN: Request to open a session.
/// VI: Request mở phiên.
/// </summary>
public record OpenSessionRequest(
Guid TableId,
Guid ShopId,
int GuestCount = 1);

View File

@@ -20,11 +20,11 @@ COPY src/ ./src/
# EN: Build the application
# VI: Build ứng dụng
WORKDIR "/src/src/OrderService.API"
RUN dotnet build "OrderService.API.csproj" -c Release -o /app/build --no-restore
RUN dotnet build "OrderService.API.csproj" -c Release -o /app/build
# Publish stage / Giai đoạn publish
FROM build AS publish
RUN dotnet publish "OrderService.API.csproj" -c Release -o /app/publish /p:UseAppHost=false --no-restore
RUN dotnet publish "OrderService.API.csproj" -c Release -o /app/publish /p:UseAppHost=false
# Runtime stage / Giai đoạn runtime
FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS final