diff --git a/Webshop.Application/Services/Admin/AdminProductService.cs b/Webshop.Application/Services/Admin/AdminProductService.cs index 1678c0a..3c1a55f 100644 --- a/Webshop.Application/Services/Admin/AdminProductService.cs +++ b/Webshop.Application/Services/Admin/AdminProductService.cs @@ -110,40 +110,28 @@ namespace Webshop.Application.Services.Admin // ... (UpdateAdminProductAsync und DeleteAdminProductAsync und MapToAdminDto bleiben unverändert) ... #region Unchanged Methods + // src/Webshop.Application/Services/Admin/AdminProductService.cs + public async Task UpdateAdminProductAsync(UpdateAdminProductDto productDto) { - Console.WriteLine($"---- UPDATE START: Produkt-ID {productDto.Id} ----"); - // SCHRITT 1: Lade NUR das Hauptprodukt, ohne Relationen. - var existingProduct = await _context.Products.FirstOrDefaultAsync(p => p.Id == productDto.Id); + // 1. Produkt laden (Tracked) + 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."); } - // --- CONCURRENCY CHECK --- + // 2. Concurrency Check (nur initial) if (!string.IsNullOrEmpty(productDto.RowVersion)) { try { - // <<<<<< WICHTIGE KORREKTUR HIER >>>>>> - // FormData wandelt "+" oft in " " um. Das reparieren wir hier: string fixedRowVersion = productDto.RowVersion.Replace(" ", "+"); - - // 1. Umwandlung von String (Base64) zu Byte-Array byte[] incomingRowVersion = Convert.FromBase64String(fixedRowVersion); - - // DEBUG-LOGGING - string dbValue = Convert.ToBase64String(existingProduct.RowVersion ?? new byte[0]); - - Console.WriteLine($"DB RowVersion: {dbValue}"); - Console.WriteLine($"Frontend RowVersion: {fixedRowVersion}"); // Logge den reparierten Wert - - if (dbValue != fixedRowVersion) - { - Console.WriteLine("!!! WARNUNG: Versionen stimmen nicht überein !!!"); - } - - // 2. Setze den Originalwert für EF Core _context.Entry(existingProduct).Property(p => p.RowVersion).OriginalValue = incomingRowVersion; } catch (FormatException) @@ -152,51 +140,83 @@ namespace Webshop.Application.Services.Admin } } + // ----------------------------------------------------------------------- + // SCHRITT A: BILDER & KATEGORIEN UPDATE + // ----------------------------------------------------------------------- + bool imagesChanged = false; - // SCHRITT 3: Lade jetzt die Relationen explizit nach. - await _context.Entry(existingProduct).Collection(p => p.Images).LoadAsync(); - await _context.Entry(existingProduct).Collection(p => p.Productcategories).LoadAsync(); - - - 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."); } - 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."); } - - // --- BILDER-LOGGING --- + // Bilder löschen if (productDto.ImagesToDelete != null && productDto.ImagesToDelete.Any()) { - Console.WriteLine($"---- LÖSCHE {productDto.ImagesToDelete.Count} BILDER ----"); var imagesToRemove = existingProduct.Images.Where(img => productDto.ImagesToDelete.Contains(img.Id)).ToList(); - _context.ProductImages.RemoveRange(imagesToRemove); + if (imagesToRemove.Any()) + { + _context.ProductImages.RemoveRange(imagesToRemove); + imagesChanged = true; + } } + + // Hauptbild if (productDto.MainImageFile != null) { - Console.WriteLine("---- ERSETZE HAUPTBILD ----"); 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); - Console.WriteLine($"---- NEUE HAUPTBILD-URL: {url} ----"); existingProduct.Images.Add(new ProductImage { Url = url, IsMainImage = true, DisplayOrder = 1 }); + imagesChanged = true; } + + // Zusatzbilder if (productDto.AdditionalImageFiles != null && productDto.AdditionalImageFiles.Any()) { - Console.WriteLine($"---- FÜGE {productDto.AdditionalImageFiles.Count} NEUE BILDER HINZU ----"); 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); - Console.WriteLine($"---- NEUE BILD-URL: {url} ----"); existingProduct.Images.Add(new ProductImage { Url = url, IsMainImage = false, DisplayOrder = displayOrder++ }); } + imagesChanged = true; } - - // --- EIGENSCHAFTEN-UPDATE --- - Console.WriteLine("---- AKTUALISIERE PRODUKT-EIGENSCHAFTEN ----"); + // Kategorien + existingProduct.Productcategories.Clear(); + if (productDto.CategorieIds != null) + { + foreach (var categorieId in productDto.CategorieIds) + { + existingProduct.Productcategories.Add(new Productcategorie { categorieId = categorieId }); + } + } + + // WICHTIG: Wenn Bilder geändert wurden, speichern wir HIER schon einmal zwischen. + // Das erlaubt der DB, Trigger auszuführen und die RowVersion zu aktualisieren. + if (imagesChanged) + { + try + { + await _context.SaveChangesAsync(); + + // Ganz wichtig: Die RowVersion im Entity neu laden, da sie sich in der DB geändert hat! + // Aber: Da wir das Objekt weiterverwenden wollen, müssen wir sicherstellen, + // dass EF Core weiß, dass die Version jetzt "neu" ist. + // Der einfachste Weg: Wir holen uns das aktuelle Token aus der DB. + await _context.Entry(existingProduct).ReloadAsync(); + } + catch (DbUpdateConcurrencyException) + { + return ServiceResult.Fail(ServiceResultType.Conflict, "Konflikt beim Aktualisieren der Bilder."); + } + } + + // ----------------------------------------------------------------------- + // SCHRITT B: PRODUKT EIGENSCHAFTEN UPDATE + // ----------------------------------------------------------------------- + + // Check auf Duplikate (SKU/Slug) hier wie gehabt... + existingProduct.Name = productDto.Name; existingProduct.Description = productDto.Description; existingProduct.SKU = productDto.SKU; @@ -208,34 +228,20 @@ namespace Webshop.Application.Services.Admin 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.LastModifiedDate = DateTimeOffset.UtcNow; + // Neue Version setzen existingProduct.RowVersion = Guid.NewGuid().ToByteArray(); - // --- KATEGORIEN-UPDATE --- - Console.WriteLine("---- AKTUALISIERE KATEGORIEN ----"); - existingProduct.Productcategories.Clear(); - if (productDto.CategorieIds != null) - { - foreach (var categorieId in productDto.CategorieIds) - { - existingProduct.Productcategories.Add(new Productcategorie { categorieId = categorieId }); - } - } - - // SCHRITT 5: Speichern (jetzt mit einem sauberen Change Tracker Zustand) try { - Console.WriteLine("---- RUFE SaveChangesAsync AUF ----"); await _context.SaveChangesAsync(); - Console.WriteLine("---- UPDATE BEENDET ----"); } catch (DbUpdateConcurrencyException) { - // Dieser Fehler tritt jetzt nur noch auf, wenn jemand anderes WIRKLICH - // die Daten in der Zwischenzeit geändert hat. - return ServiceResult.Fail(ServiceResultType.Conflict, "Das Produkt wurde in der Zwischenzeit von jemand anderem bearbeitet. Bitte laden Sie die Seite neu."); + return ServiceResult.Fail(ServiceResultType.Conflict, "Das Produkt wurde bearbeitet. Bitte neu laden."); } return ServiceResult.Ok();