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:
@@ -0,0 +1,54 @@
|
||||
@model LittleShop.DTOs.CreateVariantCollectionDto
|
||||
|
||||
@{
|
||||
ViewData["Title"] = "Create Variant Collection";
|
||||
}
|
||||
|
||||
<div class="row mb-4">
|
||||
<div class="col">
|
||||
<h1><i class="fas fa-plus"></i> Create Variant Collection</h1>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<a href="@Url.Action("Index")" class="btn btn-secondary">
|
||||
<i class="fas fa-arrow-left"></i> Back to Collections
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<form method="post">
|
||||
@Html.AntiForgeryToken()
|
||||
|
||||
<div asp-validation-summary="All" class="alert alert-danger" role="alert"></div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="Name" class="form-label">Collection Name *</label>
|
||||
<input type="text" class="form-control" id="Name" name="Name" value="@Model.Name" required maxlength="100" placeholder="e.g., Mens Clothes, Jewelry Sizes">
|
||||
<span asp-validation-for="Name" class="text-danger"></span>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="PropertiesJson" class="form-label">Properties (JSON)</label>
|
||||
<textarea class="form-control font-monospace" id="PropertiesJson" name="PropertiesJson" rows="10" placeholder='[{"name":"Size","values":["S","M","L","XL"]},{"name":"Colour","values":null}]'>@Model.PropertiesJson</textarea>
|
||||
<div class="form-text">
|
||||
Define properties as JSON array. Each property has a "name" and optional "values" array.
|
||||
<br>If "values" is null, users can enter freeform text. If "values" is an array, users select from dropdown.
|
||||
<br><strong>Example:</strong> <code>[{"name":"Size","values":["S","M","L"]},{"name":"Colour","values":null}]</code>
|
||||
</div>
|
||||
<span asp-validation-for="PropertiesJson" class="text-danger"></span>
|
||||
</div>
|
||||
|
||||
<div class="border-top pt-3">
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="fas fa-save"></i> Create Collection
|
||||
</button>
|
||||
<a href="@Url.Action("Index")" class="btn btn-secondary">Cancel</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@section Scripts {
|
||||
@{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
|
||||
}
|
||||
65
LittleShop/Areas/Admin/Views/VariantCollections/Edit.cshtml
Normal file
65
LittleShop/Areas/Admin/Views/VariantCollections/Edit.cshtml
Normal file
@@ -0,0 +1,65 @@
|
||||
@model LittleShop.DTOs.UpdateVariantCollectionDto
|
||||
|
||||
@{
|
||||
ViewData["Title"] = "Edit Variant Collection";
|
||||
var collectionId = ViewData["CollectionId"];
|
||||
}
|
||||
|
||||
<div class="row mb-4">
|
||||
<div class="col">
|
||||
<h1><i class="fas fa-edit"></i> Edit Variant Collection</h1>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<a href="@Url.Action("Index")" class="btn btn-secondary">
|
||||
<i class="fas fa-arrow-left"></i> Back to Collections
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<form method="post" action="@Url.Action("Edit", new { id = collectionId })">
|
||||
@Html.AntiForgeryToken()
|
||||
|
||||
<div asp-validation-summary="All" class="alert alert-danger" role="alert"></div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="Name" class="form-label">Collection Name</label>
|
||||
<input type="text" class="form-control" id="Name" name="Name" value="@Model.Name" maxlength="100" placeholder="e.g., Mens Clothes, Jewelry Sizes">
|
||||
<span asp-validation-for="Name" class="text-danger"></span>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="PropertiesJson" class="form-label">Properties (JSON)</label>
|
||||
<textarea class="form-control font-monospace" id="PropertiesJson" name="PropertiesJson" rows="10" placeholder='[{"name":"Size","values":["S","M","L","XL"]},{"name":"Colour","values":null}]'>@Model.PropertiesJson</textarea>
|
||||
<div class="form-text">
|
||||
Define properties as JSON array. Each property has a "name" and optional "values" array.
|
||||
<br>If "values" is null, users can enter freeform text. If "values" is an array, users select from dropdown.
|
||||
<br><strong>Example:</strong> <code>[{"name":"Size","values":["S","M","L"]},{"name":"Colour","values":null}]</code>
|
||||
</div>
|
||||
<span asp-validation-for="PropertiesJson" class="text-danger"></span>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<div class="form-check">
|
||||
<input type="checkbox" class="form-check-input" id="IsActive" name="IsActive" value="true" @(Model.IsActive == true ? "checked" : "")>
|
||||
<label class="form-check-label" for="IsActive">
|
||||
Active
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-text">Inactive collections cannot be selected when editing products</div>
|
||||
</div>
|
||||
|
||||
<div class="border-top pt-3">
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="fas fa-save"></i> Save Changes
|
||||
</button>
|
||||
<a href="@Url.Action("Index")" class="btn btn-secondary">Cancel</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@section Scripts {
|
||||
@{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
|
||||
}
|
||||
91
LittleShop/Areas/Admin/Views/VariantCollections/Index.cshtml
Normal file
91
LittleShop/Areas/Admin/Views/VariantCollections/Index.cshtml
Normal file
@@ -0,0 +1,91 @@
|
||||
@model IEnumerable<LittleShop.DTOs.VariantCollectionDto>
|
||||
|
||||
@{
|
||||
ViewData["Title"] = "Variant Collections";
|
||||
}
|
||||
|
||||
<div class="row mb-4">
|
||||
<div class="col">
|
||||
<h1><i class="fas fa-layer-group"></i> Variant Collections</h1>
|
||||
<p class="text-muted">Manage reusable variant templates for products</p>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<a href="@Url.Action("Create")" class="btn btn-primary">
|
||||
<i class="fas fa-plus"></i> Add Collection
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
@if (Model.Any())
|
||||
{
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Properties</th>
|
||||
<th>Created</th>
|
||||
<th>Updated</th>
|
||||
<th>Status</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var collection in Model)
|
||||
{
|
||||
<tr>
|
||||
<td><strong>@collection.Name</strong></td>
|
||||
<td>
|
||||
@if (collection.PropertiesJson != "[]" && !string.IsNullOrWhiteSpace(collection.PropertiesJson))
|
||||
{
|
||||
<code class="small">@collection.PropertiesJson.Substring(0, Math.Min(50, collection.PropertiesJson.Length))@(collection.PropertiesJson.Length > 50 ? "..." : "")</code>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="text-muted">No properties</span>
|
||||
}
|
||||
</td>
|
||||
<td>@collection.CreatedAt.ToString("MMM dd, yyyy")</td>
|
||||
<td>@collection.UpdatedAt.ToString("MMM dd, yyyy")</td>
|
||||
<td>
|
||||
@if (collection.IsActive)
|
||||
{
|
||||
<span class="badge bg-success">Active</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="badge bg-danger">Inactive</span>
|
||||
}
|
||||
</td>
|
||||
<td>
|
||||
<div class="btn-group btn-group-sm">
|
||||
<a href="@Url.Action("Edit", new { id = collection.Id })" class="btn btn-outline-primary">
|
||||
<i class="fas fa-edit"></i>
|
||||
</a>
|
||||
<form method="post" action="@Url.Action("Delete", new { id = collection.Id })" class="d-inline"
|
||||
onsubmit="return confirm('Are you sure you want to deactivate this collection?')">
|
||||
@Html.AntiForgeryToken()
|
||||
<button type="submit" class="btn btn-outline-danger">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="text-center py-4">
|
||||
<i class="fas fa-layer-group fa-3x text-muted mb-3"></i>
|
||||
<p class="text-muted">No variant collections found. <a href="@Url.Action("Create")">Create your first collection</a>.</p>
|
||||
<p class="small text-muted">Variant collections define reusable property templates (e.g., "Mens Clothes" with Size and Colour properties)</p>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
Reference in New Issue
Block a user