From 7aa5ec9500a25dbb42f486d194c885484bc2d7a5 Mon Sep 17 00:00:00 2001 From: "Tizian.Breuch" Date: Fri, 7 Nov 2025 09:49:51 +0100 Subject: [PATCH] test bilder upload --- .../Services/Admin/AdminProductService.cs | 147 +++++++++++++----- Webshop.Domain/Entities/Product.cs | 44 +++++- .../Interfaces/IProductRepository.cs | 3 +- .../Repositories/ProductRepository.cs | 18 ++- 4 files changed, 164 insertions(+), 48 deletions(-) diff --git a/Webshop.Application/Services/Admin/AdminProductService.cs b/Webshop.Application/Services/Admin/AdminProductService.cs index c8275bf..1b041a3 100644 --- a/Webshop.Application/Services/Admin/AdminProductService.cs +++ b/Webshop.Application/Services/Admin/AdminProductService.cs @@ -1,4 +1,5 @@ // src/Webshop.Application/Services/Admin/AdminProductService.cs + using Microsoft.EntityFrameworkCore; using Webshop.Domain.Entities; using Webshop.Domain.Interfaces; @@ -29,47 +30,42 @@ namespace Webshop.Application.Services.Admin _context = context; } - // ... (GetAllAdminProductsAsync und GetAdminProductByIdAsync bleiben unverändert) ... - #region Unchanged Methods public async Task>> GetAllAdminProductsAsync() { - var products = await _context.Products.Include(p => p.Productcategories).Include(p => p.Images).OrderBy(p => p.Name).ToListAsync(); + var products = await _context.Products + .Include(p => p.Images) + .Include(p => p.Productcategories) + .OrderBy(p => p.Name) + .ToListAsync(); + var dtos = products.Select(MapToAdminDto).ToList(); return ServiceResult.Ok>(dtos); } public async Task> GetAdminProductByIdAsync(Guid id) { - var product = await _context.Products.Include(p => p.Productcategories).Include(p => p.Images).FirstOrDefaultAsync(p => p.Id == id); - if (product == null) { return ServiceResult.Fail(ServiceResultType.NotFound, $"Produkt mit ID '{id}' nicht gefunden."); } + var product = await _productRepository.GetProductByIdForUpdateAsync(id); + if (product == null) + { + return ServiceResult.Fail(ServiceResultType.NotFound, $"Produkt mit ID '{id}' nicht gefunden."); + } return ServiceResult.Ok(MapToAdminDto(product)); } - #endregion public async Task> CreateAdminProductAsync(CreateAdminProductDto productDto) { + // Ihre Create-Logik hier... (unverändert) var skuExists = await _context.Products.AnyAsync(p => p.SKU == productDto.SKU); - if (skuExists) - { - return ServiceResult.Fail(ServiceResultType.Conflict, $"Ein Produkt mit der SKU '{productDto.SKU}' existiert bereits."); - } + if (skuExists) { return ServiceResult.Fail(ServiceResultType.Conflict, $"Ein Produkt mit der SKU '{productDto.SKU}' existiert bereits."); } var slugExists = await _context.Products.AnyAsync(p => p.Slug == productDto.Slug); - if (slugExists) - { - return ServiceResult.Fail(ServiceResultType.Conflict, $"Ein Produkt mit dem Slug '{productDto.Slug}' existiert bereits."); - } - + if (slugExists) { return ServiceResult.Fail(ServiceResultType.Conflict, $"Ein Produkt mit dem Slug '{productDto.Slug}' existiert bereits."); } var images = new List(); - - // << KORREKTUR: Null-Prüfung für das Hauptbild >> if (productDto.MainImageFile != null && productDto.MainImageFile.Length > 0) { await using var stream = productDto.MainImageFile.OpenReadStream(); var url = await _fileStorageService.SaveFileAsync(stream, productDto.MainImageFile.FileName, productDto.MainImageFile.ContentType); images.Add(new ProductImage { Url = url, IsMainImage = true, DisplayOrder = 1 }); } - - // << KORREKTUR: Null-Prüfung für zusätzliche Bilder >> if (productDto.AdditionalImageFiles != null) { int order = 2; @@ -83,17 +79,19 @@ namespace Webshop.Application.Services.Admin } } } - var newProduct = new Product { Name = productDto.Name, Description = productDto.Description, + SKU = productDto.SKU, Price = productDto.Price, IsActive = productDto.IsActive, StockQuantity = productDto.StockQuantity, + IsInStock = productDto.StockQuantity > 0, Slug = productDto.Slug, Weight = productDto.Weight, + OldPrice = productDto.OldPrice, SupplierId = productDto.SupplierId, PurchasePrice = productDto.PurchasePrice, @@ -102,40 +100,115 @@ namespace Webshop.Application.Services.Admin Images = images, Productcategories = productDto.CategorieIds.Select(cId => new Productcategorie { categorieId = cId }).ToList() }; - await _productRepository.AddProductAsync(newProduct); return ServiceResult.Ok(MapToAdminDto(newProduct)); } - // ... (UpdateAdminProductAsync und DeleteAdminProductAsync und MapToAdminDto bleiben unverändert) ... - #region Unchanged Methods public async Task UpdateAdminProductAsync(UpdateAdminProductDto productDto) { - var existingProduct = await _context.Products.Include(p => p.Images).Include(p => p.Productcategories).FirstOrDefaultAsync(p => p.Id == productDto.Id); - if (existingProduct == null) { return ServiceResult.Fail(ServiceResultType.NotFound, $"Produkt mit ID '{productDto.Id}' nicht gefunden."); } + var existingProduct = await _productRepository.GetProductByIdForUpdateAsync(productDto.Id); + if (existingProduct == null) + { + return ServiceResult.Fail(ServiceResultType.NotFound, $"Produkt mit ID '{productDto.Id}' nicht gefunden."); + } + var skuExists = await _context.Products.AnyAsync(p => p.SKU == productDto.SKU && p.Id != productDto.Id); - if (skuExists) { return ServiceResult.Fail(ServiceResultType.Conflict, $"Ein anderes Produkt mit der SKU '{productDto.SKU}' existiert bereits."); } + if (skuExists) + { + return ServiceResult.Fail(ServiceResultType.Conflict, $"Ein anderes Produkt mit der SKU '{productDto.SKU}' existiert bereits."); + } var slugExists = await _context.Products.AnyAsync(p => p.Slug == productDto.Slug && p.Id != productDto.Id); - if (slugExists) { return ServiceResult.Fail(ServiceResultType.Conflict, $"Ein anderes Produkt mit dem Slug '{productDto.Slug}' existiert bereits."); } - if (productDto.ImagesToDelete != null && productDto.ImagesToDelete.Any()) { var imagesToRemove = existingProduct.Images.Where(img => productDto.ImagesToDelete.Contains(img.Id)).ToList(); _context.ProductImages.RemoveRange(imagesToRemove); } - if (productDto.MainImageFile != null) { var existingMainImage = existingProduct.Images.FirstOrDefault(img => img.IsMainImage); if (existingMainImage != null) _context.ProductImages.Remove(existingMainImage); await using var stream = productDto.MainImageFile.OpenReadStream(); var url = await _fileStorageService.SaveFileAsync(stream, productDto.MainImageFile.FileName, productDto.MainImageFile.ContentType); existingProduct.Images.Add(new ProductImage { Url = url, IsMainImage = true, DisplayOrder = 1 }); } - if (productDto.AdditionalImageFiles != null && productDto.AdditionalImageFiles.Any()) { int displayOrder = (existingProduct.Images.Any() ? existingProduct.Images.Max(i => i.DisplayOrder) : 0) + 1; foreach (var file in productDto.AdditionalImageFiles) { await using var stream = file.OpenReadStream(); var url = await _fileStorageService.SaveFileAsync(stream, file.FileName, file.ContentType); existingProduct.Images.Add(new ProductImage { Url = url, IsMainImage = false, DisplayOrder = displayOrder++ }); } } - existingProduct.Name = productDto.Name; existingProduct.Description = productDto.Description; existingProduct.SKU = productDto.SKU; existingProduct.Price = productDto.Price; existingProduct.IsActive = productDto.IsActive; existingProduct.StockQuantity = productDto.StockQuantity; existingProduct.Slug = productDto.Slug; existingProduct.Weight = productDto.Weight; existingProduct.OldPrice = productDto.OldPrice; existingProduct.SupplierId = productDto.SupplierId; existingProduct.PurchasePrice = productDto.PurchasePrice; existingProduct.LastModifiedDate = DateTimeOffset.UtcNow; existingProduct.IsFeatured = productDto.IsFeatured; existingProduct.FeaturedDisplayOrder = productDto.FeaturedDisplayOrder; - existingProduct.Productcategories.Clear(); if (productDto.CategorieIds != null) { foreach (var categorieId in productDto.CategorieIds) { existingProduct.Productcategories.Add(new Productcategorie { categorieId = categorieId }); } } - await _productRepository.UpdateProductAsync(existingProduct); return ServiceResult.Ok(); + if (slugExists) + { + return ServiceResult.Fail(ServiceResultType.Conflict, $"Ein anderes Produkt mit dem Slug '{productDto.Slug}' existiert bereits."); + } + + // --- KORRIGIERTER METHODENAUFRUF --- + existingProduct.UpdateFromDto( + productDto.Name, productDto.Description, productDto.SKU, productDto.Price, productDto.OldPrice, + productDto.IsActive, productDto.StockQuantity, productDto.Slug, productDto.Weight, + productDto.SupplierId, productDto.PurchasePrice, productDto.IsFeatured, productDto.FeaturedDisplayOrder, + productDto.CategorieIds + ); + + // Logik für Bilder (unverändert) + if (productDto.ImagesToDelete != null && productDto.ImagesToDelete.Any()) + { + var imagesToRemove = existingProduct.Images.Where(img => productDto.ImagesToDelete.Contains(img.Id)).ToList(); + foreach (var image in imagesToRemove) + { + existingProduct.Images.Remove(image); + } + } + + if (productDto.MainImageFile != null) + { + var oldMainImage = existingProduct.Images.FirstOrDefault(i => i.IsMainImage); + if (oldMainImage != null) { existingProduct.Images.Remove(oldMainImage); } + foreach (var img in existingProduct.Images) { img.IsMainImage = false; } + await using var stream = productDto.MainImageFile.OpenReadStream(); + var url = await _fileStorageService.SaveFileAsync(stream, productDto.MainImageFile.FileName, productDto.MainImageFile.ContentType); + existingProduct.Images.Add(new ProductImage { Url = url, IsMainImage = true }); + } + + if (productDto.AdditionalImageFiles != null && productDto.AdditionalImageFiles.Any()) + { + foreach (var file in productDto.AdditionalImageFiles) + { + await using var stream = file.OpenReadStream(); + var url = await _fileStorageService.SaveFileAsync(stream, file.FileName, file.ContentType); + existingProduct.Images.Add(new ProductImage { Url = url, IsMainImage = false }); + } + } + + int currentOrder = 1; + foreach (var image in existingProduct.Images.OrderBy(img => !img.IsMainImage).ThenBy(img => img.Id)) + { + image.DisplayOrder = currentOrder++; + } + + await _productRepository.UpdateProductAsync(); + + return ServiceResult.Ok(); } public async Task DeleteAdminProductAsync(Guid id) { var product = await _productRepository.GetProductByIdAsync(id); - if (product == null) { return ServiceResult.Fail(ServiceResultType.NotFound, $"Produkt mit ID '{id}' nicht gefunden."); } - await _productRepository.DeleteProductAsync(id); return ServiceResult.Ok(); + if (product == null) + { + return ServiceResult.Fail(ServiceResultType.NotFound, $"Produkt mit ID '{id}' nicht gefunden."); + } + await _productRepository.DeleteProductAsync(id); + return ServiceResult.Ok(); } private AdminProductDto MapToAdminDto(Product product) { - return new AdminProductDto { Id = product.Id, Name = product.Name, Description = product.Description, SKU = product.SKU, Price = product.Price, OldPrice = product.OldPrice, IsActive = product.IsActive, IsInStock = product.IsInStock, StockQuantity = product.StockQuantity, Weight = product.Weight, Slug = product.Slug, CreatedDate = product.CreatedDate, LastModifiedDate = product.LastModifiedDate, SupplierId = product.SupplierId, PurchasePrice = product.PurchasePrice, IsFeatured = product.IsFeatured, FeaturedDisplayOrder = product.FeaturedDisplayOrder, categorieIds = product.Productcategories.Select(pc => pc.categorieId).ToList(), Images = product.Images.OrderBy(i => i.DisplayOrder).Select(img => new ProductImageDto { Id = img.Id, Url = img.Url, IsMainImage = img.IsMainImage, DisplayOrder = img.DisplayOrder }).ToList() }; + return new AdminProductDto + { + Id = product.Id, + Name = product.Name, + Description = product.Description, + + SKU = product.SKU, + Price = product.Price, + OldPrice = product.OldPrice, + IsActive = product.IsActive, + IsInStock = product.IsInStock, + StockQuantity = product.StockQuantity, + Weight = product.Weight, + + Slug = product.Slug, + CreatedDate = product.CreatedDate, + LastModifiedDate = product.LastModifiedDate, + SupplierId = product.SupplierId, + PurchasePrice = product.PurchasePrice, + IsFeatured = product.IsFeatured, + FeaturedDisplayOrder = product.FeaturedDisplayOrder, + categorieIds = product.Productcategories.Select(pc => pc.categorieId).ToList(), + Images = product.Images.OrderBy(i => i.DisplayOrder).Select(img => new ProductImageDto { Id = img.Id, Url = img.Url, IsMainImage = img.IsMainImage, DisplayOrder = img.DisplayOrder }).ToList() + }; } - #endregion } } \ No newline at end of file diff --git a/Webshop.Domain/Entities/Product.cs b/Webshop.Domain/Entities/Product.cs index 82f2896..190271f 100644 --- a/Webshop.Domain/Entities/Product.cs +++ b/Webshop.Domain/Entities/Product.cs @@ -16,8 +16,7 @@ namespace Webshop.Domain.Entities [MaxLength(4000)] public string? Description { get; set; } [MaxLength(500)] - public string? ShortDescription { get; set; } - [Required, MaxLength(50)] + public string SKU { get; set; } = string.Empty; [Required] public decimal Price { get; set; } @@ -29,9 +28,7 @@ namespace Webshop.Domain.Entities [Required] public int StockQuantity { get; set; } public decimal? Weight { get; set; } - public decimal? Width { get; set; } - public decimal? Height { get; set; } - public decimal? Length { get; set; } + [Required, MaxLength(255)] public string Slug { get; set; } = string.Empty; @@ -53,5 +50,42 @@ namespace Webshop.Domain.Entities public virtual ICollection ProductDiscounts { get; set; } = new List(); public virtual ICollection Productcategories { get; set; } = new List(); public virtual ICollection Images { get; set; } = new List(); + + // --- HINZUGEFÜGTE METHODE ZUR AKTUALISIERUNG --- + // Diese Methode kapselt die Logik zur Aktualisierung der Entität aus einem DTO. + // Sie stellt sicher, dass alle relevanten Felder konsistent geändert werden. + public void UpdateFromDto( + string name, string? description, string sku, decimal price, decimal? oldPrice, + bool isActive, int stockQuantity, string slug, decimal? weight, + Guid? supplierId, decimal? purchasePrice, bool isFeatured, int featuredDisplayOrder, + List categorieIds) + { + // 1. Einfache Eigenschaften aktualisieren + Name = name; + Description = description; + SKU = sku; + Price = price; + OldPrice = oldPrice; + IsActive = isActive; + StockQuantity = stockQuantity; + IsInStock = stockQuantity > 0; // Abgeleitete Logik + Slug = slug; + Weight = weight; + SupplierId = supplierId; + PurchasePrice = purchasePrice; + IsFeatured = isFeatured; + FeaturedDisplayOrder = featuredDisplayOrder; + LastModifiedDate = DateTimeOffset.UtcNow; + + // 2. Kategori-Beziehungen sauber neu aufbauen + Productcategories.Clear(); + if (categorieIds != null) + { + foreach (var catId in categorieIds) + { + Productcategories.Add(new Productcategorie { ProductId = this.Id, categorieId = catId }); + } + } + } } } \ No newline at end of file diff --git a/Webshop.Domain/Interfaces/IProductRepository.cs b/Webshop.Domain/Interfaces/IProductRepository.cs index 9b0a0c7..32090ca 100644 --- a/Webshop.Domain/Interfaces/IProductRepository.cs +++ b/Webshop.Domain/Interfaces/IProductRepository.cs @@ -12,7 +12,8 @@ namespace Webshop.Domain.Interfaces Task GetProductByIdAsync(Guid id); Task GetBySlugAsync(string slug); Task AddProductAsync(Product product); - Task UpdateProductAsync(Product product); + Task GetProductByIdForUpdateAsync(Guid id); // NEU + Task UpdateProductAsync(); // GEÄNDERT (parameterlos) Task DeleteProductAsync(Guid id); } } \ No newline at end of file diff --git a/Webshop.Infrastructure/Repositories/ProductRepository.cs b/Webshop.Infrastructure/Repositories/ProductRepository.cs index 709f0ed..066a7f4 100644 --- a/Webshop.Infrastructure/Repositories/ProductRepository.cs +++ b/Webshop.Infrastructure/Repositories/ProductRepository.cs @@ -1,3 +1,4 @@ +// src/Webshop.Infrastructure/Repositories/ProductRepository.cs using Microsoft.EntityFrameworkCore; using Webshop.Domain.Entities; using Webshop.Domain.Interfaces; @@ -8,7 +9,7 @@ using System.Threading.Tasks; namespace Webshop.Infrastructure.Repositories { - public class ProductRepository : IProductRepository + public class ProductRepository : IProductRepository // Implementiert das korrigierte Interface { private readonly ApplicationDbContext _context; @@ -27,22 +28,29 @@ namespace Webshop.Infrastructure.Repositories return await _context.Products.FindAsync(id); } - // --- HIER DIE NEUE METHODE IMPLEMENTIEREN --- public async Task GetBySlugAsync(string slug) { - // Sucht nur nach aktiven Produkten, was für die öffentliche Ansicht korrekt ist. return await _context.Products.FirstOrDefaultAsync(p => p.Slug == slug && p.IsActive); } + // --- NEUE METHODE --- + public async Task GetProductByIdForUpdateAsync(Guid id) + { + return await _context.Products + .Include(p => p.Images) + .Include(p => p.Productcategories) + .FirstOrDefaultAsync(p => p.Id == id); + } + public async Task AddProductAsync(Product product) { await _context.Products.AddAsync(product); await _context.SaveChangesAsync(); } - public async Task UpdateProductAsync(Product product) + // --- KORRIGIERTE UPDATE-METHODE (OHNE PARAMETER) --- + public async Task UpdateProductAsync() { - //_context.Products.Update(product); await _context.SaveChangesAsync(); }