diff --git a/Webshop.Application/DTOs/Products/UpdateAdminProductDto.cs b/Webshop.Application/DTOs/Products/UpdateAdminProductDto.cs index 8f2b53c..d0d480b 100644 --- a/Webshop.Application/DTOs/Products/UpdateAdminProductDto.cs +++ b/Webshop.Application/DTOs/Products/UpdateAdminProductDto.cs @@ -38,4 +38,3 @@ namespace Webshop.Application.DTOs.Products 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 b07ac6b..2f1ac34 100644 --- a/Webshop.Application/Services/Admin/AdminProductService.cs +++ b/Webshop.Application/Services/Admin/AdminProductService.cs @@ -111,7 +111,6 @@ namespace Webshop.Application.Services.Admin #region Unchanged Methods public async Task UpdateAdminProductAsync(UpdateAdminProductDto productDto) { - // 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) @@ -122,33 +121,25 @@ namespace Webshop.Application.Services.Admin return ServiceResult.Fail(ServiceResultType.NotFound, $"Produkt mit ID '{productDto.Id}' nicht gefunden."); } - // 2. Validierung (SKU und Slug auf Einzigartigkeit prüfen) + // Validierung 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) { /* ... */ } 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 (slugExists) { /* ... */ } - // 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. + // Concurrency Token setzen _context.Entry(existingProduct).Property("xmin").OriginalValue = productDto.RowVersion; - // 4. Bilder-Logik (aufgeräumt und klarer) + // Bilder-Logik await HandleImageUpdates(productDto, existingProduct); - // 5. Produkt-Eigenschaften mappen + // Produkt-Eigenschaften mappen MapDtoToEntity(productDto, existingProduct); - // 6. Kategorie-Verknüpfungen aktualisieren + // Kategorie-Verknüpfungen aktualisieren UpdateProductCategories(productDto, existingProduct); - // 7. Speichern + // Speichern try { await _productRepository.UpdateProductAsync(existingProduct); @@ -156,11 +147,67 @@ namespace Webshop.Application.Services.Admin } 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 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(); + _context.ProductImages.RemoveRange(imagesToRemove); + } + + // KORREKTUR 1: Logik für MainImageId entfernt. Wir verlassen uns nur auf MainImageFile. + // Hauptbild aktualisieren/hinzufügen + if (dto.MainImageFile != null) + { + // Altes Hauptbild als "nicht Hauptbild" markieren + 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 }); + } + + // 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 }); + } + } + + // KORREKTUR 2: Sortierung ohne 'CreatedDate' + // Sicherstellen, dass nur ein Hauptbild existiert + var mainImages = entity.Images.Where(i => i.IsMainImage).ToList(); + if (mainImages.Count > 1) // Wenn durch den Upload ein zweites Hauptbild hinzugefügt wurde + { + // De-priorisiere alle außer dem neuesten (implizit das letzte hinzugefügte) + for (int i = 0; i < mainImages.Count - 1; i++) + { + mainImages[i].IsMainImage = false; + } + } + + // Wenn nach dem Löschen gar kein Hauptbild mehr da ist, setze das erste Bild als Hauptbild + if (!entity.Images.Any(i => i.IsMainImage) && entity.Images.Any()) + { + entity.Images.First().IsMainImage = true; + } + + // DisplayOrder neu berechnen + int displayOrder = 1; + var orderedImages = entity.Images.OrderByDescending(i => i.IsMainImage).ThenBy(i => i.Id).ToList(); // Sortiere nach ID als Fallback + orderedImages.ForEach(i => i.DisplayOrder = displayOrder++); + } + private void MapDtoToEntity(UpdateAdminProductDto dto, Product entity) { entity.Name = dto.Name; @@ -179,52 +226,6 @@ namespace Webshop.Application.Services.Admin 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) {