1256 lines
25 KiB
C#
1256 lines
25 KiB
C#
|
|
using Meezi.API.Models.Tables;
|
||
|
|
|
||
|
|
using Meezi.Core.Entities;
|
||
|
|
|
||
|
|
using Meezi.Core.Enums;
|
||
|
|
|
||
|
|
using Meezi.Infrastructure.Data;
|
||
|
|
|
||
|
|
using Microsoft.EntityFrameworkCore;
|
||
|
|
|
||
|
|
using QRCoder;
|
||
|
|
|
||
|
|
|
||
|
|
|
||
|
|
namespace Meezi.API.Services;
|
||
|
|
|
||
|
|
|
||
|
|
|
||
|
|
public interface ITableService
|
||
|
|
|
||
|
|
{
|
||
|
|
|
||
|
|
Task<IReadOnlyList<TableBoardDto>> GetTableBoardAsync(
|
||
|
|
|
||
|
|
string cafeId,
|
||
|
|
|
||
|
|
bool activeOnly = true,
|
||
|
|
|
||
|
|
string? branchId = null,
|
||
|
|
|
||
|
|
CancellationToken cancellationToken = default);
|
||
|
|
|
||
|
|
|
||
|
|
|
||
|
|
Task<IReadOnlyList<TableDto>> GetTablesAsync(
|
||
|
|
|
||
|
|
string cafeId,
|
||
|
|
|
||
|
|
string? branchId = null,
|
||
|
|
|
||
|
|
CancellationToken cancellationToken = default);
|
||
|
|
|
||
|
|
|
||
|
|
|
||
|
|
Task<TableDto?> CreateTableAsync(string cafeId, CreateTableRequest request, CancellationToken cancellationToken = default);
|
||
|
|
|
||
|
|
Task<TableDto?> PatchTableAsync(string cafeId, string tableId, PatchTableRequest request, CancellationToken cancellationToken = default);
|
||
|
|
|
||
|
|
Task<TableBoardDto?> SetTableCleaningAsync(
|
||
|
|
|
||
|
|
string cafeId,
|
||
|
|
|
||
|
|
string tableId,
|
||
|
|
|
||
|
|
bool isCleaning,
|
||
|
|
|
||
|
|
CancellationToken cancellationToken = default);
|
||
|
|
|
||
|
|
Task<BranchTableOperationResult<object>> DeleteTableAsync(
|
||
|
|
string cafeId,
|
||
|
|
string tableId,
|
||
|
|
CancellationToken cancellationToken = default);
|
||
|
|
|
||
|
|
Task<IReadOnlyList<TableBoardDto>> GetBranchTableBoardAsync(
|
||
|
|
|
||
|
|
string cafeId,
|
||
|
|
|
||
|
|
string branchId,
|
||
|
|
|
||
|
|
bool activeOnly = true,
|
||
|
|
|
||
|
|
CancellationToken cancellationToken = default);
|
||
|
|
|
||
|
|
|
||
|
|
|
||
|
|
Task<IReadOnlyList<TableDto>?> GetBranchTablesAsync(
|
||
|
|
string cafeId,
|
||
|
|
string branchId,
|
||
|
|
CancellationToken cancellationToken = default);
|
||
|
|
|
||
|
|
|
||
|
|
|
||
|
|
Task<BranchTableOperationResult<TableDto>> CreateBranchTableAsync(
|
||
|
|
|
||
|
|
string cafeId,
|
||
|
|
|
||
|
|
string branchId,
|
||
|
|
|
||
|
|
CreateBranchTableRequest request,
|
||
|
|
|
||
|
|
CancellationToken cancellationToken = default);
|
||
|
|
|
||
|
|
|
||
|
|
|
||
|
|
Task<BranchTableOperationResult<TableDto>> PatchBranchTableAsync(
|
||
|
|
|
||
|
|
string cafeId,
|
||
|
|
|
||
|
|
string branchId,
|
||
|
|
|
||
|
|
string tableId,
|
||
|
|
|
||
|
|
PatchBranchTableRequest request,
|
||
|
|
|
||
|
|
CancellationToken cancellationToken = default);
|
||
|
|
|
||
|
|
|
||
|
|
|
||
|
|
Task<BranchTableOperationResult<object>> DeleteBranchTableAsync(
|
||
|
|
|
||
|
|
string cafeId,
|
||
|
|
|
||
|
|
string branchId,
|
||
|
|
|
||
|
|
string tableId,
|
||
|
|
|
||
|
|
CancellationToken cancellationToken = default);
|
||
|
|
|
||
|
|
|
||
|
|
|
||
|
|
Task<IReadOnlyList<TableSectionDto>?> GetBranchSectionsAsync(
|
||
|
|
|
||
|
|
string cafeId,
|
||
|
|
|
||
|
|
string branchId,
|
||
|
|
|
||
|
|
CancellationToken cancellationToken = default);
|
||
|
|
|
||
|
|
|
||
|
|
|
||
|
|
Task<BranchTableOperationResult<TableSectionDto>> CreateBranchSectionAsync(
|
||
|
|
|
||
|
|
string cafeId,
|
||
|
|
|
||
|
|
string branchId,
|
||
|
|
|
||
|
|
CreateTableSectionRequest request,
|
||
|
|
|
||
|
|
CancellationToken cancellationToken = default);
|
||
|
|
|
||
|
|
|
||
|
|
|
||
|
|
Task<BranchTableOperationResult<TableSectionDto>> PatchBranchSectionAsync(
|
||
|
|
|
||
|
|
string cafeId,
|
||
|
|
|
||
|
|
string branchId,
|
||
|
|
|
||
|
|
string sectionId,
|
||
|
|
|
||
|
|
PatchTableSectionRequest request,
|
||
|
|
|
||
|
|
CancellationToken cancellationToken = default);
|
||
|
|
|
||
|
|
|
||
|
|
|
||
|
|
Task<BranchTableOperationResult<object>> DeleteBranchSectionAsync(
|
||
|
|
|
||
|
|
string cafeId,
|
||
|
|
|
||
|
|
string branchId,
|
||
|
|
|
||
|
|
string sectionId,
|
||
|
|
|
||
|
|
CancellationToken cancellationToken = default);
|
||
|
|
|
||
|
|
|
||
|
|
|
||
|
|
Task<bool> CanAccessBranchAsync(
|
||
|
|
|
||
|
|
string cafeId,
|
||
|
|
|
||
|
|
string branchId,
|
||
|
|
|
||
|
|
string? userId,
|
||
|
|
|
||
|
|
EmployeeRole? role,
|
||
|
|
|
||
|
|
CancellationToken cancellationToken = default);
|
||
|
|
|
||
|
|
|
||
|
|
|
||
|
|
Task<QrResolveResponse?> ResolveQrAsync(string qrCode, CancellationToken cancellationToken = default);
|
||
|
|
|
||
|
|
Task<byte[]?> GetQrPngAsync(string cafeId, string tableId, CancellationToken cancellationToken = default);
|
||
|
|
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
|
||
|
|
public class TableService : ITableService
|
||
|
|
|
||
|
|
{
|
||
|
|
|
||
|
|
private static readonly OrderStatus[] OpenOrderStatuses =
|
||
|
|
|
||
|
|
[
|
||
|
|
|
||
|
|
OrderStatus.Pending,
|
||
|
|
|
||
|
|
OrderStatus.Confirmed,
|
||
|
|
|
||
|
|
OrderStatus.Preparing,
|
||
|
|
|
||
|
|
OrderStatus.Ready
|
||
|
|
|
||
|
|
];
|
||
|
|
|
||
|
|
|
||
|
|
|
||
|
|
private readonly AppDbContext _db;
|
||
|
|
|
||
|
|
private readonly IConfiguration _configuration;
|
||
|
|
|
||
|
|
private readonly IKdsNotifier _kdsNotifier;
|
||
|
|
|
||
|
|
private readonly IBranchIdentityService _identity;
|
||
|
|
|
||
|
|
|
||
|
|
|
||
|
|
public TableService(
|
||
|
|
AppDbContext db,
|
||
|
|
IConfiguration configuration,
|
||
|
|
IKdsNotifier kdsNotifier,
|
||
|
|
IBranchIdentityService identity)
|
||
|
|
|
||
|
|
{
|
||
|
|
|
||
|
|
_db = db;
|
||
|
|
|
||
|
|
_configuration = configuration;
|
||
|
|
|
||
|
|
_kdsNotifier = kdsNotifier;
|
||
|
|
|
||
|
|
_identity = identity;
|
||
|
|
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
|
||
|
|
public async Task<IReadOnlyList<TableBoardDto>> GetTableBoardAsync(
|
||
|
|
|
||
|
|
string cafeId,
|
||
|
|
|
||
|
|
bool activeOnly = true,
|
||
|
|
|
||
|
|
string? branchId = null,
|
||
|
|
|
||
|
|
CancellationToken cancellationToken = default)
|
||
|
|
|
||
|
|
{
|
||
|
|
|
||
|
|
var query = _db.Tables.Where(t => t.CafeId == cafeId);
|
||
|
|
|
||
|
|
if (!string.IsNullOrEmpty(branchId))
|
||
|
|
|
||
|
|
query = query.Where(t => t.BranchId == branchId);
|
||
|
|
|
||
|
|
if (activeOnly)
|
||
|
|
|
||
|
|
query = query.Where(t => t.IsActive);
|
||
|
|
|
||
|
|
|
||
|
|
|
||
|
|
var tables = await LoadTablesOrderedAsync(query, cancellationToken);
|
||
|
|
|
||
|
|
return await BuildBoardDtosAsync(cafeId, tables, cancellationToken);
|
||
|
|
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
|
||
|
|
public Task<IReadOnlyList<TableBoardDto>> GetBranchTableBoardAsync(
|
||
|
|
|
||
|
|
string cafeId,
|
||
|
|
|
||
|
|
string branchId,
|
||
|
|
|
||
|
|
bool activeOnly = true,
|
||
|
|
|
||
|
|
CancellationToken cancellationToken = default) =>
|
||
|
|
|
||
|
|
GetTableBoardAsync(cafeId, activeOnly, branchId, cancellationToken);
|
||
|
|
|
||
|
|
|
||
|
|
|
||
|
|
public async Task<IReadOnlyList<TableDto>> GetTablesAsync(
|
||
|
|
|
||
|
|
string cafeId,
|
||
|
|
|
||
|
|
string? branchId = null,
|
||
|
|
|
||
|
|
CancellationToken cancellationToken = default)
|
||
|
|
|
||
|
|
{
|
||
|
|
|
||
|
|
var query = _db.Tables.Where(t => t.CafeId == cafeId && t.IsActive);
|
||
|
|
|
||
|
|
if (!string.IsNullOrEmpty(branchId))
|
||
|
|
|
||
|
|
query = query.Where(t => t.BranchId == branchId);
|
||
|
|
|
||
|
|
var tables = await LoadTablesOrderedAsync(query, cancellationToken);
|
||
|
|
|
||
|
|
return tables.Select(ToDto).ToList();
|
||
|
|
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
|
||
|
|
public async Task<IReadOnlyList<TableDto>?> GetBranchTablesAsync(
|
||
|
|
string cafeId,
|
||
|
|
string branchId,
|
||
|
|
CancellationToken cancellationToken = default)
|
||
|
|
{
|
||
|
|
if (!await BranchExistsAsync(cafeId, branchId, cancellationToken))
|
||
|
|
return null;
|
||
|
|
|
||
|
|
|
||
|
|
|
||
|
|
return await GetTablesAsync(cafeId, branchId, cancellationToken);
|
||
|
|
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
|
||
|
|
public async Task<TableDto?> CreateTableAsync(
|
||
|
|
|
||
|
|
string cafeId,
|
||
|
|
|
||
|
|
CreateTableRequest request,
|
||
|
|
|
||
|
|
CancellationToken cancellationToken = default)
|
||
|
|
|
||
|
|
{
|
||
|
|
|
||
|
|
var branchId = request.BranchId;
|
||
|
|
|
||
|
|
if (string.IsNullOrEmpty(branchId))
|
||
|
|
|
||
|
|
branchId = await GetDefaultBranchIdAsync(cafeId, cancellationToken);
|
||
|
|
|
||
|
|
if (branchId is null) return null;
|
||
|
|
|
||
|
|
|
||
|
|
|
||
|
|
if (!await BranchExistsAsync(cafeId, branchId, cancellationToken))
|
||
|
|
|
||
|
|
return null;
|
||
|
|
|
||
|
|
|
||
|
|
|
||
|
|
var entity = new Table
|
||
|
|
|
||
|
|
{
|
||
|
|
|
||
|
|
CafeId = cafeId,
|
||
|
|
|
||
|
|
BranchId = branchId,
|
||
|
|
|
||
|
|
Number = request.Number.Trim(),
|
||
|
|
|
||
|
|
Capacity = request.Capacity,
|
||
|
|
|
||
|
|
Floor = request.Floor?.Trim(),
|
||
|
|
|
||
|
|
QrCode = Guid.NewGuid().ToString("N"),
|
||
|
|
|
||
|
|
IsActive = request.IsActive
|
||
|
|
|
||
|
|
};
|
||
|
|
|
||
|
|
|
||
|
|
|
||
|
|
_db.Tables.Add(entity);
|
||
|
|
|
||
|
|
await _db.SaveChangesAsync(cancellationToken);
|
||
|
|
|
||
|
|
return ToDto(entity);
|
||
|
|
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
|
||
|
|
public async Task<BranchTableOperationResult<TableDto>> CreateBranchTableAsync(
|
||
|
|
|
||
|
|
string cafeId,
|
||
|
|
|
||
|
|
string branchId,
|
||
|
|
|
||
|
|
CreateBranchTableRequest request,
|
||
|
|
|
||
|
|
CancellationToken cancellationToken = default)
|
||
|
|
|
||
|
|
{
|
||
|
|
|
||
|
|
if (!await BranchExistsAsync(cafeId, branchId, cancellationToken))
|
||
|
|
|
||
|
|
return Fail<TableDto>("BRANCH_NOT_FOUND", "Branch not found.");
|
||
|
|
|
||
|
|
|
||
|
|
|
||
|
|
if (!string.IsNullOrEmpty(request.SectionId))
|
||
|
|
|
||
|
|
{
|
||
|
|
|
||
|
|
var sectionOk = await _db.TableSections.AnyAsync(
|
||
|
|
|
||
|
|
s => s.Id == request.SectionId && s.CafeId == cafeId && s.BranchId == branchId,
|
||
|
|
|
||
|
|
cancellationToken);
|
||
|
|
|
||
|
|
if (!sectionOk)
|
||
|
|
|
||
|
|
return Fail<TableDto>("SECTION_NOT_FOUND", "Section not found.");
|
||
|
|
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
|
||
|
|
var entity = new Table
|
||
|
|
|
||
|
|
{
|
||
|
|
|
||
|
|
CafeId = cafeId,
|
||
|
|
|
||
|
|
BranchId = branchId,
|
||
|
|
|
||
|
|
SectionId = request.SectionId,
|
||
|
|
|
||
|
|
Number = request.Number.Trim(),
|
||
|
|
|
||
|
|
Capacity = request.Capacity,
|
||
|
|
|
||
|
|
Floor = request.Floor?.Trim(),
|
||
|
|
|
||
|
|
SortOrder = request.SortOrder,
|
||
|
|
|
||
|
|
QrCode = Guid.NewGuid().ToString("N"),
|
||
|
|
|
||
|
|
IsActive = request.IsActive
|
||
|
|
|
||
|
|
};
|
||
|
|
|
||
|
|
|
||
|
|
|
||
|
|
_db.Tables.Add(entity);
|
||
|
|
|
||
|
|
await _db.SaveChangesAsync(cancellationToken);
|
||
|
|
|
||
|
|
await _db.Entry(entity).Reference(t => t.Section).LoadAsync(cancellationToken);
|
||
|
|
|
||
|
|
return Ok(ToDto(entity));
|
||
|
|
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
|
||
|
|
public async Task<TableDto?> PatchTableAsync(
|
||
|
|
|
||
|
|
string cafeId,
|
||
|
|
|
||
|
|
string tableId,
|
||
|
|
|
||
|
|
PatchTableRequest request,
|
||
|
|
|
||
|
|
CancellationToken cancellationToken = default)
|
||
|
|
|
||
|
|
{
|
||
|
|
|
||
|
|
var entity = await _db.Tables
|
||
|
|
|
||
|
|
.Include(t => t.Section)
|
||
|
|
|
||
|
|
.FirstOrDefaultAsync(t => t.Id == tableId && t.CafeId == cafeId, cancellationToken);
|
||
|
|
|
||
|
|
if (entity is null) return null;
|
||
|
|
|
||
|
|
|
||
|
|
|
||
|
|
if (request.Number is not null) entity.Number = request.Number.Trim();
|
||
|
|
|
||
|
|
if (request.Capacity.HasValue) entity.Capacity = request.Capacity.Value;
|
||
|
|
|
||
|
|
if (request.Floor is not null) entity.Floor = request.Floor.Trim();
|
||
|
|
|
||
|
|
if (request.BranchId is not null && !string.IsNullOrEmpty(request.BranchId))
|
||
|
|
|
||
|
|
{
|
||
|
|
|
||
|
|
if (!await BranchExistsAsync(cafeId, request.BranchId, cancellationToken))
|
||
|
|
|
||
|
|
return null;
|
||
|
|
|
||
|
|
entity.BranchId = request.BranchId;
|
||
|
|
|
||
|
|
}
|
||
|
|
|
||
|
|
if (request.ImageUrl is not null)
|
||
|
|
|
||
|
|
entity.ImageUrl = string.IsNullOrWhiteSpace(request.ImageUrl) ? null : request.ImageUrl;
|
||
|
|
|
||
|
|
if (request.VideoUrl is not null)
|
||
|
|
|
||
|
|
entity.VideoUrl = string.IsNullOrWhiteSpace(request.VideoUrl) ? null : request.VideoUrl;
|
||
|
|
|
||
|
|
if (request.IsActive.HasValue) entity.IsActive = request.IsActive.Value;
|
||
|
|
|
||
|
|
if (request.IsCleaning.HasValue) entity.IsCleaning = request.IsCleaning.Value;
|
||
|
|
|
||
|
|
|
||
|
|
|
||
|
|
await _db.SaveChangesAsync(cancellationToken);
|
||
|
|
|
||
|
|
return ToDto(entity);
|
||
|
|
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
|
||
|
|
public async Task<BranchTableOperationResult<TableDto>> PatchBranchTableAsync(
|
||
|
|
|
||
|
|
string cafeId,
|
||
|
|
|
||
|
|
string branchId,
|
||
|
|
|
||
|
|
string tableId,
|
||
|
|
|
||
|
|
PatchBranchTableRequest request,
|
||
|
|
|
||
|
|
CancellationToken cancellationToken = default)
|
||
|
|
|
||
|
|
{
|
||
|
|
|
||
|
|
var entity = await _db.Tables
|
||
|
|
|
||
|
|
.Include(t => t.Section)
|
||
|
|
|
||
|
|
.FirstOrDefaultAsync(
|
||
|
|
|
||
|
|
t => t.Id == tableId && t.CafeId == cafeId && t.BranchId == branchId,
|
||
|
|
|
||
|
|
cancellationToken);
|
||
|
|
|
||
|
|
if (entity is null)
|
||
|
|
|
||
|
|
return Fail<TableDto>("TABLE_NOT_FOUND", "Table not found.");
|
||
|
|
|
||
|
|
|
||
|
|
|
||
|
|
if (request.SectionId is not null)
|
||
|
|
|
||
|
|
{
|
||
|
|
|
||
|
|
if (string.IsNullOrEmpty(request.SectionId))
|
||
|
|
|
||
|
|
entity.SectionId = null;
|
||
|
|
|
||
|
|
else
|
||
|
|
|
||
|
|
{
|
||
|
|
|
||
|
|
var sectionOk = await _db.TableSections.AnyAsync(
|
||
|
|
|
||
|
|
s => s.Id == request.SectionId && s.CafeId == cafeId && s.BranchId == branchId,
|
||
|
|
|
||
|
|
cancellationToken);
|
||
|
|
|
||
|
|
if (!sectionOk)
|
||
|
|
|
||
|
|
return Fail<TableDto>("SECTION_NOT_FOUND", "Section not found.");
|
||
|
|
|
||
|
|
entity.SectionId = request.SectionId;
|
||
|
|
|
||
|
|
}
|
||
|
|
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
|
||
|
|
if (request.Number is not null) entity.Number = request.Number.Trim();
|
||
|
|
|
||
|
|
if (request.Capacity.HasValue) entity.Capacity = request.Capacity.Value;
|
||
|
|
|
||
|
|
if (request.Floor is not null) entity.Floor = request.Floor.Trim();
|
||
|
|
|
||
|
|
if (request.SortOrder.HasValue) entity.SortOrder = request.SortOrder.Value;
|
||
|
|
|
||
|
|
if (request.ImageUrl is not null)
|
||
|
|
|
||
|
|
entity.ImageUrl = string.IsNullOrWhiteSpace(request.ImageUrl) ? null : request.ImageUrl;
|
||
|
|
|
||
|
|
if (request.VideoUrl is not null)
|
||
|
|
|
||
|
|
entity.VideoUrl = string.IsNullOrWhiteSpace(request.VideoUrl) ? null : request.VideoUrl;
|
||
|
|
|
||
|
|
if (request.IsActive.HasValue) entity.IsActive = request.IsActive.Value;
|
||
|
|
|
||
|
|
if (request.IsCleaning.HasValue) entity.IsCleaning = request.IsCleaning.Value;
|
||
|
|
|
||
|
|
|
||
|
|
|
||
|
|
await _db.SaveChangesAsync(cancellationToken);
|
||
|
|
|
||
|
|
await _db.Entry(entity).Reference(t => t.Section).LoadAsync(cancellationToken);
|
||
|
|
|
||
|
|
return Ok(ToDto(entity));
|
||
|
|
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
|
||
|
|
public async Task<BranchTableOperationResult<object>> DeleteTableAsync(
|
||
|
|
string cafeId,
|
||
|
|
string tableId,
|
||
|
|
CancellationToken cancellationToken = default)
|
||
|
|
{
|
||
|
|
var branchId = await _db.Tables
|
||
|
|
.Where(t => t.Id == tableId && t.CafeId == cafeId)
|
||
|
|
.Select(t => t.BranchId)
|
||
|
|
.FirstOrDefaultAsync(cancellationToken);
|
||
|
|
if (string.IsNullOrEmpty(branchId))
|
||
|
|
return Fail<object>("TABLE_NOT_FOUND", "Table not found.");
|
||
|
|
return await DeleteBranchTableAsync(cafeId, branchId, tableId, cancellationToken);
|
||
|
|
}
|
||
|
|
|
||
|
|
public async Task<BranchTableOperationResult<object>> DeleteBranchTableAsync(
|
||
|
|
|
||
|
|
string cafeId,
|
||
|
|
|
||
|
|
string branchId,
|
||
|
|
|
||
|
|
string tableId,
|
||
|
|
|
||
|
|
CancellationToken cancellationToken = default)
|
||
|
|
|
||
|
|
{
|
||
|
|
|
||
|
|
var entity = await _db.Tables.FirstOrDefaultAsync(
|
||
|
|
|
||
|
|
t => t.Id == tableId && t.CafeId == cafeId && t.BranchId == branchId,
|
||
|
|
|
||
|
|
cancellationToken);
|
||
|
|
|
||
|
|
if (entity is null)
|
||
|
|
|
||
|
|
return Fail<object>("TABLE_NOT_FOUND", "Table not found.");
|
||
|
|
|
||
|
|
|
||
|
|
|
||
|
|
var hasOpenOrder = await _db.Orders.AnyAsync(
|
||
|
|
|
||
|
|
o => o.CafeId == cafeId
|
||
|
|
|
||
|
|
&& o.TableId == tableId
|
||
|
|
|
||
|
|
&& OpenOrderStatuses.Contains(o.Status),
|
||
|
|
|
||
|
|
cancellationToken);
|
||
|
|
|
||
|
|
if (hasOpenOrder)
|
||
|
|
|
||
|
|
return Fail<object>("TABLE_HAS_OPEN_ORDER", "This table has an open order.");
|
||
|
|
|
||
|
|
|
||
|
|
|
||
|
|
entity.DeletedAt = DateTime.UtcNow;
|
||
|
|
|
||
|
|
entity.IsActive = false;
|
||
|
|
|
||
|
|
entity.IsCleaning = false;
|
||
|
|
|
||
|
|
await _db.SaveChangesAsync(cancellationToken);
|
||
|
|
|
||
|
|
await _kdsNotifier.NotifyTableStatusChangedAsync(cafeId, cancellationToken);
|
||
|
|
|
||
|
|
return Ok<object>(new { tableId });
|
||
|
|
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
|
||
|
|
public async Task<IReadOnlyList<TableSectionDto>?> GetBranchSectionsAsync(
|
||
|
|
|
||
|
|
string cafeId,
|
||
|
|
|
||
|
|
string branchId,
|
||
|
|
|
||
|
|
CancellationToken cancellationToken = default)
|
||
|
|
|
||
|
|
{
|
||
|
|
|
||
|
|
if (!await BranchExistsAsync(cafeId, branchId, cancellationToken))
|
||
|
|
|
||
|
|
return null;
|
||
|
|
|
||
|
|
|
||
|
|
|
||
|
|
var sections = await _db.TableSections
|
||
|
|
.Where(s => s.CafeId == cafeId && s.BranchId == branchId)
|
||
|
|
.OrderBy(s => s.SortOrder)
|
||
|
|
.ThenBy(s => s.Name)
|
||
|
|
.ToListAsync(cancellationToken);
|
||
|
|
|
||
|
|
var tableCounts = await _db.Tables
|
||
|
|
.Where(t => t.CafeId == cafeId && t.BranchId == branchId && t.SectionId != null)
|
||
|
|
.GroupBy(t => t.SectionId!)
|
||
|
|
.Select(g => new { SectionId = g.Key, Count = g.Count() })
|
||
|
|
.ToDictionaryAsync(x => x.SectionId, x => x.Count, cancellationToken);
|
||
|
|
|
||
|
|
return sections
|
||
|
|
.Select(s => new TableSectionDto(
|
||
|
|
s.Id,
|
||
|
|
s.BranchId,
|
||
|
|
s.Name,
|
||
|
|
s.SortOrder,
|
||
|
|
s.IsActive,
|
||
|
|
tableCounts.GetValueOrDefault(s.Id)))
|
||
|
|
.ToList();
|
||
|
|
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
|
||
|
|
public async Task<BranchTableOperationResult<TableSectionDto>> CreateBranchSectionAsync(
|
||
|
|
|
||
|
|
string cafeId,
|
||
|
|
|
||
|
|
string branchId,
|
||
|
|
|
||
|
|
CreateTableSectionRequest request,
|
||
|
|
|
||
|
|
CancellationToken cancellationToken = default)
|
||
|
|
|
||
|
|
{
|
||
|
|
|
||
|
|
if (!await BranchExistsAsync(cafeId, branchId, cancellationToken))
|
||
|
|
|
||
|
|
return Fail<TableSectionDto>("BRANCH_NOT_FOUND", "Branch not found.");
|
||
|
|
|
||
|
|
|
||
|
|
|
||
|
|
var entity = new TableSection
|
||
|
|
|
||
|
|
{
|
||
|
|
|
||
|
|
CafeId = cafeId,
|
||
|
|
|
||
|
|
BranchId = branchId,
|
||
|
|
|
||
|
|
Name = request.Name.Trim(),
|
||
|
|
|
||
|
|
SortOrder = request.SortOrder
|
||
|
|
|
||
|
|
};
|
||
|
|
|
||
|
|
_db.TableSections.Add(entity);
|
||
|
|
|
||
|
|
await _db.SaveChangesAsync(cancellationToken);
|
||
|
|
|
||
|
|
return Ok(new TableSectionDto(entity.Id, entity.BranchId, entity.Name, entity.SortOrder, entity.IsActive, 0));
|
||
|
|
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
|
||
|
|
public async Task<BranchTableOperationResult<TableSectionDto>> PatchBranchSectionAsync(
|
||
|
|
|
||
|
|
string cafeId,
|
||
|
|
|
||
|
|
string branchId,
|
||
|
|
|
||
|
|
string sectionId,
|
||
|
|
|
||
|
|
PatchTableSectionRequest request,
|
||
|
|
|
||
|
|
CancellationToken cancellationToken = default)
|
||
|
|
|
||
|
|
{
|
||
|
|
|
||
|
|
var entity = await _db.TableSections.FirstOrDefaultAsync(
|
||
|
|
|
||
|
|
s => s.Id == sectionId && s.CafeId == cafeId && s.BranchId == branchId,
|
||
|
|
|
||
|
|
cancellationToken);
|
||
|
|
|
||
|
|
if (entity is null)
|
||
|
|
|
||
|
|
return Fail<TableSectionDto>("SECTION_NOT_FOUND", "Section not found.");
|
||
|
|
|
||
|
|
|
||
|
|
|
||
|
|
if (request.Name is not null) entity.Name = request.Name.Trim();
|
||
|
|
|
||
|
|
if (request.SortOrder.HasValue) entity.SortOrder = request.SortOrder.Value;
|
||
|
|
|
||
|
|
if (request.IsActive.HasValue) entity.IsActive = request.IsActive.Value;
|
||
|
|
|
||
|
|
|
||
|
|
|
||
|
|
await _db.SaveChangesAsync(cancellationToken);
|
||
|
|
|
||
|
|
var tableCount = await _db.Tables.CountAsync(
|
||
|
|
|
||
|
|
t => t.SectionId == sectionId && t.CafeId == cafeId,
|
||
|
|
|
||
|
|
cancellationToken);
|
||
|
|
|
||
|
|
return Ok(new TableSectionDto(entity.Id, entity.BranchId, entity.Name, entity.SortOrder, entity.IsActive, tableCount));
|
||
|
|
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
|
||
|
|
public async Task<BranchTableOperationResult<object>> DeleteBranchSectionAsync(
|
||
|
|
|
||
|
|
string cafeId,
|
||
|
|
|
||
|
|
string branchId,
|
||
|
|
|
||
|
|
string sectionId,
|
||
|
|
|
||
|
|
CancellationToken cancellationToken = default)
|
||
|
|
|
||
|
|
{
|
||
|
|
|
||
|
|
var entity = await _db.TableSections.FirstOrDefaultAsync(
|
||
|
|
|
||
|
|
s => s.Id == sectionId && s.CafeId == cafeId && s.BranchId == branchId,
|
||
|
|
|
||
|
|
cancellationToken);
|
||
|
|
|
||
|
|
if (entity is null)
|
||
|
|
|
||
|
|
return Fail<object>("SECTION_NOT_FOUND", "Section not found.");
|
||
|
|
|
||
|
|
|
||
|
|
|
||
|
|
var hasTables = await _db.Tables.AnyAsync(
|
||
|
|
|
||
|
|
t => t.SectionId == sectionId && t.CafeId == cafeId && t.BranchId == branchId,
|
||
|
|
|
||
|
|
cancellationToken);
|
||
|
|
|
||
|
|
if (hasTables)
|
||
|
|
|
||
|
|
return Fail<object>("TABLE_SECTION_HAS_TABLES", "Section has tables assigned.");
|
||
|
|
|
||
|
|
|
||
|
|
|
||
|
|
entity.DeletedAt = DateTime.UtcNow;
|
||
|
|
|
||
|
|
await _db.SaveChangesAsync(cancellationToken);
|
||
|
|
|
||
|
|
return Ok<object>(new { sectionId });
|
||
|
|
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
|
||
|
|
public async Task<bool> CanAccessBranchAsync(
|
||
|
|
|
||
|
|
string cafeId,
|
||
|
|
|
||
|
|
string branchId,
|
||
|
|
|
||
|
|
string? userId,
|
||
|
|
|
||
|
|
EmployeeRole? role,
|
||
|
|
|
||
|
|
CancellationToken cancellationToken = default)
|
||
|
|
|
||
|
|
{
|
||
|
|
|
||
|
|
if (!await BranchExistsAsync(cafeId, branchId, cancellationToken))
|
||
|
|
|
||
|
|
return false;
|
||
|
|
|
||
|
|
if (role is not EmployeeRole.Manager)
|
||
|
|
|
||
|
|
return true;
|
||
|
|
|
||
|
|
if (string.IsNullOrEmpty(userId))
|
||
|
|
|
||
|
|
return false;
|
||
|
|
|
||
|
|
var employee = await _db.Employees.FirstOrDefaultAsync(
|
||
|
|
|
||
|
|
e => e.Id == userId && e.CafeId == cafeId,
|
||
|
|
|
||
|
|
cancellationToken);
|
||
|
|
|
||
|
|
return employee?.BranchId == branchId;
|
||
|
|
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
|
||
|
|
public async Task<TableBoardDto?> SetTableCleaningAsync(
|
||
|
|
|
||
|
|
string cafeId,
|
||
|
|
|
||
|
|
string tableId,
|
||
|
|
|
||
|
|
bool isCleaning,
|
||
|
|
|
||
|
|
CancellationToken cancellationToken = default)
|
||
|
|
|
||
|
|
{
|
||
|
|
|
||
|
|
var entity = await _db.Tables.FirstOrDefaultAsync(t => t.Id == tableId && t.CafeId == cafeId, cancellationToken);
|
||
|
|
|
||
|
|
if (entity is null) return null;
|
||
|
|
|
||
|
|
|
||
|
|
|
||
|
|
entity.IsCleaning = isCleaning;
|
||
|
|
|
||
|
|
await _db.SaveChangesAsync(cancellationToken);
|
||
|
|
|
||
|
|
await _kdsNotifier.NotifyTableStatusChangedAsync(cafeId, cancellationToken);
|
||
|
|
|
||
|
|
var board = await GetTableBoardAsync(cafeId, activeOnly: false, branchId: entity.BranchId, cancellationToken);
|
||
|
|
|
||
|
|
return board.FirstOrDefault(t => t.Id == tableId);
|
||
|
|
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
|
||
|
|
public async Task<QrResolveResponse?> ResolveQrAsync(string qrCode, CancellationToken cancellationToken = default)
|
||
|
|
|
||
|
|
{
|
||
|
|
|
||
|
|
var table = await _db.Tables
|
||
|
|
.Include(t => t.Cafe)
|
||
|
|
.Include(t => t.Branch)
|
||
|
|
.FirstOrDefaultAsync(
|
||
|
|
t => t.QrCode == qrCode && t.IsActive && t.DeletedAt == null,
|
||
|
|
cancellationToken);
|
||
|
|
|
||
|
|
if (table?.Cafe is null || table.Branch is null || !table.Branch.IsActive)
|
||
|
|
return null;
|
||
|
|
|
||
|
|
var identity = await _identity.GetEffectiveIdentityAsync(
|
||
|
|
table.CafeId,
|
||
|
|
table.BranchId,
|
||
|
|
cancellationToken);
|
||
|
|
|
||
|
|
return new QrResolveResponse(
|
||
|
|
table.CafeId,
|
||
|
|
table.Cafe.Slug,
|
||
|
|
table.Id,
|
||
|
|
table.Number,
|
||
|
|
table.Number,
|
||
|
|
table.BranchId,
|
||
|
|
table.Branch.Name,
|
||
|
|
table.Cafe.Name,
|
||
|
|
identity?.PrimaryColor ?? "#0F6E56",
|
||
|
|
identity?.LogoUrl ?? table.Cafe.LogoUrl,
|
||
|
|
identity?.WelcomeText ?? "خوش آمدید",
|
||
|
|
identity?.WifiPassword,
|
||
|
|
identity?.Address,
|
||
|
|
table.IsCleaning);
|
||
|
|
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
|
||
|
|
public async Task<byte[]?> GetQrPngAsync(string cafeId, string tableId, CancellationToken cancellationToken = default)
|
||
|
|
|
||
|
|
{
|
||
|
|
|
||
|
|
var table = await _db.Tables
|
||
|
|
|
||
|
|
.FirstOrDefaultAsync(t => t.Id == tableId && t.CafeId == cafeId, cancellationToken);
|
||
|
|
|
||
|
|
if (table is null) return null;
|
||
|
|
|
||
|
|
|
||
|
|
|
||
|
|
var url = BuildPublicQrUrl(table.QrCode);
|
||
|
|
|
||
|
|
using var generator = new QRCodeGenerator();
|
||
|
|
|
||
|
|
using var data = generator.CreateQrCode(url, QRCodeGenerator.ECCLevel.Q);
|
||
|
|
|
||
|
|
var png = new PngByteQRCode(data);
|
||
|
|
|
||
|
|
return png.GetGraphic(20);
|
||
|
|
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
|
||
|
|
private static BranchTableOperationResult<T> Ok<T>(T data) =>
|
||
|
|
|
||
|
|
new(true, data, null, null);
|
||
|
|
|
||
|
|
|
||
|
|
|
||
|
|
private static BranchTableOperationResult<T> Fail<T>(string code, string message) =>
|
||
|
|
|
||
|
|
new(false, default, code, message);
|
||
|
|
|
||
|
|
|
||
|
|
|
||
|
|
private async Task<bool> BranchExistsAsync(string cafeId, string branchId, CancellationToken ct) =>
|
||
|
|
|
||
|
|
await _db.Branches.AnyAsync(b => b.Id == branchId && b.CafeId == cafeId, ct);
|
||
|
|
|
||
|
|
|
||
|
|
|
||
|
|
private async Task<string?> GetDefaultBranchIdAsync(string cafeId, CancellationToken ct) =>
|
||
|
|
|
||
|
|
await _db.Branches
|
||
|
|
|
||
|
|
.Where(b => b.CafeId == cafeId && b.IsActive)
|
||
|
|
|
||
|
|
.OrderBy(b => b.CreatedAt)
|
||
|
|
|
||
|
|
.Select(b => b.Id)
|
||
|
|
|
||
|
|
.FirstOrDefaultAsync(ct);
|
||
|
|
|
||
|
|
|
||
|
|
|
||
|
|
private static async Task<List<Table>> LoadTablesOrderedAsync(
|
||
|
|
|
||
|
|
IQueryable<Table> query,
|
||
|
|
|
||
|
|
CancellationToken ct)
|
||
|
|
|
||
|
|
{
|
||
|
|
|
||
|
|
var tables = await query.Include(t => t.Section).ToListAsync(ct);
|
||
|
|
|
||
|
|
return tables
|
||
|
|
|
||
|
|
.OrderBy(t => t.Section?.SortOrder ?? int.MaxValue)
|
||
|
|
|
||
|
|
.ThenBy(t => t.SortOrder)
|
||
|
|
|
||
|
|
.ThenBy(t => t.Number, StringComparer.Ordinal)
|
||
|
|
|
||
|
|
.ToList();
|
||
|
|
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
|
||
|
|
private async Task<IReadOnlyList<TableBoardDto>> BuildBoardDtosAsync(
|
||
|
|
|
||
|
|
string cafeId,
|
||
|
|
|
||
|
|
List<Table> tables,
|
||
|
|
|
||
|
|
CancellationToken cancellationToken)
|
||
|
|
|
||
|
|
{
|
||
|
|
|
||
|
|
if (tables.Count == 0)
|
||
|
|
|
||
|
|
return [];
|
||
|
|
|
||
|
|
|
||
|
|
|
||
|
|
var tableIds = tables.Select(t => t.Id).ToList();
|
||
|
|
|
||
|
|
var today = DateOnly.FromDateTime(DateTime.UtcNow);
|
||
|
|
|
||
|
|
|
||
|
|
|
||
|
|
var openOrders = await _db.Orders
|
||
|
|
|
||
|
|
.Include(o => o.Customer)
|
||
|
|
|
||
|
|
.Where(o => o.CafeId == cafeId
|
||
|
|
|
||
|
|
&& o.TableId != null
|
||
|
|
|
||
|
|
&& tableIds.Contains(o.TableId)
|
||
|
|
|
||
|
|
&& OpenOrderStatuses.Contains(o.Status))
|
||
|
|
|
||
|
|
.ToListAsync(cancellationToken);
|
||
|
|
|
||
|
|
|
||
|
|
|
||
|
|
var reservations = await _db.TableReservations
|
||
|
|
|
||
|
|
.Where(r => r.CafeId == cafeId
|
||
|
|
|
||
|
|
&& r.TableId != null
|
||
|
|
|
||
|
|
&& tableIds.Contains(r.TableId!)
|
||
|
|
|
||
|
|
&& r.Date == today
|
||
|
|
|
||
|
|
&& r.Status != ReservationStatus.Cancelled)
|
||
|
|
|
||
|
|
.ToListAsync(cancellationToken);
|
||
|
|
|
||
|
|
|
||
|
|
|
||
|
|
return tables.Select(t =>
|
||
|
|
|
||
|
|
{
|
||
|
|
|
||
|
|
var order = openOrders.FirstOrDefault(o => o.TableId == t.Id);
|
||
|
|
|
||
|
|
var reservation = reservations.FirstOrDefault(r => r.TableId == t.Id && order is null);
|
||
|
|
|
||
|
|
|
||
|
|
|
||
|
|
var status = t.IsCleaning
|
||
|
|
|
||
|
|
? TableBoardStatus.Cleaning
|
||
|
|
|
||
|
|
: order is not null
|
||
|
|
|
||
|
|
? TableBoardStatus.Busy
|
||
|
|
|
||
|
|
: reservation is not null
|
||
|
|
|
||
|
|
? TableBoardStatus.Reserved
|
||
|
|
|
||
|
|
: TableBoardStatus.Free;
|
||
|
|
|
||
|
|
|
||
|
|
|
||
|
|
TableCurrentOrderSummary? current = null;
|
||
|
|
|
||
|
|
if (order is not null)
|
||
|
|
|
||
|
|
{
|
||
|
|
|
||
|
|
var guestLabel = !string.IsNullOrWhiteSpace(order.GuestName)
|
||
|
|
|
||
|
|
? order.GuestName
|
||
|
|
|
||
|
|
: order.Customer?.Name;
|
||
|
|
|
||
|
|
current = new TableCurrentOrderSummary(
|
||
|
|
order.Id,
|
||
|
|
order.Status,
|
||
|
|
order.Total,
|
||
|
|
guestLabel,
|
||
|
|
order.Source);
|
||
|
|
|
||
|
|
}
|
||
|
|
|
||
|
|
else if (reservation is not null)
|
||
|
|
|
||
|
|
{
|
||
|
|
|
||
|
|
current = new TableCurrentOrderSummary(
|
||
|
|
|
||
|
|
reservation.Id,
|
||
|
|
|
||
|
|
OrderStatus.Pending,
|
||
|
|
|
||
|
|
0,
|
||
|
|
|
||
|
|
reservation.GuestName);
|
||
|
|
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
|
||
|
|
return new TableBoardDto(
|
||
|
|
|
||
|
|
t.Id,
|
||
|
|
|
||
|
|
t.BranchId,
|
||
|
|
|
||
|
|
t.SectionId,
|
||
|
|
|
||
|
|
t.Section?.Name,
|
||
|
|
|
||
|
|
t.SortOrder,
|
||
|
|
|
||
|
|
t.Number,
|
||
|
|
|
||
|
|
t.Capacity,
|
||
|
|
|
||
|
|
t.Floor,
|
||
|
|
|
||
|
|
t.QrCode,
|
||
|
|
|
||
|
|
BuildPublicQrUrl(t.QrCode),
|
||
|
|
|
||
|
|
t.ImageUrl,
|
||
|
|
|
||
|
|
t.VideoUrl,
|
||
|
|
|
||
|
|
t.IsActive,
|
||
|
|
|
||
|
|
status,
|
||
|
|
|
||
|
|
current,
|
||
|
|
|
||
|
|
t.IsCleaning);
|
||
|
|
|
||
|
|
}).ToList();
|
||
|
|
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
|
||
|
|
private TableDto ToDto(Table t) => new(
|
||
|
|
|
||
|
|
t.Id,
|
||
|
|
|
||
|
|
t.BranchId,
|
||
|
|
|
||
|
|
t.SectionId,
|
||
|
|
|
||
|
|
t.Section?.Name,
|
||
|
|
|
||
|
|
t.SortOrder,
|
||
|
|
|
||
|
|
t.Number,
|
||
|
|
|
||
|
|
t.Capacity,
|
||
|
|
|
||
|
|
t.Floor,
|
||
|
|
|
||
|
|
t.QrCode,
|
||
|
|
|
||
|
|
BuildPublicQrUrl(t.QrCode),
|
||
|
|
|
||
|
|
t.ImageUrl,
|
||
|
|
|
||
|
|
t.VideoUrl,
|
||
|
|
|
||
|
|
t.IsActive);
|
||
|
|
|
||
|
|
|
||
|
|
|
||
|
|
private string BuildPublicQrUrl(string qrCode)
|
||
|
|
|
||
|
|
{
|
||
|
|
|
||
|
|
var baseUrl = _configuration["App:QrPublicBaseUrl"]?.TrimEnd('/')
|
||
|
|
|
||
|
|
?? "https://meezi.ir";
|
||
|
|
|
||
|
|
return $"{baseUrl}/q/{qrCode}";
|
||
|
|
|
||
|
|
}
|
||
|
|
|
||
|
|
}
|
||
|
|
|
||
|
|
|