diff --git a/services/booking-service-net/src/BookingService.API/Controllers/Admin/AdminAppointmentsController.cs b/services/booking-service-net/src/BookingService.API/Controllers/Admin/AdminAppointmentsController.cs new file mode 100644 index 00000000..136c79d2 --- /dev/null +++ b/services/booking-service-net/src/BookingService.API/Controllers/Admin/AdminAppointmentsController.cs @@ -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 _logger; + + public AdminAppointmentsController(IMediator mediator, ILogger logger) + { + _mediator = mediator ?? throw new ArgumentNullException(nameof(mediator)); + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + } + + /// + /// 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. + /// + [HttpGet] + [ProducesResponseType(typeof(ApiResponse>), StatusCodes.Status200OK)] + public async Task>>> 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>.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>.Ok(result)); + } + + /// + /// EN: Get appointment statistics for a date range. + /// VI: Lấy thống kê cuộc hẹn theo khoảng thời gian. + /// + [HttpGet("statistics")] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] + public async Task>> 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.Ok(statistics)); + } +} diff --git a/services/booking-service-net/src/BookingService.API/Controllers/Admin/AdminResourcesController.cs b/services/booking-service-net/src/BookingService.API/Controllers/Admin/AdminResourcesController.cs new file mode 100644 index 00000000..4c947570 --- /dev/null +++ b/services/booking-service-net/src/BookingService.API/Controllers/Admin/AdminResourcesController.cs @@ -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 _logger; + + public AdminResourcesController(IMediator mediator, ILogger logger) + { + _mediator = mediator ?? throw new ArgumentNullException(nameof(mediator)); + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + } + + /// + /// 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). + /// + [HttpGet("{shopId:guid}")] + [ProducesResponseType(typeof(ApiResponse>), StatusCodes.Status200OK)] + public async Task>>> 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>.Ok(result)); + } +} diff --git a/services/booking-service-net/src/BookingService.API/Controllers/AppointmentsController.cs b/services/booking-service-net/src/BookingService.API/Controllers/AppointmentsController.cs new file mode 100644 index 00000000..4d4f057e --- /dev/null +++ b/services/booking-service-net/src/BookingService.API/Controllers/AppointmentsController.cs @@ -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 _logger; + + public AppointmentsController(IMediator mediator, ILogger logger) + { + _mediator = mediator ?? throw new ArgumentNullException(nameof(mediator)); + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + } + + /// + /// 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. + /// + [HttpGet] + [ProducesResponseType(typeof(ApiResponse>), StatusCodes.Status200OK)] + public async Task>>> 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 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>.Fail("Either shopId or customerId must be provided")); + } + + return Ok(ApiResponse>.Ok(result)); + } + + /// + /// EN: Get appointment by ID. + /// VI: Lấy chi tiết cuộc appointment theo ID. + /// + [HttpGet("{id:guid}")] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task>> GetAppointment( + Guid id, + CancellationToken cancellationToken = default) + { + var query = new GetAppointmentQuery(id); + var result = await _mediator.Send(query, cancellationToken); + + if (result == null) + { + return NotFound(ApiResponse.Fail($"Appointment {id} not found")); + } + + return Ok(ApiResponse.Ok(result)); + } + + /// + /// EN: Create a new appointment. + /// VI: Tạo cuộc hẹn mới. + /// + [HttpPost] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status201Created)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + public async Task>> 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.Ok(result, "Appointment created successfully")); + } + + /// + /// EN: Update appointment status. + /// VI: Cập nhật trạng thái cuộc hẹn. + /// + [HttpPatch("{id:guid}/status")] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task>> 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.Ok(result, "Status updated successfully")); + } + + /// + /// EN: Cancel an appointment. + /// VI: Hủy cuộc hẹn. + /// + [HttpDelete("{id:guid}")] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task>> 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.Ok(result, "Appointment cancelled successfully")); + } +} diff --git a/services/booking-service-net/src/BookingService.API/Controllers/ResourcesController.cs b/services/booking-service-net/src/BookingService.API/Controllers/ResourcesController.cs new file mode 100644 index 00000000..d17042d9 --- /dev/null +++ b/services/booking-service-net/src/BookingService.API/Controllers/ResourcesController.cs @@ -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 _logger; + + public ResourcesController(IMediator mediator, ILogger logger) + { + _mediator = mediator ?? throw new ArgumentNullException(nameof(mediator)); + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + } + + /// + /// EN: Get resources by shop. + /// VI: Lấy danh sách tài nguyên theo shop. + /// + [HttpGet] + [ProducesResponseType(typeof(ApiResponse>), StatusCodes.Status200OK)] + public async Task>>> 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>.Ok(result)); + } + + /// + /// EN: Create a new resource. + /// VI: Tạo tài nguyên mới. + /// + [HttpPost] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status201Created)] + public async Task>> 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.Ok(result, "Resource created successfully")); + } + + /// + /// EN: Update a resource. + /// VI: Cập nhật tài nguyên. + /// + [HttpPut("{id:guid}")] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task>> 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.Ok(result, "Resource updated successfully")); + } +} diff --git a/services/booking-service-net/src/BookingService.API/Controllers/SlotsController.cs b/services/booking-service-net/src/BookingService.API/Controllers/SlotsController.cs new file mode 100644 index 00000000..17921ed2 --- /dev/null +++ b/services/booking-service-net/src/BookingService.API/Controllers/SlotsController.cs @@ -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 _logger; + + public SlotsController(IMediator mediator, ILogger logger) + { + _mediator = mediator ?? throw new ArgumentNullException(nameof(mediator)); + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + } + + /// + /// 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. + /// + [HttpPost("find")] + [ProducesResponseType(typeof(ApiResponse>), StatusCodes.Status200OK)] + public async Task>>> 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>.Ok( + result, + $"Found {result.Count} available slots")); + } +} diff --git a/services/booking-service-net/src/BookingService.API/Controllers/StaffSchedulesController.cs b/services/booking-service-net/src/BookingService.API/Controllers/StaffSchedulesController.cs new file mode 100644 index 00000000..3cd9bf16 --- /dev/null +++ b/services/booking-service-net/src/BookingService.API/Controllers/StaffSchedulesController.cs @@ -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 _logger; + + public StaffSchedulesController(IMediator mediator, ILogger logger) + { + _mediator = mediator ?? throw new ArgumentNullException(nameof(mediator)); + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + } + + /// + /// EN: Get staff schedule. + /// VI: Lấy lịch làm việc của nhân viên. + /// + [HttpGet] + [ProducesResponseType(typeof(ApiResponse>), StatusCodes.Status200OK)] + public async Task>>> 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>.Ok(result)); + } + + /// + /// EN: Update staff schedule. + /// VI: Cập nhật lịch làm việc nhân viên. + /// + [HttpPut] + [ProducesResponseType(typeof(ApiResponse>), StatusCodes.Status200OK)] + public async Task>>> 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>.Ok(result, "Schedule updated successfully")); + } +} diff --git a/services/booking-service-net/src/BookingService.API/Models/Responses/ApiResponse.cs b/services/booking-service-net/src/BookingService.API/Models/Responses/ApiResponse.cs new file mode 100644 index 00000000..c44cc792 --- /dev/null +++ b/services/booking-service-net/src/BookingService.API/Models/Responses/ApiResponse.cs @@ -0,0 +1,32 @@ +// EN: Generic API response wrapper. +// VI: Wrapper response API generic. + +namespace BookingService.API.Models.Responses; + +public record ApiResponse +{ + public bool Success { get; init; } + public T? Data { get; init; } + public string? Message { get; init; } + public List? Errors { get; init; } + + public static ApiResponse Ok(T data, string? message = null) + { + return new ApiResponse + { + Success = true, + Data = data, + Message = message + }; + } + + public static ApiResponse Fail(string message, List? errors = null) + { + return new ApiResponse + { + Success = false, + Message = message, + Errors = errors + }; + } +} diff --git a/services/booking-service-net/src/BookingService.Infrastructure/Repositories/IStaffScheduleRepository.cs b/services/booking-service-net/src/BookingService.Infrastructure/Repositories/IStaffScheduleRepository.cs index fcec3bda..273e7876 100644 --- a/services/booking-service-net/src/BookingService.Infrastructure/Repositories/IStaffScheduleRepository.cs +++ b/services/booking-service-net/src/BookingService.Infrastructure/Repositories/IStaffScheduleRepository.cs @@ -6,8 +6,9 @@ using BookingService.Domain.SeedWork; namespace BookingService.Infrastructure.Repositories; -public interface IStaffScheduleRepository : IRepository +public interface IStaffScheduleRepository { + IUnitOfWork UnitOfWork { get; } StaffSchedule Add(StaffSchedule schedule); void Update(StaffSchedule schedule); void Remove(StaffSchedule schedule); diff --git a/services/fnb-engine-net/Dockerfile b/services/fnb-engine-net/Dockerfile index e81296bc..b3bfa54b 100644 --- a/services/fnb-engine-net/Dockerfile +++ b/services/fnb-engine-net/Dockerfile @@ -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 diff --git a/services/fnb-engine-net/src/FnbEngine.API/Application/Commands/CloseSessionCommandHandler.cs b/services/fnb-engine-net/src/FnbEngine.API/Application/Commands/CloseSessionCommandHandler.cs new file mode 100644 index 00000000..632bb55f --- /dev/null +++ b/services/fnb-engine-net/src/FnbEngine.API/Application/Commands/CloseSessionCommandHandler.cs @@ -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; + +/// +/// EN: Handler for closing a session. +/// VI: Handler đóng phiên. +/// +public class CloseSessionCommandHandler : IRequestHandler +{ + private readonly ISessionRepository _sessionRepository; + private readonly ITableRepository _tableRepository; + private readonly ILogger _logger; + + public CloseSessionCommandHandler( + ISessionRepository sessionRepository, + ITableRepository tableRepository, + ILogger 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 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; + } +} diff --git a/services/fnb-engine-net/src/FnbEngine.API/Application/Commands/CreateKitchenTicketCommand.cs b/services/fnb-engine-net/src/FnbEngine.API/Application/Commands/CreateKitchenTicketCommand.cs new file mode 100644 index 00000000..dcca5fb6 --- /dev/null +++ b/services/fnb-engine-net/src/FnbEngine.API/Application/Commands/CreateKitchenTicketCommand.cs @@ -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; diff --git a/services/fnb-engine-net/src/FnbEngine.API/Application/Commands/CreateKitchenTicketCommandHandler.cs b/services/fnb-engine-net/src/FnbEngine.API/Application/Commands/CreateKitchenTicketCommandHandler.cs new file mode 100644 index 00000000..f621ad10 --- /dev/null +++ b/services/fnb-engine-net/src/FnbEngine.API/Application/Commands/CreateKitchenTicketCommandHandler.cs @@ -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 +{ + private readonly IKitchenTicketRepository _repository; + + public CreateKitchenTicketCommandHandler(IKitchenTicketRepository repository) + { + _repository = repository ?? throw new ArgumentNullException(nameof(repository)); + } + + public async Task 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; + } +} diff --git a/services/fnb-engine-net/src/FnbEngine.API/Application/Commands/UpdateTicketStatusCommand.cs b/services/fnb-engine-net/src/FnbEngine.API/Application/Commands/UpdateTicketStatusCommand.cs new file mode 100644 index 00000000..1e2a27a9 --- /dev/null +++ b/services/fnb-engine-net/src/FnbEngine.API/Application/Commands/UpdateTicketStatusCommand.cs @@ -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; diff --git a/services/fnb-engine-net/src/FnbEngine.API/Application/Commands/UpdateTicketStatusCommandHandler.cs b/services/fnb-engine-net/src/FnbEngine.API/Application/Commands/UpdateTicketStatusCommandHandler.cs new file mode 100644 index 00000000..909370ba --- /dev/null +++ b/services/fnb-engine-net/src/FnbEngine.API/Application/Commands/UpdateTicketStatusCommandHandler.cs @@ -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 +{ + private readonly IKitchenTicketRepository _repository; + + public UpdateTicketStatusCommandHandler(IKitchenTicketRepository repository) + { + _repository = repository ?? throw new ArgumentNullException(nameof(repository)); + } + + public async Task 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; + } +} diff --git a/services/fnb-engine-net/src/FnbEngine.API/Application/Queries/GetPendingTicketsQuery.cs b/services/fnb-engine-net/src/FnbEngine.API/Application/Queries/GetPendingTicketsQuery.cs new file mode 100644 index 00000000..45f6bb4f --- /dev/null +++ b/services/fnb-engine-net/src/FnbEngine.API/Application/Queries/GetPendingTicketsQuery.cs @@ -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>; + +public record KitchenTicketDto( + Guid Id, + Guid SessionId, + Guid OrderItemId, + string ItemName, + string? Station, + int Priority, + string Status, + DateTime CreatedAt +); diff --git a/services/fnb-engine-net/src/FnbEngine.API/Application/Queries/GetPendingTicketsQueryHandler.cs b/services/fnb-engine-net/src/FnbEngine.API/Application/Queries/GetPendingTicketsQueryHandler.cs new file mode 100644 index 00000000..447337cb --- /dev/null +++ b/services/fnb-engine-net/src/FnbEngine.API/Application/Queries/GetPendingTicketsQueryHandler.cs @@ -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> +{ + private readonly IKitchenTicketRepository _repository; + + public GetPendingTicketsQueryHandler(IKitchenTicketRepository repository) + { + _repository = repository ?? throw new ArgumentNullException(nameof(repository)); + } + + public async Task> 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 + )); + } +} diff --git a/services/fnb-engine-net/src/FnbEngine.API/Application/Queries/GetSessionQuery.cs b/services/fnb-engine-net/src/FnbEngine.API/Application/Queries/GetSessionQuery.cs new file mode 100644 index 00000000..7bdd1c87 --- /dev/null +++ b/services/fnb-engine-net/src/FnbEngine.API/Application/Queries/GetSessionQuery.cs @@ -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; + +/// +/// EN: Query to get session by ID. +/// VI: Query lấy phiên theo ID. +/// +public record GetSessionQuery(Guid SessionId) : IRequest; + +/// +/// EN: Session data transfer object. +/// VI: Data transfer object cho Session. +/// +public record SessionDto( + Guid Id, + Guid TableId, + Guid ShopId, + int GuestCount, + DateTime StartedAt, + DateTime? ClosedAt, + string Status +); diff --git a/services/fnb-engine-net/src/FnbEngine.API/Application/Queries/GetSessionQueryHandler.cs b/services/fnb-engine-net/src/FnbEngine.API/Application/Queries/GetSessionQueryHandler.cs new file mode 100644 index 00000000..2f7ae8cd --- /dev/null +++ b/services/fnb-engine-net/src/FnbEngine.API/Application/Queries/GetSessionQueryHandler.cs @@ -0,0 +1,39 @@ +// EN: Handler for GetSessionQuery. +// VI: Handler cho GetSessionQuery. + +using MediatR; +using FnbEngine.Domain.AggregatesModel.SessionAggregate; + +namespace FnbEngine.API.Application.Queries; + +/// +/// EN: Handler for getting session details. +/// VI: Handler lấy thông tin phiên. +/// +public class GetSessionQueryHandler : IRequestHandler +{ + private readonly ISessionRepository _sessionRepository; + + public GetSessionQueryHandler(ISessionRepository sessionRepository) + { + _sessionRepository = sessionRepository ?? throw new ArgumentNullException(nameof(sessionRepository)); + } + + public async Task 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 + ); + } +} diff --git a/services/fnb-engine-net/src/FnbEngine.API/Controllers/KitchenController.cs b/services/fnb-engine-net/src/FnbEngine.API/Controllers/KitchenController.cs new file mode 100644 index 00000000..d35f9129 --- /dev/null +++ b/services/fnb-engine-net/src/FnbEngine.API/Controllers/KitchenController.cs @@ -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)); + } + + /// + /// EN: Get pending kitchen tickets. + /// VI: Lấy danh sách phiếu bếp chờ. + /// + [HttpGet("tickets")] + [ProducesResponseType(typeof(ApiResponse>), 200)] + public async Task>>> GetPendingTickets( + [FromQuery] string? station = null, + CancellationToken ct = default) + { + var result = await _mediator.Send(new GetPendingTicketsQuery(station), ct); + return Ok(new ApiResponse> { Success = true, Data = result }); + } + + /// + /// EN: Update ticket status. + /// VI: Cập nhật trạng thái phiếu. + /// + [HttpPatch("tickets/{id}/status")] + [ProducesResponseType(typeof(ApiResponse), 200)] + [ProducesResponseType(404)] + public async Task>> UpdateStatus( + Guid id, + [FromBody] UpdateStatusRequest request, + CancellationToken ct = default) + { + var result = await _mediator.Send(new UpdateTicketStatusCommand(id, request.Status), ct); + return Ok(new ApiResponse { Success = true, Data = result }); + } +} + +public record UpdateStatusRequest(string Status); diff --git a/services/fnb-engine-net/src/FnbEngine.API/Controllers/SessionsController.cs b/services/fnb-engine-net/src/FnbEngine.API/Controllers/SessionsController.cs new file mode 100644 index 00000000..4b4a6f50 --- /dev/null +++ b/services/fnb-engine-net/src/FnbEngine.API/Controllers/SessionsController.cs @@ -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; + +/// +/// EN: Controller for session management. +/// VI: Controller quản lý phiên. +/// +[ApiController] +[ApiVersion("1.0")] +[Route("api/v{version:apiVersion}/sessions")] +public class SessionsController : ControllerBase +{ + private readonly IMediator _mediator; + private readonly ILogger _logger; + + public SessionsController(IMediator mediator, ILogger logger) + { + _mediator = mediator ?? throw new ArgumentNullException(nameof(mediator)); + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + } + + /// + /// EN: Open a new session. + /// VI: Mở phiên mới. + /// + [HttpPost] + [ProducesResponseType(typeof(ApiResponse), 201)] + [ProducesResponseType(400)] + public async Task>> 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 { Success = true, Data = result }); + } + + /// + /// EN: Get session details. + /// VI: Lấy thông tin phiên. + /// + [HttpGet("{id}")] + [ProducesResponseType(typeof(ApiResponse), 200)] + [ProducesResponseType(404)] + public async Task>> GetSession( + Guid id, + CancellationToken ct = default) + { + var result = await _mediator.Send(new GetSessionQuery(id), ct); + + if (result == null) + return NotFound(new ApiResponse { Success = false, Error = "Session not found" }); + + return Ok(new ApiResponse { Success = true, Data = result }); + } + + /// + /// EN: Close a session. + /// VI: Đóng phiên. + /// + [HttpPost("{id}/close")] + [ProducesResponseType(typeof(ApiResponse), 200)] + [ProducesResponseType(404)] + public async Task>> CloseSession( + Guid id, + CancellationToken ct = default) + { + var result = await _mediator.Send(new CloseSessionCommand(id), ct); + return Ok(new ApiResponse { Success = true, Data = result }); + } +} + +/// +/// EN: Request to open a session. +/// VI: Request mở phiên. +/// +public record OpenSessionRequest( + Guid TableId, + Guid ShopId, + int GuestCount = 1); diff --git a/services/order-service-net/Dockerfile b/services/order-service-net/Dockerfile index 43703e01..0737d3b3 100644 --- a/services/order-service-net/Dockerfile +++ b/services/order-service-net/Dockerfile @@ -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