Major restructuring of product variations: - Renamed ProductVariation to ProductMultiBuy for quantity-based pricing (e.g., "3 for £25") - Added new ProductVariant model for string-based options (colors, flavors) - Complete separation of multi-buy pricing from variant selection Features implemented: - Multi-buy deals with automatic price-per-unit calculation - Product variants for colors/flavors/sizes with stock tracking - TeleBot checkout supports both multi-buys and variant selection - Shopping cart correctly calculates multi-buy bundle prices - Order system tracks selected variants and multi-buy choices - Real-time bot activity monitoring with SignalR - Public bot directory page with QR codes for Telegram launch - Admin dashboard shows multi-buy and variant metrics Technical changes: - Updated all DTOs, services, and controllers - Fixed cart total calculation for multi-buy bundles - Comprehensive test coverage for new functionality - All existing tests passing with new features Database changes: - Migrated ProductVariations to ProductMultiBuys - Added ProductVariants table - Updated OrderItems to track variants 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
152 lines
4.5 KiB
C#
152 lines
4.5 KiB
C#
using Microsoft.AspNetCore.Mvc;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using QRCoder;
|
|
using LittleShop.Data;
|
|
using LittleShop.DTOs;
|
|
using LittleShop.Enums;
|
|
|
|
namespace LittleShop.Controllers;
|
|
|
|
public class BotDirectoryController : Controller
|
|
{
|
|
private readonly LittleShopContext _context;
|
|
private readonly ILogger<BotDirectoryController> _logger;
|
|
|
|
public BotDirectoryController(LittleShopContext context, ILogger<BotDirectoryController> logger)
|
|
{
|
|
_context = context;
|
|
_logger = logger;
|
|
}
|
|
|
|
// GET: /bots
|
|
[HttpGet("/bots")]
|
|
public async Task<IActionResult> Index()
|
|
{
|
|
var bots = await _context.Bots
|
|
.Where(b => b.Status == BotStatus.Active && b.IsActive)
|
|
.OrderBy(b => b.Name)
|
|
.Select(b => new BotDirectoryDto
|
|
{
|
|
Id = b.Id,
|
|
Name = b.Name,
|
|
Description = b.Description,
|
|
Type = b.Type.ToString(),
|
|
PersonalityName = b.PersonalityName,
|
|
TelegramUsername = b.Settings.Contains("\"telegram_username\":")
|
|
? ExtractTelegramUsername(b.Settings)
|
|
: b.PlatformUsername,
|
|
LastSeenAt = b.LastSeenAt,
|
|
CreatedAt = b.CreatedAt
|
|
})
|
|
.ToListAsync();
|
|
|
|
return View(bots);
|
|
}
|
|
|
|
// GET: /bots/qr/{id}
|
|
[HttpGet("/bots/qr/{id}")]
|
|
public async Task<IActionResult> GetQRCode(Guid id)
|
|
{
|
|
var bot = await _context.Bots
|
|
.Where(b => b.Id == id && b.Status == BotStatus.Active)
|
|
.FirstOrDefaultAsync();
|
|
|
|
if (bot == null)
|
|
{
|
|
return NotFound();
|
|
}
|
|
|
|
// Extract Telegram username from settings JSON or use platform username
|
|
string? telegramUsername = ExtractTelegramUsername(bot.Settings) ?? bot.PlatformUsername;
|
|
|
|
if (string.IsNullOrEmpty(telegramUsername))
|
|
{
|
|
return NotFound("Bot does not have a Telegram username configured");
|
|
}
|
|
|
|
// Generate Telegram deep link
|
|
string telegramUrl = $"https://t.me/{telegramUsername}";
|
|
|
|
// Generate QR code
|
|
using (QRCodeGenerator qrGenerator = new QRCodeGenerator())
|
|
{
|
|
QRCodeData qrCodeData = qrGenerator.CreateQrCode(telegramUrl, QRCodeGenerator.ECCLevel.Q);
|
|
using (PngByteQRCode qrCode = new PngByteQRCode(qrCodeData))
|
|
{
|
|
byte[] qrCodeImage = qrCode.GetGraphic(20);
|
|
return File(qrCodeImage, "image/png");
|
|
}
|
|
}
|
|
}
|
|
|
|
private static string? ExtractTelegramUsername(string settings)
|
|
{
|
|
try
|
|
{
|
|
var json = System.Text.Json.JsonDocument.Parse(settings);
|
|
if (json.RootElement.TryGetProperty("telegram_username", out var username))
|
|
{
|
|
return username.GetString();
|
|
}
|
|
}
|
|
catch
|
|
{
|
|
// JSON parsing failed
|
|
}
|
|
return null;
|
|
}
|
|
}
|
|
|
|
public class BotDirectoryDto
|
|
{
|
|
public Guid Id { get; set; }
|
|
public string Name { get; set; } = string.Empty;
|
|
public string Description { get; set; } = string.Empty;
|
|
public string Type { get; set; } = string.Empty;
|
|
public string PersonalityName { get; set; } = string.Empty;
|
|
public string? TelegramUsername { get; set; }
|
|
public DateTime? LastSeenAt { get; set; }
|
|
public DateTime CreatedAt { get; set; }
|
|
|
|
public string GetBadgeColor()
|
|
{
|
|
return Type.ToLower() switch
|
|
{
|
|
"sales" => "primary",
|
|
"support" => "success",
|
|
"marketing" => "warning",
|
|
"technical" => "info",
|
|
_ => "secondary"
|
|
};
|
|
}
|
|
|
|
public string GetStatusBadge()
|
|
{
|
|
if (!LastSeenAt.HasValue)
|
|
return "Never Connected";
|
|
|
|
var timeSinceLastSeen = DateTime.UtcNow - LastSeenAt.Value;
|
|
if (timeSinceLastSeen.TotalMinutes < 5)
|
|
return "Online";
|
|
else if (timeSinceLastSeen.TotalHours < 1)
|
|
return "Recently Active";
|
|
else if (timeSinceLastSeen.TotalDays < 1)
|
|
return "Active Today";
|
|
else
|
|
return $"Last seen {timeSinceLastSeen.Days} days ago";
|
|
}
|
|
|
|
public string GetStatusColor()
|
|
{
|
|
if (!LastSeenAt.HasValue)
|
|
return "secondary";
|
|
|
|
var timeSinceLastSeen = DateTime.UtcNow - LastSeenAt.Value;
|
|
if (timeSinceLastSeen.TotalMinutes < 5)
|
|
return "success";
|
|
else if (timeSinceLastSeen.TotalHours < 1)
|
|
return "info";
|
|
else
|
|
return "secondary";
|
|
}
|
|
} |