using Meezi.API.Models.Menu; using Meezi.Core.Constants; using Meezi.Core.Entities; using Meezi.Infrastructure.Data; using Microsoft.EntityFrameworkCore; namespace Meezi.API.Services; public interface IMenuService { Task> GetCategoriesAsync(string cafeId, CancellationToken cancellationToken = default); Task CreateCategoryAsync(string cafeId, CreateMenuCategoryRequest request, CancellationToken cancellationToken = default); Task UpdateCategoryAsync(string cafeId, string id, UpdateMenuCategoryRequest request, CancellationToken cancellationToken = default); Task DeleteCategoryAsync(string cafeId, string id, CancellationToken cancellationToken = default); Task> GetItemsAsync(string cafeId, string? categoryId, CancellationToken cancellationToken = default); Task CreateItemAsync(string cafeId, CreateMenuItemRequest request, CancellationToken cancellationToken = default); Task UpdateItemAsync(string cafeId, string id, UpdateMenuItemRequest request, CancellationToken cancellationToken = default); Task SetAvailabilityAsync(string cafeId, string id, bool isAvailable, CancellationToken cancellationToken = default); } public class MenuService : IMenuService { private readonly AppDbContext _db; public MenuService(AppDbContext db) { _db = db; } public async Task> GetCategoriesAsync(string cafeId, CancellationToken cancellationToken = default) { return await _db.MenuCategories .Where(c => c.CafeId == cafeId) .OrderBy(c => c.SortOrder) .Select(c => ToCategoryDto(c)) .ToListAsync(cancellationToken); } public async Task CreateCategoryAsync(string cafeId, CreateMenuCategoryRequest request, CancellationToken cancellationToken = default) { var entity = new MenuCategory { CafeId = cafeId, Name = request.Name, NameAr = request.NameAr, NameEn = request.NameEn, SortOrder = request.SortOrder, TaxId = request.TaxId, DiscountPercent = request.DiscountPercent, Icon = NormalizeOptionalText(request.Icon), IconPresetId = NormalizeIconPreset(request.IconPresetId), IconStyle = NormalizeIconStyle(request.IconStyle) ?? (NormalizeIconPreset(request.IconPresetId) is not null ? CategoryIconPresets.IconStyle.Flat : null), ImageUrl = NormalizeOptionalText(request.ImageUrl), IsActive = request.IsActive, KitchenStationId = string.IsNullOrWhiteSpace(request.KitchenStationId) ? null : request.KitchenStationId }; _db.MenuCategories.Add(entity); await _db.SaveChangesAsync(cancellationToken); return ToCategoryDto(entity); } public async Task UpdateCategoryAsync(string cafeId, string id, UpdateMenuCategoryRequest request, CancellationToken cancellationToken = default) { var entity = await _db.MenuCategories.FirstOrDefaultAsync(c => c.Id == id && c.CafeId == cafeId, cancellationToken); if (entity is null) return null; if (request.Name is not null) entity.Name = request.Name; if (request.NameAr is not null) entity.NameAr = request.NameAr; if (request.NameEn is not null) entity.NameEn = request.NameEn; if (request.SortOrder.HasValue) entity.SortOrder = request.SortOrder.Value; if (request.TaxId is not null) entity.TaxId = request.TaxId; if (request.DiscountPercent.HasValue) entity.DiscountPercent = request.DiscountPercent.Value; if (request.IsActive.HasValue) entity.IsActive = request.IsActive.Value; if (request.Icon is not null) entity.Icon = NormalizeOptionalText(request.Icon); if (request.IconPresetId is not null) entity.IconPresetId = NormalizeIconPreset(request.IconPresetId); if (request.IconStyle is not null) entity.IconStyle = NormalizeIconStyle(request.IconStyle); if (request.ImageUrl is not null) entity.ImageUrl = NormalizeOptionalText(request.ImageUrl); if (request.KitchenStationId is not null) entity.KitchenStationId = string.IsNullOrWhiteSpace(request.KitchenStationId) ? null : request.KitchenStationId; await _db.SaveChangesAsync(cancellationToken); return ToCategoryDto(entity); } public async Task DeleteCategoryAsync(string cafeId, string id, CancellationToken cancellationToken = default) { var entity = await _db.MenuCategories.FirstOrDefaultAsync(c => c.Id == id && c.CafeId == cafeId, cancellationToken); if (entity is null) return false; entity.DeletedAt = DateTime.UtcNow; await _db.SaveChangesAsync(cancellationToken); return true; } public async Task> GetItemsAsync(string cafeId, string? categoryId, CancellationToken cancellationToken = default) { var query = _db.MenuItems.Where(i => i.CafeId == cafeId); if (!string.IsNullOrEmpty(categoryId)) query = query.Where(i => i.CategoryId == categoryId); var items = await query.Include(i => i.Category).OrderBy(i => i.Name).ToListAsync(cancellationToken); return items.Select(ToItemDto).ToList(); } public async Task CreateItemAsync(string cafeId, CreateMenuItemRequest request, CancellationToken cancellationToken = default) { var category = await _db.MenuCategories .FirstOrDefaultAsync(c => c.Id == request.CategoryId && c.CafeId == cafeId, cancellationToken); if (category is null) return null; var imageUrl = string.IsNullOrWhiteSpace(request.ImageUrl) ? MenuItemImageDefaults.GetDefaultImageUrl( MenuItemImageDefaults.InferKind(request.CategoryId, category.Name)) : request.ImageUrl; var entity = new MenuItem { CafeId = cafeId, CategoryId = request.CategoryId, Name = request.Name, NameAr = request.NameAr, NameEn = request.NameEn, Description = request.Description, Price = request.Price, DiscountPercent = request.DiscountPercent, ImageUrl = imageUrl, VideoUrl = request.VideoUrl, Model3dUrl = NormalizeOptionalText(request.Model3dUrl), IsAvailable = request.IsAvailable }; _db.MenuItems.Add(entity); await _db.SaveChangesAsync(cancellationToken); return ToItemDto(entity); } public async Task UpdateItemAsync(string cafeId, string id, UpdateMenuItemRequest request, CancellationToken cancellationToken = default) { var entity = await _db.MenuItems .Include(i => i.Category) .FirstOrDefaultAsync(i => i.Id == id && i.CafeId == cafeId, cancellationToken); if (entity is null) return null; if (request.CategoryId is not null) { var categoryExists = await _db.MenuCategories.AnyAsync(c => c.Id == request.CategoryId && c.CafeId == cafeId, cancellationToken); if (!categoryExists) return null; entity.CategoryId = request.CategoryId; } if (request.Name is not null) entity.Name = request.Name; if (request.NameAr is not null) entity.NameAr = request.NameAr; if (request.NameEn is not null) entity.NameEn = request.NameEn; if (request.Description is not null) entity.Description = request.Description; if (request.Price.HasValue) entity.Price = request.Price.Value; if (request.DiscountPercent.HasValue) entity.DiscountPercent = request.DiscountPercent.Value; if (request.ImageUrl is not null) { entity.ImageUrl = string.IsNullOrWhiteSpace(request.ImageUrl) ? MenuItemImageDefaults.ResolveImageUrl(entity.Id, entity.CategoryId, entity.Category?.Name) : request.ImageUrl; } if (request.VideoUrl is not null) entity.VideoUrl = string.IsNullOrWhiteSpace(request.VideoUrl) ? null : request.VideoUrl; if (request.Model3dUrl is not null) entity.Model3dUrl = string.IsNullOrWhiteSpace(request.Model3dUrl) ? null : request.Model3dUrl.Trim(); if (request.IsAvailable.HasValue) entity.IsAvailable = request.IsAvailable.Value; await _db.SaveChangesAsync(cancellationToken); return ToItemDto(entity); } public async Task SetAvailabilityAsync(string cafeId, string id, bool isAvailable, CancellationToken cancellationToken = default) { var entity = await _db.MenuItems.FirstOrDefaultAsync(i => i.Id == id && i.CafeId == cafeId, cancellationToken); if (entity is null) return null; entity.IsAvailable = isAvailable; await _db.SaveChangesAsync(cancellationToken); return ToItemDto(entity); } private static string? NormalizeOptionalText(string? value) => string.IsNullOrWhiteSpace(value) ? null : value.Trim(); private static string? NormalizeIconPreset(string? value) { var normalized = CategoryIconPresets.NormalizePreset(value); if (normalized is null) return null; return CategoryIconPresets.IsValidPreset(normalized) ? normalized : null; } private static string? NormalizeIconStyle(string? value) { var normalized = CategoryIconPresets.NormalizeStyle(value); if (normalized is null) return null; return CategoryIconPresets.IsValidStyle(normalized) ? normalized : null; } private static MenuCategoryDto ToCategoryDto(MenuCategory c) => new( c.Id, c.Name, c.NameAr, c.NameEn, c.SortOrder, c.TaxId, c.DiscountPercent, c.Icon, c.IconPresetId, c.IconStyle, c.ImageUrl, c.IsActive, c.KitchenStationId); private static MenuItemDto ToItemDto(MenuItem i) => new( i.Id, i.CategoryId, i.Name, i.NameAr, i.NameEn, i.Description, i.Price, i.DiscountPercent, MenuItemImageDefaults.ResolveDisplayImageUrl(i), i.VideoUrl, i.Model3dUrl, i.IsAvailable); }