diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..f0895cc --- /dev/null +++ b/Dockerfile @@ -0,0 +1,14 @@ +FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build +WORKDIR /src + +COPY ["Francesco.Recipes.World/Francesco.Recipes.World.csproj", "Francesco.Recipes.World/"] +RUN dotnet restore "Francesco.Recipes.World/Francesco.Recipes.World.csproj" + +COPY . . +WORKDIR "/src/Francesco.Recipes.World" +RUN dotnet publish "Francesco.Recipes.World.csproj" -c Release -o /app/publish + +FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS runtime +WORKDIR /app +COPY --from=build /app/publish . +ENTRYPOINT ["dotnet", "Francesco.Recipes.World.dll"] diff --git a/Francesco.Recipes.World/Controller/ShoppingList/ShoppingListController.cs b/Francesco.Recipes.World/Controller/ShoppingList/ShoppingListController.cs index a1494a7..8922abd 100644 --- a/Francesco.Recipes.World/Controller/ShoppingList/ShoppingListController.cs +++ b/Francesco.Recipes.World/Controller/ShoppingList/ShoppingListController.cs @@ -32,14 +32,52 @@ return BadRequest("No ingredients provided."); } - var shoppingList = await _shoppingListRepository.AddIngredientsToShoppingListAsync(request.RecipeId, request.IngredientIds); + // Compute duplicates for this recipe in the current shopping list + var existingShoppingList = await _context.ShoppingLists + .Include(sl => sl.RecipeShoppingList) + .ThenInclude(rsl => rsl.SelectedIngredients) + .ThenInclude(si => si.RecipeIngredient) + .Include(sl => sl.RecipeShoppingList) + .ThenInclude(rsl => rsl.Recipe) + .FirstOrDefaultAsync(sl => sl.RecipeShoppingList.Any(rsl => rsl.Recipe.Id == request.RecipeId)); + + var existingForRecipe = existingShoppingList? + .RecipeShoppingList.FirstOrDefault(rsl => rsl.Recipe.Id == request.RecipeId); + + var existingIds = existingForRecipe?.SelectedIngredients + .Where(si => si.RecipeIngredient != null) + .Select(si => si.RecipeIngredient.Id) + .ToHashSet() ?? new HashSet(); + + var skippedExisting = request.IngredientIds.Where(id => existingIds.Contains(id)).Distinct().ToList(); + var ingredientIdsToAdd = request.IngredientIds.Except(skippedExisting).Distinct().ToList(); + + if (ingredientIdsToAdd.Count == 0) + { + return Json(new + { + shoppingListId = existingShoppingList?.Id, + skippedExisting, + addedCount = 0, + skippedCount = skippedExisting.Count, + noAdditions = true, + }); + } + + var shoppingList = await _shoppingListRepository.AddIngredientsToShoppingListAsync(request.RecipeId, ingredientIdsToAdd); if (shoppingList == null) { return BadRequest("Error creating shopping list."); } - return Json(new { shoppingListId = shoppingList.Id }); + return Json(new + { + shoppingListId = shoppingList.Id, + skippedExisting, + addedCount = ingredientIdsToAdd.Count, + skippedCount = skippedExisting.Count, + }); } [HttpGet("RecipeCount")] diff --git a/Francesco.Recipes.World/Views/Recipe/Details.cshtml b/Francesco.Recipes.World/Views/Recipe/Details.cshtml index 65a7001..1546875 100644 --- a/Francesco.Recipes.World/Views/Recipe/Details.cshtml +++ b/Francesco.Recipes.World/Views/Recipe/Details.cshtml @@ -196,10 +196,28 @@ }) .then(result => { localStorage.setItem('shoppingListId', result.shoppingListId); - alert('Einkaufsliste aktualisiert.'); + const showInfo = (msg) => { + if (window.Swal) { + Swal.fire({ icon: 'info', title: 'Hinweis', text: msg, timer: 1800, showConfirmButton: false }); + } else { alert(msg); } + }; + + if (result.skippedCount && result.skippedCount > 0) { + if (result.addedCount && result.addedCount > 0) { + showInfo(`Einkaufsliste aktualisiert. ${result.skippedCount} Zutaten waren bereits vorhanden und wurden übersprungen.`); + } else { + showInfo('Diese Zutaten sind bereits in der Einkaufsliste vorhanden.'); + } + } else { + showInfo('Einkaufsliste aktualisiert.'); + } }) .catch(error => { - alert('Fehler beim Aktualisieren der Einkaufsliste.'); + if (window.Swal) { + Swal.fire({ icon: 'error', title: 'Fehler', text: 'Fehler beim Aktualisieren der Einkaufsliste.' }); + } else { + alert('Fehler beim Aktualisieren der Einkaufsliste.'); + } }); }; }); diff --git a/Francesco.Recipes.World/Views/ShoppingList/Details.cshtml b/Francesco.Recipes.World/Views/ShoppingList/Details.cshtml index a816f60..6c7f383 100644 --- a/Francesco.Recipes.World/Views/ShoppingList/Details.cshtml +++ b/Francesco.Recipes.World/Views/ShoppingList/Details.cshtml @@ -13,7 +13,7 @@ id = ri.Id, name = ri.Ingredient.Name, amount = ri.Quantity, - unit = ri.Unit.Name, + unit = ri.Unit.Symbol, shoppingListId = Model.RecipeIngredientToShoppingListMap.ContainsKey(ri.Id) ? Model.RecipeIngredientToShoppingListMap[ri.Id] : Guid.Empty @@ -104,3 +104,4 @@ } + diff --git a/Francesco.Recipes.World/appsettings.Production.json b/Francesco.Recipes.World/appsettings.Production.json new file mode 100644 index 0000000..0cddf7c --- /dev/null +++ b/Francesco.Recipes.World/appsettings.Production.json @@ -0,0 +1,11 @@ +{ + "ConnectionStrings": { + "FrancescosRecipesWorldDbContextConnection": "Server=sqlserver-recipes,1433;Database=Francesco.Recipes.World;User Id=sa;Password=YourRecipesPassword123!;TrustServerCertificate=True;MultipleActiveResultSets=true" + }, + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..53f52c5 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,48 @@ +networks: + recipesnet-dev: + driver: bridge + +services: + + francescos-recipes-world: + container_name: francescos-recipes-world-dev + depends_on: + sqlserver-recipes: + condition: service_healthy + environment: + - ASPNETCORE_ENVIRONMENT=Production + - TZ=Europe/Zurich + - ConnectionStrings__FrancescosRecipesWorldDbContextConnection=Server=sqlserver-recipes,1433;Database=Francesco.Recipes.World;User Id=sa;Password=YourRecipesPassword123!;TrustServerCertificate=True;MultipleActiveResultSets=true + image: francesco448/francescos.recipes.world:latest + networks: + - recipesnet-dev + ports: + - '9003:8080' + restart: unless-stopped + volumes: + - /mnt/PoolHDD/AppData/Francescos--Recipes-World/app:/app + working_dir: /app + + sqlserver-recipes: + container_name: sqlserver-recipes-dev + environment: + - ACCEPT_EULA=Y + - SA_PASSWORD=YourRecipesPassword123! + healthcheck: + interval: 10s + retries: 10 + start_period: 30s + test: + - CMD-SHELL + - >- + /opt/mssql-tools18/bin/sqlcmd -S localhost -U sa -P YourRecipesPassword123! + -Q 'SELECT 1' -No + timeout: 5s + image: mcr.microsoft.com/mssql/server:2022-latest + networks: + - recipesnet-dev + ports: + - '1702:1433' + restart: unless-stopped + volumes: + - /mnt/PoolHDD/AppData/Francescos--Recipes-World/sqlserver:/var/opt/mssql