diff --git a/TeleBot/TeleBot/Handlers/CallbackHandler.cs b/TeleBot/TeleBot/Handlers/CallbackHandler.cs index 2b8e969..4c01349 100644 --- a/TeleBot/TeleBot/Handlers/CallbackHandler.cs +++ b/TeleBot/TeleBot/Handlers/CallbackHandler.cs @@ -170,7 +170,23 @@ namespace TeleBot.Handlers case "cancel_support": await HandleCancelSupport(bot, callbackQuery, session); break; - + + case "selectvar": + await HandleSelectVariant(bot, callbackQuery.Message, session, data); + break; + + case "setvariant": + await HandleSetVariant(bot, callbackQuery.Message, session, data); + break; + + case "addvariant": + await HandleAddVariant(bot, callbackQuery.Message, session, data); + break; + + case "confirmvar": + await HandleConfirmVariant(bot, callbackQuery, session, data); + break; + default: _logger.LogWarning("Unknown callback action: {Action}", action); break; @@ -1126,5 +1142,122 @@ namespace TeleBot.Handlers ); } } + + private async Task HandleSelectVariant(ITelegramBotClient bot, Message message, UserSession session, string[] data) + { + // Format: selectvar:productId:quantity:multiBuyId + var productId = Guid.Parse(data[1]); + var quantity = int.Parse(data[2]); + var multiBuyId = data.Length > 3 && data[3] != "null" ? data[3] : null; + + var product = await _shopService.GetProductAsync(productId); + if (product == null) + { + await bot.SendTextMessageAsync(message.Chat.Id, "Product not found."); + return; + } + + // Clear any previous selections + session.TempData["selected_variants"] = new List(); + session.TempData["current_product"] = productId; + session.TempData["current_quantity"] = quantity; + session.TempData["current_multibuy"] = multiBuyId; + + await bot.EditMessageReplyMarkupAsync( + message.Chat.Id, + message.MessageId, + replyMarkup: MenuBuilder.VariantSelectionMenu(product, quantity, multiBuyId) + ); + + session.State = SessionState.SelectingVariants; + } + + private async Task HandleSetVariant(ITelegramBotClient bot, Message message, UserSession session, string[] data) + { + // Format: setvariant:productId:variantName (for single item) + var productId = Guid.Parse(data[1]); + var variantName = data[2]; + + var product = await _shopService.GetProductAsync(productId); + if (product == null) return; + + // For single item, replace selection + var selectedVariants = new List { variantName }; + session.TempData["selected_variants"] = selectedVariants; + + var quantity = session.TempData.ContainsKey("current_quantity") + ? (int)session.TempData["current_quantity"] + : 1; + + var multiBuyId = session.TempData.ContainsKey("current_multibuy") + ? session.TempData["current_multibuy"] as string + : null; + + await bot.EditMessageReplyMarkupAsync( + message.Chat.Id, + message.MessageId, + replyMarkup: MenuBuilder.VariantSelectionMenu(product, quantity, multiBuyId, selectedVariants) + ); + } + + private async Task HandleAddVariant(ITelegramBotClient bot, Message message, UserSession session, string[] data) + { + // Format: addvariant:productId:quantity:variantName:multiBuyId (for multi-buy) + var productId = Guid.Parse(data[1]); + var quantity = int.Parse(data[2]); + var variantName = data[3]; + var multiBuyId = data.Length > 4 && data[4] != "null" ? data[4] : null; + + var product = await _shopService.GetProductAsync(productId); + if (product == null) return; + + // Get current selections + var selectedVariants = session.TempData.ContainsKey("selected_variants") + ? (List)session.TempData["selected_variants"] + : new List(); + + // Add variant if not at quantity limit + if (selectedVariants.Count < quantity) + { + selectedVariants.Add(variantName); + session.TempData["selected_variants"] = selectedVariants; + } + + await bot.EditMessageReplyMarkupAsync( + message.Chat.Id, + message.MessageId, + replyMarkup: MenuBuilder.VariantSelectionMenu(product, quantity, multiBuyId, selectedVariants) + ); + } + + private async Task HandleConfirmVariant(ITelegramBotClient bot, CallbackQuery callbackQuery, UserSession session, string[] data) + { + // Format: confirmvar:productId:quantity:multiBuyId:variantList + var productId = Guid.Parse(data[1]); + var quantity = int.Parse(data[2]); + var multiBuyId = data[3] != "null" ? Guid.Parse(data[3]) : (Guid?)null; + var variantString = data[4]; + var selectedVariants = variantString.Split(',').ToList(); + + var product = await _shopService.GetProductAsync(productId); + if (product == null) return; + + // Add to cart with selected variants + var cartItem = session.Cart.AddItem(product, quantity, multiBuyId, selectedVariants); + + // Show success message + await bot.AnswerCallbackQueryAsync( + callbackQuery.Id, + $"āœ… Added {quantity}x {product.Name} with {string.Join(", ", selectedVariants)} to cart!", + showAlert: true + ); + + // Return to product view + await HandleProductDetail(bot, callbackQuery.Message!, session, productId); + + // Clear temp data + session.TempData.Remove("selected_variants"); + session.TempData.Remove("current_multibuy"); + } } } \ No newline at end of file diff --git a/TeleBot/TeleBot/Models/ShoppingCart.cs b/TeleBot/TeleBot/Models/ShoppingCart.cs index 1cb4147..425e9c8 100644 --- a/TeleBot/TeleBot/Models/ShoppingCart.cs +++ b/TeleBot/TeleBot/Models/ShoppingCart.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using LittleShop.Client.Models; namespace TeleBot.Models { @@ -11,6 +12,64 @@ namespace TeleBot.Models public DateTime CreatedAt { get; set; } = DateTime.UtcNow; public DateTime UpdatedAt { get; set; } = DateTime.UtcNow; + // New method that accepts Product and variants list + public CartItem AddItem(Product product, int quantity = 1, Guid? multiBuyId = null, List? selectedVariants = null) + { + decimal price = product.Price; + + // If multi-buy selected, get the multi-buy price + if (multiBuyId.HasValue && product.MultiBuys != null) + { + var multiBuy = product.MultiBuys.FirstOrDefault(mb => mb.Id == multiBuyId.Value); + if (multiBuy != null) + { + price = multiBuy.Price; + quantity = multiBuy.Quantity; // Use the multi-buy quantity + } + } + + // Create unique key for comparison + var variantKey = selectedVariants != null && selectedVariants.Any() + ? string.Join(",", selectedVariants.OrderBy(v => v)) + : null; + + // Check if item already exists + var existingItem = Items.FirstOrDefault(i => + i.ProductId == product.Id && + i.MultiBuyId == multiBuyId && + (i.SelectedVariant == variantKey || + (i.SelectedVariants != null && string.Join(",", i.SelectedVariants.OrderBy(v => v)) == variantKey))); + + if (existingItem != null) + { + // For multi-buys, we don't add quantities - each multi-buy is a separate bundle + if (!multiBuyId.HasValue) + { + existingItem.Quantity += quantity; + existingItem.UpdateTotalPrice(); + } + return existingItem; + } + else + { + var newItem = new CartItem + { + ProductId = product.Id, + MultiBuyId = multiBuyId, + SelectedVariant = selectedVariants?.Count == 1 ? selectedVariants[0] : null, + SelectedVariants = selectedVariants ?? new List(), + ProductName = product.Name, + UnitPrice = price, + Quantity = quantity + }; + newItem.UpdateTotalPrice(); + Items.Add(newItem); + UpdatedAt = DateTime.UtcNow; + return newItem; + } + } + + // Keep the old method for backward compatibility public void AddItem(Guid productId, string productName, decimal price, int quantity = 1, Guid? multiBuyId = null, string? selectedVariant = null) { var existingItem = Items.FirstOrDefault(i => @@ -96,7 +155,8 @@ namespace TeleBot.Models { public Guid ProductId { get; set; } public Guid? MultiBuyId { get; set; } // For quantity pricing (e.g., 3 for Ā£25) - public string? SelectedVariant { get; set; } // For color/flavor selection + public string? SelectedVariant { get; set; } // For single items - one variant + public List SelectedVariants { get; set; } = new(); // For multi-buys - multiple variants public string ProductName { get; set; } = string.Empty; public int Quantity { get; set; } public decimal UnitPrice { get; set; } // For multi-buys, this is the bundle price; for regular items, it's per-unit diff --git a/TeleBot/TeleBot/Models/UserSession.cs b/TeleBot/TeleBot/Models/UserSession.cs index 12f75a5..e614de0 100644 --- a/TeleBot/TeleBot/Models/UserSession.cs +++ b/TeleBot/TeleBot/Models/UserSession.cs @@ -92,6 +92,7 @@ namespace TeleBot.Models BrowsingCategories, ViewingProducts, ViewingProduct, + SelectingVariants, ViewingCart, CheckoutFlow, ViewingOrders, diff --git a/TeleBot/TeleBot/Services/ProductCarouselService.cs b/TeleBot/TeleBot/Services/ProductCarouselService.cs index d475a77..29f765c 100644 --- a/TeleBot/TeleBot/Services/ProductCarouselService.cs +++ b/TeleBot/TeleBot/Services/ProductCarouselService.cs @@ -298,15 +298,40 @@ namespace TeleBot.Services { var caption = $"šŸ›ļø *{product.Name}*\n"; caption += $"šŸ’° *${product.Price:F2}*\n"; - + + // Show multi-buy deals if available + if (product.MultiBuys != null && product.MultiBuys.Any(mb => mb.IsActive)) + { + caption += "\nšŸ·ļø *Special Offers:*\n"; + foreach (var multibuy in product.MultiBuys.Where(mb => mb.IsActive).OrderBy(mb => mb.Quantity)) + { + caption += $" • {multibuy.Name}: ${multibuy.Price:F2} (${multibuy.PricePerUnit:F2} each)\n"; + } + } + + // Show available variants + if (product.Variants != null && product.Variants.Any(v => v.IsActive)) + { + var variantTypes = product.Variants + .Where(v => v.IsActive) + .GroupBy(v => v.VariantType) + .ToList(); + + foreach (var group in variantTypes) + { + var variantNames = string.Join(", ", group.OrderBy(v => v.SortOrder).Select(v => v.Name)); + caption += $"\nšŸŽØ *{group.Key}:* {variantNames}"; + } + } + if (!string.IsNullOrEmpty(product.Description)) { - var desc = product.Description.Length > 200 - ? product.Description.Substring(0, 197) + "..." + var desc = product.Description.Length > 200 + ? product.Description.Substring(0, 197) + "..." : product.Description; - caption += $"\n_{desc}_"; + caption += $"\n\n_{desc}_"; } - + if (!string.IsNullOrEmpty(product.CategoryName)) { caption += $"\n\nšŸ“ {product.CategoryName}"; diff --git a/TeleBot/TeleBot/UI/MenuBuilder.cs b/TeleBot/TeleBot/UI/MenuBuilder.cs index 58f9e0d..3891d8e 100644 --- a/TeleBot/TeleBot/UI/MenuBuilder.cs +++ b/TeleBot/TeleBot/UI/MenuBuilder.cs @@ -94,30 +94,42 @@ namespace TeleBot.UI { var buttons = new List(); + // Check if product has variants + bool hasVariants = product.Variants?.Any(v => v.IsActive) == true; + // Show multi-buy options if available - if (product.MultiBuys?.Any() == true) + if (product.MultiBuys?.Any(mb => mb.IsActive) == true) { buttons.Add(new[] { - InlineKeyboardButton.WithCallbackData("šŸ’° Multi-Buy Options:", "noop") + InlineKeyboardButton.WithCallbackData("šŸ’° Multi-Buy Deals:", "noop") }); - foreach (var multiBuy in product.MultiBuys.OrderBy(mb => mb.Quantity)) + foreach (var multiBuy in product.MultiBuys.Where(mb => mb.IsActive).OrderBy(mb => mb.Quantity)) { - var label = $"{multiBuy.Name} - Ā£{multiBuy.Price:F2}"; + var label = $"{multiBuy.Name} - ${multiBuy.Price:F2}"; if (multiBuy.Quantity > 1) - label += $" (Ā£{multiBuy.PricePerUnit:F2}/each)"; + label += $" (${multiBuy.PricePerUnit:F2}/each)"; + + // If has variants, need variant selection first + var callbackData = hasVariants + ? $"selectvar:{product.Id}:{multiBuy.Quantity}:{multiBuy.Id}" + : $"add:{product.Id}:{multiBuy.Quantity}:{multiBuy.Id}"; buttons.Add(new[] { - InlineKeyboardButton.WithCallbackData(label, $"add:{product.Id}:{multiBuy.Quantity}:{multiBuy.Id}") + InlineKeyboardButton.WithCallbackData(label, callbackData) }); } // Add regular single item option + var singleCallbackData = hasVariants + ? $"selectvar:{product.Id}:1" + : $"add:{product.Id}:1"; + buttons.Add(new[] { InlineKeyboardButton.WithCallbackData( - $"šŸ›’ Single Item - Ā£{product.Price:F2}", - $"add:{product.Id}:1" + $"šŸ›’ Single Item - ${product.Price:F2}", + singleCallbackData ) }); } @@ -134,14 +146,38 @@ namespace TeleBot.UI buttons.Add(quantityButtons.ToArray()); // Add to cart button + var addCallbackData = hasVariants + ? $"selectvar:{product.Id}:{quantity}" + : $"add:{product.Id}:{quantity}"; + buttons.Add(new[] { InlineKeyboardButton.WithCallbackData( - $"šŸ›’ Add to Cart - Ā£{product.Price:F2}", - $"add:{product.Id}:{quantity}" + $"šŸ›’ Add to Cart - ${product.Price * quantity:F2}", + addCallbackData ) }); } + // Show variant info if available + if (hasVariants) + { + var variantTypes = product.Variants + .Where(v => v.IsActive) + .GroupBy(v => v.VariantType) + .Select(g => $"{g.Key}: {g.Count()} options") + .ToList(); + + if (variantTypes.Any()) + { + buttons.Add(new[] { + InlineKeyboardButton.WithCallbackData( + $"šŸŽØ Variants: {string.Join(", ", variantTypes)}", + "noop" + ) + }); + } + } + // Navigation buttons.Add(new[] { InlineKeyboardButton.WithCallbackData("ā¬…ļø Back to Products", "browse") }); @@ -294,6 +330,117 @@ namespace TeleBot.UI return new InlineKeyboardMarkup(buttons); } + public static InlineKeyboardMarkup VariantSelectionMenu(Product product, int quantity, string? multiBuyId = null, List? selectedVariants = null) + { + var buttons = new List(); + selectedVariants ??= new List(); + + if (product.Variants?.Any(v => v.IsActive) != true) + { + // No variants, just add to cart + var callbackData = multiBuyId != null + ? $"add:{product.Id}:{quantity}:{multiBuyId}" + : $"add:{product.Id}:{quantity}"; + + buttons.Add(new[] { + InlineKeyboardButton.WithCallbackData($"šŸ›’ Add to Cart", callbackData) + }); + buttons.Add(new[] { InlineKeyboardButton.WithCallbackData("ā¬…ļø Back", $"product:{product.Id}") }); + return new InlineKeyboardMarkup(buttons); + } + + // Group variants by type + var variantGroups = product.Variants + .Where(v => v.IsActive) + .GroupBy(v => v.VariantType) + .ToList(); + + // Show selection status for multi-buy + if (quantity > 1) + { + buttons.Add(new[] { + InlineKeyboardButton.WithCallbackData( + $"šŸ“¦ Selecting for {quantity} items ({selectedVariants.Count} selected)", + "noop" + ) + }); + } + + // For each variant type, show options + foreach (var group in variantGroups) + { + buttons.Add(new[] { + InlineKeyboardButton.WithCallbackData($"Select {group.Key}:", "noop") + }); + + var variantButtons = new List(); + foreach (var variant in group.OrderBy(v => v.SortOrder)) + { + // For multi-buy, allow multiple selections + if (quantity > 1) + { + var count = selectedVariants.Count(v => v == variant.Name); + var buttonText = count > 0 ? $"{variant.Name} ({count})" : variant.Name; + + variantButtons.Add(InlineKeyboardButton.WithCallbackData( + buttonText, + $"addvariant:{product.Id}:{quantity}:{variant.Name}:{multiBuyId ?? "null"}" + )); + } + else + { + // Single item, select one variant + var isSelected = selectedVariants.Contains(variant.Name); + var buttonText = isSelected ? $"āœ… {variant.Name}" : variant.Name; + + variantButtons.Add(InlineKeyboardButton.WithCallbackData( + buttonText, + $"setvariant:{product.Id}:{variant.Name}" + )); + } + } + + // Add variant buttons in rows of 3 + for (int i = 0; i < variantButtons.Count; i += 3) + { + buttons.Add(variantButtons.Skip(i).Take(3).ToArray()); + } + } + + // Add confirm button when selections are complete + bool canConfirm = quantity == 1 ? selectedVariants.Count == 1 : selectedVariants.Count == quantity; + + if (canConfirm) + { + var variantString = string.Join(",", selectedVariants); + var callbackData = multiBuyId != null + ? $"confirmvar:{product.Id}:{quantity}:{multiBuyId}:{variantString}" + : $"confirmvar:{product.Id}:{quantity}:null:{variantString}"; + + buttons.Add(new[] { + InlineKeyboardButton.WithCallbackData( + $"āœ… Confirm & Add to Cart", + callbackData + ) + }); + } + + // Clear selection button for multi-buy + if (quantity > 1 && selectedVariants.Any()) + { + buttons.Add(new[] { + InlineKeyboardButton.WithCallbackData( + "šŸ”„ Clear Selections", + $"selectvar:{product.Id}:{quantity}:{multiBuyId ?? "null"}" + ) + }); + } + + buttons.Add(new[] { InlineKeyboardButton.WithCallbackData("ā¬…ļø Back", $"product:{product.Id}") }); + + return new InlineKeyboardMarkup(buttons); + } + private static string GetCurrencyEmoji(string currency) { return currency.ToUpper() switch