Add: CSV import with Replace All feature and auto-create categories
- Added Replace All checkbox to import UI for clean slate imports - Implemented DeleteAllProductsAndCategoriesAsync for complete data wipe - Added auto-creation of categories during CSV import - Created products_import.csv with 13 products across 4 categories - Added comprehensive IMPORT_INSTRUCTIONS.md documentation Technical changes: - ProductImportService: Added replaceAll parameter to all import methods - ProductImportService: Categories now auto-created if missing from CSV - ProductsController: Added replaceAll parameter to Import action - Import.cshtml: Added Replace All checkbox with danger warnings Categories: Flour, Cereal, Vitamins, Herbal Products: 13 products with full variant pricing structures 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -10,9 +10,10 @@ namespace LittleShop.Services;
|
||||
|
||||
public interface IProductImportService
|
||||
{
|
||||
Task<ProductImportResultDto> ImportFromCsvAsync(Stream csvStream);
|
||||
Task<ProductImportResultDto> ImportFromTextAsync(string csvText);
|
||||
Task<ProductImportResultDto> ImportFromHumanTextAsync(string textContent);
|
||||
Task<ProductImportResultDto> ImportFromCsvAsync(Stream csvStream, bool replaceAll = false);
|
||||
Task<ProductImportResultDto> ImportFromTextAsync(string csvText, bool replaceAll = false);
|
||||
Task<ProductImportResultDto> ImportFromHumanTextAsync(string textContent, bool replaceAll = false);
|
||||
Task<int> DeleteAllProductsAndCategoriesAsync();
|
||||
string GenerateTemplateAsCsv();
|
||||
string GenerateTemplateAsHumanText();
|
||||
Task<string> ExportProductsAsCsvAsync();
|
||||
@@ -37,16 +38,25 @@ public class ProductImportService : IProductImportService
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<ProductImportResultDto> ImportFromCsvAsync(Stream csvStream)
|
||||
public async Task<ProductImportResultDto> ImportFromCsvAsync(Stream csvStream, bool replaceAll = false)
|
||||
{
|
||||
using var reader = new StreamReader(csvStream);
|
||||
var csvText = await reader.ReadToEndAsync();
|
||||
return await ImportFromTextAsync(csvText);
|
||||
return await ImportFromTextAsync(csvText, replaceAll);
|
||||
}
|
||||
|
||||
public async Task<ProductImportResultDto> ImportFromTextAsync(string csvText)
|
||||
public async Task<ProductImportResultDto> ImportFromTextAsync(string csvText, bool replaceAll = false)
|
||||
{
|
||||
var result = new ProductImportResultDto();
|
||||
|
||||
// Replace all existing data if requested
|
||||
if (replaceAll)
|
||||
{
|
||||
_logger.LogWarning("REPLACE ALL: Deleting all existing products, variants, and categories");
|
||||
var deletedCount = await DeleteAllProductsAndCategoriesAsync();
|
||||
_logger.LogInformation("Deleted {Count} existing records", deletedCount);
|
||||
}
|
||||
|
||||
var lines = csvText.Split('\n', StringSplitOptions.RemoveEmptyEntries);
|
||||
|
||||
if (lines.Length == 0)
|
||||
@@ -83,10 +93,41 @@ public class ProductImportService : IProductImportService
|
||||
|
||||
result.TotalRows = lines.Length - 1; // Exclude header
|
||||
|
||||
// Get categories for lookup
|
||||
// Get categories for lookup (will be populated as we create new ones)
|
||||
var categories = await _categoryService.GetAllCategoriesAsync();
|
||||
var categoryLookup = categories.ToDictionary(c => c.Name, c => c.Id, StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
// Auto-create categories from CSV if they don't exist
|
||||
var uniqueCategoryNames = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
for (int i = 1; i < lines.Length; i++)
|
||||
{
|
||||
var values = ParseCsvLine(lines[i]);
|
||||
if (values.Length >= 7)
|
||||
{
|
||||
var categoryName = GetValue(values, headers, "CategoryName", "");
|
||||
if (!string.IsNullOrEmpty(categoryName))
|
||||
{
|
||||
uniqueCategoryNames.Add(categoryName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create missing categories
|
||||
foreach (var categoryName in uniqueCategoryNames)
|
||||
{
|
||||
if (!categoryLookup.ContainsKey(categoryName))
|
||||
{
|
||||
var createCategoryDto = new CreateCategoryDto
|
||||
{
|
||||
Name = categoryName,
|
||||
Description = $"Auto-created category for {categoryName} products"
|
||||
};
|
||||
var newCategory = await _categoryService.CreateCategoryAsync(createCategoryDto);
|
||||
categoryLookup[categoryName] = newCategory.Id;
|
||||
_logger.LogInformation("Auto-created category: {CategoryName}", categoryName);
|
||||
}
|
||||
}
|
||||
|
||||
// Process data rows
|
||||
for (int i = 1; i < lines.Length; i++)
|
||||
{
|
||||
@@ -332,10 +373,18 @@ public class ProductImportService : IProductImportService
|
||||
|
||||
// ===== HUMAN-READABLE TEXT FORMAT IMPORT =====
|
||||
|
||||
public async Task<ProductImportResultDto> ImportFromHumanTextAsync(string textContent)
|
||||
public async Task<ProductImportResultDto> ImportFromHumanTextAsync(string textContent, bool replaceAll = false)
|
||||
{
|
||||
var result = new ProductImportResultDto();
|
||||
|
||||
// Replace all existing data if requested
|
||||
if (replaceAll)
|
||||
{
|
||||
_logger.LogWarning("REPLACE ALL: Deleting all existing products, variants, and categories");
|
||||
var deletedCount = await DeleteAllProductsAndCategoriesAsync();
|
||||
_logger.LogInformation("Deleted {Count} existing records", deletedCount);
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(textContent))
|
||||
{
|
||||
result.Errors.Add(new ProductImportErrorDto
|
||||
@@ -677,4 +726,38 @@ stock: 75
|
||||
- 250g Bulk; 32.00; 10
|
||||
";
|
||||
}
|
||||
|
||||
public async Task<int> DeleteAllProductsAndCategoriesAsync()
|
||||
{
|
||||
var deletedCount = 0;
|
||||
|
||||
// Delete all product photos
|
||||
var photos = await _context.ProductPhotos.ToListAsync();
|
||||
_context.ProductPhotos.RemoveRange(photos);
|
||||
deletedCount += photos.Count;
|
||||
|
||||
// Delete all product variants
|
||||
var variants = await _context.ProductVariants.ToListAsync();
|
||||
_context.ProductVariants.RemoveRange(variants);
|
||||
deletedCount += variants.Count;
|
||||
|
||||
// Delete all product multi-buys
|
||||
var multiBuys = await _context.ProductMultiBuys.ToListAsync();
|
||||
_context.ProductMultiBuys.RemoveRange(multiBuys);
|
||||
deletedCount += multiBuys.Count;
|
||||
|
||||
// Delete all products
|
||||
var products = await _context.Products.ToListAsync();
|
||||
_context.Products.RemoveRange(products);
|
||||
deletedCount += products.Count;
|
||||
|
||||
// Delete all categories
|
||||
var categories = await _context.Categories.ToListAsync();
|
||||
_context.Categories.RemoveRange(categories);
|
||||
deletedCount += categories.Count;
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
return deletedCount;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user