Add variant collections system and enhance ProductVariant with weight/stock tracking

This commit introduces a comprehensive variant management system and enhances
the existing ProductVariant model with per-variant weight overrides and stock
tracking, integrated across Admin Panel and TeleBot.

Features Added:
- Variant Collections: Reusable variant templates (e.g., "Standard Sizes")
- Admin UI for managing variant collections (CRUD operations)
- Dynamic variant editor with JavaScript-based UI
- Per-variant weight and weight unit overrides
- Per-variant stock level tracking
- SalesLedger model for financial tracking

ProductVariant Enhancements:
- Added Weight (decimal, nullable) field for variant-specific weights
- Added WeightUnit (enum, nullable) field for variant-specific units
- Maintains backward compatibility with product-level weights

TeleBot Integration:
- Enhanced variant selection UI to display stock levels
- Shows weight information with proper unit conversion (µg, g, oz, lb, ml, L)
- Compact button format: "Medium (15 in stock, 350g)"
- Real-time stock availability display

Database Migrations:
- 20250928014850_AddVariantCollectionsAndSalesLedger
- 20250928155814_AddWeightToProductVariants

Technical Changes:
- Updated Product model to support VariantCollectionId and VariantsJson
- Extended ProductService with variant collection operations
- Enhanced OrderService to handle variant-specific pricing and weights
- Updated LittleShop.Client DTOs to match server models
- Added JavaScript dynamic variant form builder

Files Modified: 15
Files Added: 17
Lines Changed: ~2000

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
sysadmin
2025-09-28 17:03:09 +01:00
parent 191a9f27f2
commit eb87148c63
32 changed files with 5884 additions and 102 deletions

View File

@@ -862,8 +862,8 @@ namespace LittleShop.Migrations
b.Property<int>("Quantity")
.HasColumnType("INTEGER");
b.Property<string>("SelectedVariant")
.HasMaxLength(100)
b.Property<string>("SelectedVariants")
.HasMaxLength(500)
.HasColumnType("TEXT");
b.Property<decimal>("TotalPrice")
@@ -916,6 +916,12 @@ namespace LittleShop.Migrations
b.Property<DateTime>("UpdatedAt")
.HasColumnType("TEXT");
b.Property<Guid?>("VariantCollectionId")
.HasColumnType("TEXT");
b.Property<string>("VariantsJson")
.HasColumnType("TEXT");
b.Property<decimal>("Weight")
.HasColumnType("decimal(18,4)");
@@ -926,6 +932,8 @@ namespace LittleShop.Migrations
b.HasIndex("CategoryId");
b.HasIndex("VariantCollectionId");
b.ToTable("Products");
});
@@ -1050,6 +1058,12 @@ namespace LittleShop.Migrations
.HasMaxLength(50)
.HasColumnType("TEXT");
b.Property<decimal?>("Weight")
.HasColumnType("TEXT");
b.Property<int?>("WeightUnit")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("IsActive");
@@ -1190,6 +1204,57 @@ namespace LittleShop.Migrations
b.ToTable("Reviews");
});
modelBuilder.Entity("LittleShop.Models.SalesLedger", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<string>("Cryptocurrency")
.HasMaxLength(50)
.HasColumnType("TEXT");
b.Property<string>("FiatCurrency")
.IsRequired()
.HasMaxLength(3)
.HasColumnType("TEXT");
b.Property<Guid>("OrderId")
.HasColumnType("TEXT");
b.Property<Guid>("ProductId")
.HasColumnType("TEXT");
b.Property<string>("ProductName")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("TEXT");
b.Property<int>("Quantity")
.HasColumnType("INTEGER");
b.Property<decimal?>("SalePriceBTC")
.HasColumnType("decimal(18,8)");
b.Property<decimal>("SalePriceFiat")
.HasColumnType("decimal(18,2)");
b.Property<DateTime>("SoldAt")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("OrderId");
b.HasIndex("ProductId");
b.HasIndex("SoldAt");
b.HasIndex("ProductId", "SoldAt");
b.ToTable("SalesLedgers");
});
modelBuilder.Entity("LittleShop.Models.ShippingRate", b =>
{
b.Property<Guid>("Id")
@@ -1303,6 +1368,39 @@ namespace LittleShop.Migrations
b.ToTable("Users");
});
modelBuilder.Entity("LittleShop.Models.VariantCollection", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT");
b.Property<bool>("IsActive")
.HasColumnType("INTEGER");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("TEXT");
b.Property<string>("PropertiesJson")
.IsRequired()
.HasColumnType("TEXT");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("IsActive");
b.HasIndex("Name");
b.ToTable("VariantCollections");
});
modelBuilder.Entity("LittleShop.Models.BotActivity", b =>
{
b.HasOne("LittleShop.Models.Bot", "Bot")
@@ -1454,7 +1552,13 @@ namespace LittleShop.Migrations
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.HasOne("LittleShop.Models.VariantCollection", "VariantCollection")
.WithMany()
.HasForeignKey("VariantCollectionId");
b.Navigation("Category");
b.Navigation("VariantCollection");
});
modelBuilder.Entity("LittleShop.Models.ProductMultiBuy", b =>
@@ -1541,6 +1645,25 @@ namespace LittleShop.Migrations
b.Navigation("Product");
});
modelBuilder.Entity("LittleShop.Models.SalesLedger", b =>
{
b.HasOne("LittleShop.Models.Order", "Order")
.WithMany()
.HasForeignKey("OrderId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.HasOne("LittleShop.Models.Product", "Product")
.WithMany("SalesLedgers")
.HasForeignKey("ProductId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.Navigation("Order");
b.Navigation("Product");
});
modelBuilder.Entity("LittleShop.Models.Bot", b =>
{
b.Navigation("Metrics");
@@ -1584,6 +1707,8 @@ namespace LittleShop.Migrations
b.Navigation("Reviews");
b.Navigation("SalesLedgers");
b.Navigation("Variants");
});