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:
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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"));
|
||||
}
|
||||
}
|
||||
@@ -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"));
|
||||
}
|
||||
}
|
||||
@@ -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"));
|
||||
}
|
||||
}
|
||||
@@ -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"));
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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>;
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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>;
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
);
|
||||
@@ -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
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
);
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user