diff --git a/LittleShop/Areas/Admin/Controllers/BotActivityController.cs b/LittleShop/Areas/Admin/Controllers/BotActivityController.cs new file mode 100644 index 0000000..d96fabd --- /dev/null +++ b/LittleShop/Areas/Admin/Controllers/BotActivityController.cs @@ -0,0 +1,142 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using LittleShop.Data; +using LittleShop.Models; + +namespace LittleShop.Areas.Admin.Controllers; + +[Area("Admin")] +[Authorize(Policy = "AdminOnly")] +public class BotActivityController : Controller +{ + private readonly LittleShopContext _context; + + public BotActivityController(LittleShopContext context) + { + _context = context; + } + + public IActionResult LiveView() + { + return View(); + } + + [HttpGet] + public async Task GetRecentActivities(int count = 50) + { + var activities = await _context.BotActivities + .Include(a => a.Bot) + .Include(a => a.Product) + .OrderByDescending(a => a.Timestamp) + .Take(count) + .Select(a => new + { + a.Id, + a.UserDisplayName, + a.ActivityType, + a.ActivityDescription, + a.ProductName, + a.CategoryName, + a.Value, + a.Quantity, + a.Platform, + a.Location, + a.Timestamp, + BotName = a.Bot.Name, + TimeAgo = GetTimeAgo(a.Timestamp) + }) + .ToListAsync(); + + return Json(activities); + } + + [HttpGet] + public async Task GetActiveUsers() + { + var cutoff = DateTime.UtcNow.AddMinutes(-5); // Users active in last 5 minutes + + var activeUsers = await _context.BotActivities + .Where(a => a.Timestamp >= cutoff) + .GroupBy(a => new { a.SessionIdentifier, a.UserDisplayName }) + .Select(g => new + { + SessionId = g.Key.SessionIdentifier, + UserName = g.Key.UserDisplayName, + ActivityCount = g.Count(), + LastActivity = g.Max(a => a.Timestamp), + LastAction = g.OrderByDescending(a => a.Timestamp).FirstOrDefault()!.ActivityDescription, + TotalValue = g.Sum(a => a.Value ?? 0) + }) + .OrderByDescending(u => u.LastActivity) + .ToListAsync(); + + return Json(activeUsers); + } + + [HttpGet] + public async Task GetStatistics() + { + var today = DateTime.UtcNow.Date; + var yesterday = today.AddDays(-1); + + var stats = new + { + TodayActivities = await _context.BotActivities + .Where(a => a.Timestamp >= today) + .CountAsync(), + + YesterdayActivities = await _context.BotActivities + .Where(a => a.Timestamp >= yesterday && a.Timestamp < today) + .CountAsync(), + + UniqueUsersToday = await _context.BotActivities + .Where(a => a.Timestamp >= today) + .Select(a => a.SessionIdentifier) + .Distinct() + .CountAsync(), + + PopularProducts = await _context.BotActivities + .Where(a => a.ProductId != null && a.Timestamp >= today.AddDays(-7)) + .GroupBy(a => new { a.ProductId, a.ProductName }) + .Select(g => new + { + ProductName = g.Key.ProductName, + ViewCount = g.Count(a => a.ActivityType == "ViewProduct"), + AddToCartCount = g.Count(a => a.ActivityType == "AddToCart") + }) + .OrderByDescending(p => p.ViewCount + p.AddToCartCount) + .Take(5) + .ToListAsync(), + + ActivityByHour = await _context.BotActivities + .Where(a => a.Timestamp >= today) + .GroupBy(a => a.Timestamp.Hour) + .Select(g => new + { + Hour = g.Key, + Count = g.Count() + }) + .OrderBy(h => h.Hour) + .ToListAsync() + }; + + return Json(stats); + } + + private static string GetTimeAgo(DateTime timestamp) + { + var span = DateTime.UtcNow - timestamp; + + if (span.TotalSeconds < 60) + return "just now"; + if (span.TotalMinutes < 60) + return $"{(int)span.TotalMinutes}m ago"; + if (span.TotalHours < 24) + return $"{(int)span.TotalHours}h ago"; + if (span.TotalDays < 7) + return $"{(int)span.TotalDays}d ago"; + + return timestamp.ToString("MMM dd"); + } +} \ No newline at end of file diff --git a/LittleShop/Areas/Admin/Controllers/ProductsController.cs b/LittleShop/Areas/Admin/Controllers/ProductsController.cs index 64798f5..ce8c892 100644 --- a/LittleShop/Areas/Admin/Controllers/ProductsController.cs +++ b/LittleShop/Areas/Admin/Controllers/ProductsController.cs @@ -298,6 +298,93 @@ public class ProductsController : Controller return RedirectToAction(nameof(Variations), new { id = variation.ProductId }); } + // Product Variants (Colors/Flavors) + public async Task Variants(Guid id) + { + var product = await _productService.GetProductByIdAsync(id); + if (product == null) + return NotFound(); + + ViewData["Product"] = product; + var variants = await _productService.GetProductVariantsAsync(id); + return View("ProductVariants", variants); + } + + public async Task CreateVariant(Guid productId) + { + var product = await _productService.GetProductByIdAsync(productId); + if (product == null) + return NotFound(); + + ViewData["Product"] = product; + return View("CreateVariant", new CreateProductVariantDto { ProductId = productId }); + } + + [HttpPost] + [ValidateAntiForgeryToken] + public async Task CreateVariant(CreateProductVariantDto model) + { + if (!ModelState.IsValid) + { + var product = await _productService.GetProductByIdAsync(model.ProductId); + ViewData["Product"] = product; + return View("CreateVariant", model); + } + + await _productService.CreateProductVariantAsync(model); + return RedirectToAction(nameof(Variants), new { id = model.ProductId }); + } + + public async Task EditVariant(Guid id) + { + var variant = await _productService.GetProductVariantByIdAsync(id); + if (variant == null) + return NotFound(); + + var product = await _productService.GetProductByIdAsync(variant.ProductId); + ViewData["Product"] = product; + + var model = new UpdateProductVariantDto + { + Name = variant.Name, + VariantType = variant.VariantType, + StockLevel = variant.StockLevel, + SortOrder = variant.SortOrder, + IsActive = variant.IsActive + }; + + return View("EditVariant", model); + } + + [HttpPost] + [ValidateAntiForgeryToken] + public async Task EditVariant(Guid id, UpdateProductVariantDto model) + { + if (!ModelState.IsValid) + { + var variant = await _productService.GetProductVariantByIdAsync(id); + var product = await _productService.GetProductByIdAsync(variant!.ProductId); + ViewData["Product"] = product; + return View("EditVariant", model); + } + + await _productService.UpdateProductVariantAsync(id, model); + var variantToRedirect = await _productService.GetProductVariantByIdAsync(id); + return RedirectToAction(nameof(Variants), new { id = variantToRedirect!.ProductId }); + } + + [HttpPost] + [ValidateAntiForgeryToken] + public async Task DeleteVariant(Guid id) + { + var variant = await _productService.GetProductVariantByIdAsync(id); + if (variant == null) + return NotFound(); + + await _productService.DeleteProductVariantAsync(id); + return RedirectToAction(nameof(Variants), new { id = variant.ProductId }); + } + // Product Import/Export public IActionResult Import() { diff --git a/LittleShop/Areas/Admin/Views/BotActivity/LiveView.cshtml b/LittleShop/Areas/Admin/Views/BotActivity/LiveView.cshtml new file mode 100644 index 0000000..1b134b6 --- /dev/null +++ b/LittleShop/Areas/Admin/Views/BotActivity/LiveView.cshtml @@ -0,0 +1,362 @@ +@{ + ViewData["Title"] = "Live Bot Activity"; + Layout = "_Layout"; +} + + + +
+
+
+

Live Bot Activity

+

Real-time view of customer interactions across all bots

+
+
+
+ LIVE + Updates every 5 seconds +
+
+
+ + +
+
+
+
+
+
+
Active Users
+

0

+
+
+ +
+
+
+
+
+
+
+
+
+
+
Today's Activities
+

0

+
+
+ +
+
+
+
+
+
+
+
+
+
+
Unique Visitors
+

0

+
+
+ +
+
+
+
+
+
+
+
+
+
+
Cart Actions
+

0

+
+
+ +
+
+
+
+
+
+ +
+ +
+
+
+ Active Users (Last 5 min) +
+
+
+ +
+
+
+ + +
+
+ Trending Products (7 days) +
+
+
+ +
+
+
+
+ + +
+
+
+ Live Activity Feed +
+
+ +
+
+
+
+
+ +@section Scripts { + +} \ No newline at end of file diff --git a/LittleShop/Areas/Admin/Views/Products/CreateVariant.cshtml b/LittleShop/Areas/Admin/Views/Products/CreateVariant.cshtml new file mode 100644 index 0000000..c3f6a40 --- /dev/null +++ b/LittleShop/Areas/Admin/Views/Products/CreateVariant.cshtml @@ -0,0 +1,112 @@ +@model LittleShop.DTOs.CreateProductVariantDto + +@{ + ViewData["Title"] = "Create Variant"; + var product = ViewData["Product"] as LittleShop.DTOs.ProductDto; +} + +
+
+ +

Add Product Variant

+

Add a new color, flavor, or other option for @product?.Name

+
+
+ +
+
+
+
+
+ @Html.AntiForgeryToken() + + +
+ + + + The specific variant option (color, flavor, size, etc.) +
+ +
+ + + + Categorize this variant for better organization +
+ +
+ + + + Track inventory for this specific variant (optional) +
+ +
+ + + + Controls the display order of variants +
+ +
+
+ + +
+ Active variants are available for selection +
+ +
+ + + Cancel + +
+
+
+
+
+ +
+
+
+
About Variants
+

+ Variants allow customers to choose specific options for a product, such as: +

+
    +
  • Colors: Red, Blue, Green, Black, White
  • +
  • Flavors: Vanilla, Chocolate, Strawberry
  • +
  • Sizes: Small, Medium, Large, XL
  • +
  • Materials: Cotton, Polyester, Leather
  • +
+

+ When customers order this product (single item or multi-buy), they'll be able to select from the available variants. +

+
+ Multi-Buy Variants: For multi-buy deals (e.g., 3 for £25), customers can choose different variants for each item in the bundle. +
+
+
+
+
+ +@section Scripts { + +} \ No newline at end of file diff --git a/LittleShop/Areas/Admin/Views/Products/EditVariant.cshtml b/LittleShop/Areas/Admin/Views/Products/EditVariant.cshtml new file mode 100644 index 0000000..df21c4a --- /dev/null +++ b/LittleShop/Areas/Admin/Views/Products/EditVariant.cshtml @@ -0,0 +1,83 @@ +@model LittleShop.DTOs.UpdateProductVariantDto + +@{ + ViewData["Title"] = "Edit Variant"; + var product = ViewData["Product"] as LittleShop.DTOs.ProductDto; +} + +
+
+ +

Edit Product Variant

+

Update variant details for @product?.Name

+
+
+ +
+
+
+
+
+ @Html.AntiForgeryToken() + +
+ + + +
+ +
+ + + +
+ +
+ + + +
+ +
+ + + +
+ +
+
+ + +
+
+ +
+ + + Cancel + +
+
+
+
+
+
+ +@section Scripts { + +} \ No newline at end of file diff --git a/LittleShop/Areas/Admin/Views/Products/Index.cshtml b/LittleShop/Areas/Admin/Views/Products/Index.cshtml index 16ca46f..70233ee 100644 --- a/LittleShop/Areas/Admin/Views/Products/Index.cshtml +++ b/LittleShop/Areas/Admin/Views/Products/Index.cshtml @@ -110,8 +110,11 @@ - - + + + + +
diff --git a/LittleShop/Areas/Admin/Views/Products/ProductVariants.cshtml b/LittleShop/Areas/Admin/Views/Products/ProductVariants.cshtml new file mode 100644 index 0000000..f780ea7 --- /dev/null +++ b/LittleShop/Areas/Admin/Views/Products/ProductVariants.cshtml @@ -0,0 +1,107 @@ +@model IEnumerable + +@{ + ViewData["Title"] = "Product Variants"; + var product = ViewData["Product"] as LittleShop.DTOs.ProductDto; +} + +
+
+ +

Product Variants

+

Manage color, flavor, or other options for @product?.Name

+
+ +
+ +
+
+ @if (Model.Any()) + { +
+ + + + + + + + + + + + + @foreach (var variant in Model.OrderBy(v => v.SortOrder).ThenBy(v => v.Name)) + { + + + + + + + + + } + +
NameTypeStock LevelSort OrderStatusActions
+ @variant.Name + + @variant.VariantType + + @if (variant.StockLevel > 0) + { + @variant.StockLevel in stock + } + else + { + Out of stock + } + @variant.SortOrder + @if (variant.IsActive) + { + Active + } + else + { + Inactive + } + +
+ + Edit + + + @Html.AntiForgeryToken() + + +
+
+
+ } + else + { +
+ No variants have been added for this product yet. + Add your first variant +
+ } +
+
+ + \ No newline at end of file diff --git a/LittleShop/Areas/Admin/Views/Products/Variations.cshtml b/LittleShop/Areas/Admin/Views/Products/Variations.cshtml index 80675e8..04f4b02 100644 --- a/LittleShop/Areas/Admin/Views/Products/Variations.cshtml +++ b/LittleShop/Areas/Admin/Views/Products/Variations.cshtml @@ -1,7 +1,7 @@ @model IEnumerable @{ - ViewData["Title"] = "Product Variations"; + ViewData["Title"] = "Product Multi-Buys"; var product = ViewData["Product"] as LittleShop.DTOs.ProductDto; } @@ -10,15 +10,15 @@ -

Product Variations

-

Manage quantity-based pricing for @product?.Name

+

Product Multi-Buys

+

Manage quantity-based pricing deals for @product?.Name

diff --git a/LittleShop/Areas/Admin/Views/Shared/_Layout.cshtml b/LittleShop/Areas/Admin/Views/Shared/_Layout.cshtml index 9801317..d4b68a5 100644 --- a/LittleShop/Areas/Admin/Views/Shared/_Layout.cshtml +++ b/LittleShop/Areas/Admin/Views/Shared/_Layout.cshtml @@ -92,6 +92,11 @@ Bots +