From 34474698517e576e4f6c8196f3061b63e8e276ba Mon Sep 17 00:00:00 2001 From: "Tizian.Breuch" Date: Thu, 25 Sep 2025 14:22:29 +0200 Subject: [PATCH] adminproduct --- .../Admin/AdminProductsController.cs | 87 +++++++--- .../Services/Admin/AdminProductService.cs | 161 ++++++++++-------- .../Admin/Interfaces/IAdminProductService.cs | 11 +- 3 files changed, 156 insertions(+), 103 deletions(-) diff --git a/Webshop.Api/Controllers/Admin/AdminProductsController.cs b/Webshop.Api/Controllers/Admin/AdminProductsController.cs index 894ece5..1cd8b5e 100644 --- a/Webshop.Api/Controllers/Admin/AdminProductsController.cs +++ b/Webshop.Api/Controllers/Admin/AdminProductsController.cs @@ -5,8 +5,10 @@ using Microsoft.AspNetCore.Mvc; using System; using System.Collections.Generic; using System.Threading.Tasks; +using Webshop.Application; using Webshop.Application.DTOs.Products; using Webshop.Application.Services.Admin.Interfaces; + namespace Webshop.Api.Controllers.Admin { [ApiController] @@ -22,54 +24,93 @@ namespace Webshop.Api.Controllers.Admin } [HttpGet] - public async Task>> GetAdminProducts() + [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] + public async Task GetAdminProducts() { - var products = await _adminProductService.GetAllAdminProductsAsync(); - return Ok(products); + var result = await _adminProductService.GetAllAdminProductsAsync(); + return Ok(result.Value); } [HttpGet("{id}")] - public async Task> GetAdminProduct(Guid id) + [ProducesResponseType(typeof(AdminProductDto), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)] + public async Task GetAdminProduct(Guid id) { - var product = await _adminProductService.GetAdminProductByIdAsync(id); - if (product == null) return NotFound(); - return Ok(product); + var result = await _adminProductService.GetAdminProductByIdAsync(id); + + return result.Type switch + { + ServiceResultType.Success => Ok(result.Value), + ServiceResultType.NotFound => NotFound(new { Message = result.ErrorMessage }), + _ => StatusCode(StatusCodes.Status500InternalServerError, new { Message = result.ErrorMessage ?? "Ein unerwarteter Fehler ist aufgetreten." }) + }; } [HttpPost] [Consumes("multipart/form-data")] - public async Task> CreateAdminProduct([FromForm] CreateAdminProductDto productDto) + [ProducesResponseType(typeof(AdminProductDto), StatusCodes.Status201Created)] + [ProducesResponseType(typeof(ValidationProblemDetails), StatusCodes.Status400BadRequest)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status409Conflict)] + public async Task CreateAdminProduct([FromForm] CreateAdminProductDto productDto) { - if (!ModelState.IsValid) return BadRequest(ModelState); + if (!ModelState.IsValid) + { + return BadRequest(ModelState); + } - var createdProduct = await _adminProductService.CreateAdminProductAsync(productDto); - if (createdProduct == null) return BadRequest("Produkt konnte nicht erstellt werden."); + var result = await _adminProductService.CreateAdminProductAsync(productDto); - return CreatedAtAction(nameof(GetAdminProduct), new { id = createdProduct.Id }, createdProduct); + return result.Type switch + { + ServiceResultType.Success => CreatedAtAction(nameof(GetAdminProduct), new { id = result.Value!.Id }, result.Value), + ServiceResultType.Conflict => Conflict(new { Message = result.ErrorMessage }), + ServiceResultType.InvalidInput => BadRequest(new { Message = result.ErrorMessage }), + _ => StatusCode(StatusCodes.Status500InternalServerError, new { Message = result.ErrorMessage ?? "Ein unerwarteter Fehler ist aufgetreten." }) + }; } - [HttpPut("{id}")] [Consumes("multipart/form-data")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(typeof(ValidationProblemDetails), StatusCodes.Status400BadRequest)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status409Conflict)] public async Task UpdateAdminProduct(Guid id, [FromForm] UpdateAdminProductDto productDto) { - if (id != productDto.Id) return BadRequest("ID in URL und Body stimmen nicht überein."); - if (!ModelState.IsValid) return BadRequest(ModelState); + if (id != productDto.Id) + { + return BadRequest(new { Message = "ID in der URL und im Body stimmen nicht überein." }); + } + if (!ModelState.IsValid) + { + return BadRequest(ModelState); + } - var success = await _adminProductService.UpdateAdminProductAsync(productDto); + var result = await _adminProductService.UpdateAdminProductAsync(productDto); - if (!success) return NotFound(); - - return NoContent(); + return result.Type switch + { + ServiceResultType.Success => NoContent(), + ServiceResultType.NotFound => NotFound(new { Message = result.ErrorMessage }), + ServiceResultType.Conflict => Conflict(new { Message = result.ErrorMessage }), + ServiceResultType.InvalidInput => BadRequest(new { Message = result.ErrorMessage }), + _ => StatusCode(StatusCodes.Status500InternalServerError, new { Message = result.ErrorMessage ?? "Ein unerwarteter Fehler ist aufgetreten." }) + }; } - [HttpDelete("{id}")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)] public async Task DeleteAdminProduct(Guid id) { - var success = await _adminProductService.DeleteAdminProductAsync(id); - if (!success) return NotFound(); - return NoContent(); + var result = await _adminProductService.DeleteAdminProductAsync(id); + + return result.Type switch + { + ServiceResultType.Success => NoContent(), + ServiceResultType.NotFound => NotFound(new { Message = result.ErrorMessage }), + _ => StatusCode(StatusCodes.Status500InternalServerError, new { Message = result.ErrorMessage ?? "Ein unerwarteter Fehler ist aufgetreten." }) + }; } } } \ No newline at end of file diff --git a/Webshop.Application/Services/Admin/AdminProductService.cs b/Webshop.Application/Services/Admin/AdminProductService.cs index 503d6d8..24cd0b3 100644 --- a/Webshop.Application/Services/Admin/AdminProductService.cs +++ b/Webshop.Application/Services/Admin/AdminProductService.cs @@ -6,6 +6,7 @@ using System; using System.Collections.Generic; using System.Threading.Tasks; using System.Linq; +using Webshop.Application; using Webshop.Application.DTOs.Products; using Webshop.Application.Services.Admin.Interfaces; using Webshop.Infrastructure.Data; @@ -16,7 +17,7 @@ namespace Webshop.Application.Services.Admin { private readonly IProductRepository _productRepository; private readonly IFileStorageService _fileStorageService; - private readonly ApplicationDbContext _context; + private readonly ApplicationDbContext _context; // For checks that might not be in the repository public AdminProductService( IProductRepository productRepository, @@ -28,7 +29,7 @@ namespace Webshop.Application.Services.Admin _context = context; } - public async Task> GetAllAdminProductsAsync() + public async Task>> GetAllAdminProductsAsync() { var products = await _context.Products .Include(p => p.Productcategories) @@ -36,83 +37,46 @@ namespace Webshop.Application.Services.Admin .OrderBy(p => p.Name) .ToListAsync(); - return products.Select(p => new AdminProductDto - { - Id = p.Id, - Name = p.Name, - Description = p.Description, - SKU = p.SKU, - Price = p.Price, - OldPrice = p.OldPrice, - IsActive = p.IsActive, - IsInStock = p.IsInStock, - StockQuantity = p.StockQuantity, - Weight = p.Weight, - Slug = p.Slug, - CreatedDate = p.CreatedDate, - LastModifiedDate = p.LastModifiedDate, - SupplierId = p.SupplierId, - PurchasePrice = p.PurchasePrice, - categorieIds = p.Productcategories.Select(pc => pc.categorieId).ToList(), - Images = p.Images.OrderBy(i => i.DisplayOrder).Select(img => new ProductImageDto - { - Id = img.Id, - Url = img.Url, - IsMainImage = img.IsMainImage, - DisplayOrder = img.DisplayOrder - }).ToList() - }).ToList(); + var dtos = products.Select(MapToAdminDto).ToList(); + return ServiceResult.Ok>(dtos); } - public async Task GetAdminProductByIdAsync(Guid id) + 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 null; - - return new AdminProductDto + if (product == null) { - 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, - 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 ServiceResult.Fail(ServiceResultType.NotFound, $"Produkt mit ID '{id}' nicht gefunden."); + } + + return ServiceResult.Ok(MapToAdminDto(product)); } - public async Task CreateAdminProductAsync(CreateAdminProductDto productDto) + public async Task> CreateAdminProductAsync(CreateAdminProductDto productDto) { - var images = new List(); + 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."); + } + 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."); + } - // Hauptbild hochladen + var images = new List(); + // Bild-Upload Logik... if (productDto.MainImageFile != null) { 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 }); } - // Weitere Bilder hochladen if (productDto.AdditionalImageFiles != null) { int order = 2; @@ -137,33 +101,46 @@ namespace Webshop.Application.Services.Admin OldPrice = productDto.OldPrice, SupplierId = productDto.SupplierId, PurchasePrice = productDto.PurchasePrice, - IsFeatured = productDto.IsFeatured, // << NEU >> - FeaturedDisplayOrder = productDto.FeaturedDisplayOrder, // << NEU >> + IsFeatured = productDto.IsFeatured, + FeaturedDisplayOrder = productDto.FeaturedDisplayOrder, Images = images, Productcategories = productDto.CategorieIds.Select(cId => new Productcategorie { categorieId = cId }).ToList() }; await _productRepository.AddProductAsync(newProduct); - return await GetAdminProductByIdAsync(newProduct.Id); + return ServiceResult.Ok(MapToAdminDto(newProduct)); } - public async Task UpdateAdminProductAsync(UpdateAdminProductDto productDto) + 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 false; + if (existingProduct == null) + { + return ServiceResult.Fail(ServiceResultType.NotFound, $"Produkt mit ID '{productDto.Id}' nicht gefunden."); + } - // Bilder löschen + 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."); + } + + // Bild-Management Logik... if (productDto.ImagesToDelete != null && productDto.ImagesToDelete.Any()) { var imagesToRemove = existingProduct.Images.Where(img => productDto.ImagesToDelete.Contains(img.Id)).ToList(); _context.ProductImages.RemoveRange(imagesToRemove); } - // Hauptbild aktualisieren/hochladen if (productDto.MainImageFile != null) { var existingMainImage = existingProduct.Images.FirstOrDefault(img => img.IsMainImage); @@ -173,7 +150,6 @@ namespace Webshop.Application.Services.Admin var url = await _fileStorageService.SaveFileAsync(stream, productDto.MainImageFile.FileName, productDto.MainImageFile.ContentType); existingProduct.Images.Add(new ProductImage { Url = url, IsMainImage = true, DisplayOrder = 1 }); } - // Weitere Bilder hinzufügen if (productDto.AdditionalImageFiles != null && productDto.AdditionalImageFiles.Any()) { int displayOrder = (existingProduct.Images.Any() ? existingProduct.Images.Max(i => i.DisplayOrder) : 0) + 1; @@ -185,7 +161,7 @@ namespace Webshop.Application.Services.Admin } } - // Basisdaten aktualisieren + // Basisdaten aktualisieren... existingProduct.Name = productDto.Name; existingProduct.Description = productDto.Description; existingProduct.SKU = productDto.SKU; @@ -198,8 +174,8 @@ namespace Webshop.Application.Services.Admin existingProduct.SupplierId = productDto.SupplierId; existingProduct.PurchasePrice = productDto.PurchasePrice; existingProduct.LastModifiedDate = DateTimeOffset.UtcNow; - existingProduct.IsFeatured = productDto.IsFeatured; // << NEU >> - existingProduct.FeaturedDisplayOrder = productDto.FeaturedDisplayOrder; // << NEU >> + existingProduct.IsFeatured = productDto.IsFeatured; + existingProduct.FeaturedDisplayOrder = productDto.FeaturedDisplayOrder; // Kategorien synchronisieren existingProduct.Productcategories.Clear(); @@ -212,16 +188,51 @@ namespace Webshop.Application.Services.Admin } await _productRepository.UpdateProductAsync(existingProduct); - return true; + return ServiceResult.Ok(); } - public async Task DeleteAdminProductAsync(Guid id) + public async Task DeleteAdminProductAsync(Guid id) { var product = await _productRepository.GetProductByIdAsync(id); - if (product == null) return false; + if (product == null) + { + return ServiceResult.Fail(ServiceResultType.NotFound, $"Produkt mit ID '{id}' nicht gefunden."); + } await _productRepository.DeleteProductAsync(id); - return true; + 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() + }; } } } \ No newline at end of file diff --git a/Webshop.Application/Services/Admin/Interfaces/IAdminProductService.cs b/Webshop.Application/Services/Admin/Interfaces/IAdminProductService.cs index 66070ad..5a07d14 100644 --- a/Webshop.Application/Services/Admin/Interfaces/IAdminProductService.cs +++ b/Webshop.Application/Services/Admin/Interfaces/IAdminProductService.cs @@ -2,16 +2,17 @@ using System; using System.Collections.Generic; using System.Threading.Tasks; +using Webshop.Application; using Webshop.Application.DTOs.Products; namespace Webshop.Application.Services.Admin.Interfaces { public interface IAdminProductService { - Task> GetAllAdminProductsAsync(); - Task GetAdminProductByIdAsync(Guid id); - Task CreateAdminProductAsync(CreateAdminProductDto productDto); // << DTO-TYP GEÄNDERT >> - Task UpdateAdminProductAsync(UpdateAdminProductDto productDto); // << DTO-TYP GEÄNDERT >> - Task DeleteAdminProductAsync(Guid id); + Task>> GetAllAdminProductsAsync(); + Task> GetAdminProductByIdAsync(Guid id); + Task> CreateAdminProductAsync(CreateAdminProductDto productDto); + Task UpdateAdminProductAsync(UpdateAdminProductDto productDto); + Task DeleteAdminProductAsync(Guid id); } } \ No newline at end of file