From 2839eccf7183aa7f346de26073430c232a4afbc8 Mon Sep 17 00:00:00 2001 From: "Tizian.Breuch" Date: Fri, 7 Nov 2025 12:12:46 +0100 Subject: [PATCH] test bilder --- .../DTOs/Products/UpdateAdminProductDto.cs | 2 + .../Services/Admin/AdminProductService.cs | 133 ++++++++++++++++-- 2 files changed, 125 insertions(+), 10 deletions(-) diff --git a/Webshop.Application/DTOs/Products/UpdateAdminProductDto.cs b/Webshop.Application/DTOs/Products/UpdateAdminProductDto.cs index 23c98a4..8f2b53c 100644 --- a/Webshop.Application/DTOs/Products/UpdateAdminProductDto.cs +++ b/Webshop.Application/DTOs/Products/UpdateAdminProductDto.cs @@ -35,5 +35,7 @@ namespace Webshop.Application.DTOs.Products public decimal? PurchasePrice { get; set; } public bool IsFeatured { get; set; } public int FeaturedDisplayOrder { get; set; } + public uint RowVersion { get; set; } } +} } \ No newline at end of file diff --git a/Webshop.Application/Services/Admin/AdminProductService.cs b/Webshop.Application/Services/Admin/AdminProductService.cs index c8275bf..b07ac6b 100644 --- a/Webshop.Application/Services/Admin/AdminProductService.cs +++ b/Webshop.Application/Services/Admin/AdminProductService.cs @@ -111,18 +111,131 @@ namespace Webshop.Application.Services.Admin #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."); } + // 1. Produkt aus der DB laden (inklusive der zugehörigen Bilder und Kategorien) + 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."); + } + + // 2. Validierung (SKU und Slug auf Einzigartigkeit prüfen) 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."); + } + + // 3. WICHTIG: Den Concurrency Token setzen! + // Sagt Entity Framework, welche Version des Produkts der Benutzer ursprünglich hatte. + // "xmin" ist der Name der Concurrency-Spalte in PostgreSQL. + _context.Entry(existingProduct).Property("xmin").OriginalValue = productDto.RowVersion; + + // 4. Bilder-Logik (aufgeräumt und klarer) + await HandleImageUpdates(productDto, existingProduct); + + // 5. Produkt-Eigenschaften mappen + MapDtoToEntity(productDto, existingProduct); + + // 6. Kategorie-Verknüpfungen aktualisieren + UpdateProductCategories(productDto, existingProduct); + + // 7. Speichern + try + { + await _productRepository.UpdateProductAsync(existingProduct); + return ServiceResult.Ok(); + } + catch (DbUpdateConcurrencyException) + { + // Dieser Fehler wird jetzt ausgelöst, wenn jemand anderes das Produkt in der Zwischenzeit geändert hat. + return ServiceResult.Fail(ServiceResultType.Conflict, "Die Produktdaten wurden von einem anderen Benutzer geändert. Bitte laden Sie die Seite neu."); + } + } + + private void MapDtoToEntity(UpdateAdminProductDto dto, Product entity) + { + entity.Name = dto.Name; + entity.Description = dto.Description; + entity.SKU = dto.SKU; + entity.Price = dto.Price; + entity.IsActive = dto.IsActive; + entity.StockQuantity = dto.StockQuantity; + entity.Slug = dto.Slug; + entity.Weight = dto.Weight; + entity.OldPrice = dto.OldPrice; + entity.SupplierId = dto.SupplierId; + entity.PurchasePrice = dto.PurchasePrice; + entity.LastModifiedDate = DateTimeOffset.UtcNow; + entity.IsFeatured = dto.IsFeatured; + entity.FeaturedDisplayOrder = dto.FeaturedDisplayOrder; + } + + private async Task HandleImageUpdates(UpdateAdminProductDto dto, Product entity) + { + // Bilder zum Löschen entfernen + if (dto.ImagesToDelete != null && dto.ImagesToDelete.Any()) + { + var imagesToRemove = entity.Images.Where(img => dto.ImagesToDelete.Contains(img.Id)).ToList(); + foreach (var image in imagesToRemove) + { + // Optional: Datei auch aus dem Storage löschen + // await _fileStorageService.DeleteFileAsync(image.Url); + } + _context.ProductImages.RemoveRange(imagesToRemove); + } + + // Hauptbild aktualisieren/hinzufügen + if (dto.MainImageFile != null) + { + // Altes Hauptbild als "nicht Hauptbild" markieren oder löschen + var oldMainImage = entity.Images.FirstOrDefault(i => i.IsMainImage); + if (oldMainImage != null) oldMainImage.IsMainImage = false; + + await using var stream = dto.MainImageFile.OpenReadStream(); + var url = await _fileStorageService.SaveFileAsync(stream, dto.MainImageFile.FileName, dto.MainImageFile.ContentType); + entity.Images.Add(new ProductImage { Url = url, IsMainImage = true }); + } + else if (dto.MainImageId.HasValue) // Wenn ein existierendes Bild zum Hauptbild gemacht wird + { + entity.Images.ToList().ForEach(i => i.IsMainImage = (i.Id == dto.MainImageId.Value)); + } + + // Zusätzliche neue Bilder hinzufügen + if (dto.AdditionalImageFiles != null && dto.AdditionalImageFiles.Any()) + { + foreach (var file in dto.AdditionalImageFiles) + { + await using var stream = file.OpenReadStream(); + var url = await _fileStorageService.SaveFileAsync(stream, file.FileName, file.ContentType); + entity.Images.Add(new ProductImage { Url = url, IsMainImage = false }); + } + } + + // DisplayOrder neu berechnen + int displayOrder = 1; + var orderedImages = entity.Images.OrderByDescending(i => i.IsMainImage).ThenBy(i => i.CreatedDate).ToList(); + orderedImages.ForEach(i => i.DisplayOrder = displayOrder++); + } + + private void UpdateProductCategories(UpdateAdminProductDto dto, Product entity) + { + entity.Productcategories.Clear(); + if (dto.CategorieIds != null) + { + foreach (var categoryId in dto.CategorieIds) + { + entity.Productcategories.Add(new Productcategorie { categorieId = categoryId }); + } + } } public async Task DeleteAdminProductAsync(Guid id)