Recept och bilder
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
Elias Jansson
2025-05-22 22:15:33 +02:00
parent addef3a3ad
commit bd8c3a0fab
6 changed files with 213 additions and 77 deletions

View File

@@ -94,7 +94,6 @@ namespace Aberwyn.Controllers
} }
else else
{ {
// ✅ Hämta tidigare måltid och kopiera bilddata
var existingMeal = service.GetMealById(meal.Id); var existingMeal = service.GetMealById(meal.Id);
if (existingMeal != null) if (existingMeal != null)
{ {
@@ -103,13 +102,17 @@ namespace Aberwyn.Controllers
} }
} }
service.SaveOrUpdateMeal(meal); // 🔁 Här är ändringen använd metoden som även sparar ingredienser
service.SaveOrUpdateMealWithIngredients(meal);
return RedirectToAction("View", new { id = meal.Id }); return RedirectToAction("View", new { id = meal.Id });
} }
[HttpPost] [HttpPost]
public IActionResult DeleteMeal(int id) public IActionResult DeleteMeal(int id)
{ {

View File

@@ -134,6 +134,55 @@ namespace Aberwyn.Data
return weeklyMenu; return weeklyMenu;
} }
public List<Ingredient> GetIngredientsForMeal(int mealId)
{
var ingredients = new List<Ingredient>();
using var conn = GetConnection();
conn.Open();
string query = "SELECT Id, MealId, Quantity, Item FROM Ingredients WHERE MealId = @mealId";
using var cmd = new MySqlCommand(query, conn);
cmd.Parameters.AddWithValue("@mealId", mealId);
using var reader = cmd.ExecuteReader();
while (reader.Read())
{
ingredients.Add(new Ingredient
{
Id = reader.GetInt32("Id"),
MealId = reader.GetInt32("MealId"),
Quantity = reader.GetString("Quantity"),
Item = reader.GetString("Item")
});
}
return ingredients;
}
public void SaveIngredients(int mealId, List<Ingredient> ingredients)
{
using var conn = GetConnection();
conn.Open();
using var tx = conn.BeginTransaction();
// Ta bort gamla
var deleteCmd = new MySqlCommand("DELETE FROM Ingredients WHERE MealId = @mealId", conn, tx);
deleteCmd.Parameters.AddWithValue("@mealId", mealId);
deleteCmd.ExecuteNonQuery();
foreach (var ingredient in ingredients)
{
var insertCmd = new MySqlCommand(
"INSERT INTO Ingredients (MealId, Quantity, Item) VALUES (@mealId, @quantity, @item)",
conn, tx);
insertCmd.Parameters.AddWithValue("@mealId", mealId);
insertCmd.Parameters.AddWithValue("@quantity", ingredient.Quantity ?? "");
insertCmd.Parameters.AddWithValue("@item", ingredient.Item ?? "");
insertCmd.ExecuteNonQuery();
}
tx.Commit();
}
public List<Meal> GetMeals() public List<Meal> GetMeals()
{ {
@@ -205,7 +254,7 @@ namespace Aberwyn.Data
using var connection = GetConnection(); using var connection = GetConnection();
connection.Open(); connection.Open();
string query = @"SELECT Id, Name, Description, ProteinType, CarbType, RecipeUrl, CreatedAt, ImageUrl, ImageData, ImageMimeType FROM Meals WHERE Id = @id"; string query = @"SELECT Id, Name, Description, ProteinType, CarbType, RecipeUrl, CreatedAt, ImageUrl, ImageData, ImageMimeType, Instructions FROM Meals WHERE Id = @id";
using var cmd = new MySqlCommand(query, connection); using var cmd = new MySqlCommand(query, connection);
cmd.Parameters.AddWithValue("@id", id); cmd.Parameters.AddWithValue("@id", id);
@@ -224,8 +273,9 @@ namespace Aberwyn.Data
ImageUrl = reader.IsDBNull(reader.GetOrdinal("ImageUrl")) ? null : reader.GetString(reader.GetOrdinal("ImageUrl")), ImageUrl = reader.IsDBNull(reader.GetOrdinal("ImageUrl")) ? null : reader.GetString(reader.GetOrdinal("ImageUrl")),
CreatedAt = reader.GetDateTime(reader.GetOrdinal("CreatedAt")), CreatedAt = reader.GetDateTime(reader.GetOrdinal("CreatedAt")),
ImageData = reader.IsDBNull(reader.GetOrdinal("ImageData")) ? null : (byte[])reader["ImageData"], ImageData = reader.IsDBNull(reader.GetOrdinal("ImageData")) ? null : (byte[])reader["ImageData"],
ImageMimeType = reader.IsDBNull(reader.GetOrdinal("ImageMimeType")) ? null : reader.GetString(reader.GetOrdinal("ImageMimeType")) ImageMimeType = reader.IsDBNull(reader.GetOrdinal("ImageMimeType")) ? null : reader.GetString(reader.GetOrdinal("ImageMimeType")),
Instructions = reader.IsDBNull(reader.GetOrdinal("Instructions")) ? null : reader.GetString(reader.GetOrdinal("Instructions")),
Ingredients = GetIngredientsForMeal(id)
}; };
@@ -254,9 +304,9 @@ namespace Aberwyn.Data
{ {
cmd.CommandText = @" cmd.CommandText = @"
INSERT INTO Meals INSERT INTO Meals
(Name, Description, ProteinType, CarbType, RecipeUrl, CreatedAt, ImageUrl, ImageData, ImageMimeType) (Name, Description, ProteinType, CarbType, RecipeUrl, CreatedAt, ImageUrl, ImageData, ImageMimeType, Instructions)
VALUES VALUES
(@Name, @Description, @ProteinType, @CarbType, @RecipeUrl, @CreatedAt, @ImageUrl, @ImageData, @ImageMimeType); (@Name, @Description, @ProteinType, @CarbType, @RecipeUrl, @CreatedAt, @ImageUrl, @ImageData, @ImageMimeType, @Instructions);
SELECT LAST_INSERT_ID();"; SELECT LAST_INSERT_ID();";
} }
else else
@@ -270,7 +320,8 @@ namespace Aberwyn.Data
RecipeUrl = @RecipeUrl, RecipeUrl = @RecipeUrl,
ImageUrl = @ImageUrl, ImageUrl = @ImageUrl,
ImageData = @ImageData, ImageData = @ImageData,
ImageMimeType = @ImageMimeType ImageMimeType = @ImageMimeType,
Instructions = @Instructions
WHERE Id = @Id"; WHERE Id = @Id";
cmd.Parameters.AddWithValue("@Id", meal.Id); cmd.Parameters.AddWithValue("@Id", meal.Id);
} }
@@ -282,10 +333,9 @@ namespace Aberwyn.Data
cmd.Parameters.AddWithValue("@RecipeUrl", meal.RecipeUrl ?? ""); cmd.Parameters.AddWithValue("@RecipeUrl", meal.RecipeUrl ?? "");
cmd.Parameters.AddWithValue("@CreatedAt", meal.CreatedAt == default ? DateTime.Now : meal.CreatedAt); cmd.Parameters.AddWithValue("@CreatedAt", meal.CreatedAt == default ? DateTime.Now : meal.CreatedAt);
cmd.Parameters.AddWithValue("@ImageUrl", meal.ImageUrl ?? ""); cmd.Parameters.AddWithValue("@ImageUrl", meal.ImageUrl ?? "");
// ✨ Här är nyckeln
cmd.Parameters.AddWithValue("@ImageData", (object?)meal.ImageData ?? DBNull.Value); cmd.Parameters.AddWithValue("@ImageData", (object?)meal.ImageData ?? DBNull.Value);
cmd.Parameters.AddWithValue("@ImageMimeType", meal.ImageMimeType ?? (object)DBNull.Value); cmd.Parameters.AddWithValue("@ImageMimeType", meal.ImageMimeType ?? (object)DBNull.Value);
cmd.Parameters.AddWithValue("@Instructions", meal.Instructions ?? "");
if (meal.Id == 0) if (meal.Id == 0)
{ {
@@ -298,9 +348,6 @@ namespace Aberwyn.Data
} }
} }
public void UpdateWeeklyMenu(MenuViewModel menuData) public void UpdateWeeklyMenu(MenuViewModel menuData)
{ {
if (menuData == null || menuData.WeeklyMenus == null) if (menuData == null || menuData.WeeklyMenus == null)
@@ -413,6 +460,17 @@ namespace Aberwyn.Data
_ => throw new System.ArgumentException("Invalid day name") _ => throw new System.ArgumentException("Invalid day name")
}; };
} }
public void SaveOrUpdateMealWithIngredients(Meal meal)
{
// Spara/uppdatera måltid
SaveOrUpdateMeal(meal);
// Om måltiden har ingredienser spara dem
if (meal.Ingredients != null && meal.Ingredients.Count > 0)
{
SaveIngredients(meal.Id, meal.Ingredients);
}
}
public List<WeeklyMenu> GetMenuEntriesByDateRange(DateTime startDate, DateTime endDate) public List<WeeklyMenu> GetMenuEntriesByDateRange(DateTime startDate, DateTime endDate)
{ {
var results = new List<WeeklyMenu>(); var results = new List<WeeklyMenu>();

View File

@@ -46,7 +46,15 @@ namespace Aberwyn.Models
public DateTime CreatedAt { get; set; } public DateTime CreatedAt { get; set; }
public byte[] ImageData { get; set; } public byte[] ImageData { get; set; }
public string ImageMimeType { get; set; } // t.ex. "image/jpeg" public string ImageMimeType { get; set; } // t.ex. "image/jpeg"
public string Instructions { get; set; } // 👈 Tillagningstext
public List<Ingredient> Ingredients { get; set; } = new();
}
public class Ingredient
{
public int Id { get; set; }
public int MealId { get; set; }
public string Quantity { get; set; }
public string Item { get; set; }
} }
public class MealDto public class MealDto

View File

@@ -151,4 +151,8 @@
} }
} }
}); });
const modalImageInput = document.getElementById("modalImageInput");
</script> </script>

View File

@@ -1,4 +1,7 @@
@model Aberwyn.Models.Meal @model Aberwyn.Models.Meal
@using Microsoft.AspNetCore.Authorization
@inject Microsoft.AspNetCore.Http.IHttpContextAccessor HttpContextAccessor
@{ @{
ViewData["Title"] = Model.Name; ViewData["Title"] = Model.Name;
bool isEditing = (bool)(ViewData["IsEditing"] ?? false); bool isEditing = (bool)(ViewData["IsEditing"] ?? false);
@@ -14,13 +17,21 @@
imageSrc = "/images/placeholder-meal.jpg"; imageSrc = "/images/placeholder-meal.jpg";
} }
bool isChef = HttpContextAccessor.HttpContext.User.IsInRole("Chef");
isEditing = isEditing && isChef;
} }
<div class="meal-container"> <div class="meal-container">
<div class="meal-header"> <div class="meal-header">
<div id="imageDropArea" class="meal-image-wrapper" title="Klicka för att ladda upp bild"> <div class="meal-image-wrapper clickable" title="Bild på rätten">
<img id="imagePreview" src="@imageSrc" alt="@Model.Name" class="meal-image" /> <img id="imagePreview" src="@imageSrc" alt="@Model.Name" class="meal-image" />
</div> </div>
@if (isEditing)
{
<div style="margin-top: 0.5rem;">
<button type="button" class="btn-outline" onclick="openImageModal()">Byt bild</button>
</div>
}
<div class="meal-meta"> <div class="meal-meta">
<h1 class="meal-title">@Model.Name</h1> <h1 class="meal-title">@Model.Name</h1>
@@ -32,8 +43,6 @@
{ {
<form asp-action="SaveMeal" method="post" enctype="multipart/form-data"> <form asp-action="SaveMeal" method="post" enctype="multipart/form-data">
<input type="hidden" name="Id" value="@Model.Id" /> <input type="hidden" name="Id" value="@Model.Id" />
<input type="hidden" name="ExistingImageUrl" value="@Model.ImageUrl" />
<input type="file" id="ImageFile" name="ImageFile" accept="image/*" style="display: none;" /> <input type="file" id="ImageFile" name="ImageFile" accept="image/*" style="display: none;" />
<div class="form-group"> <div class="form-group">
@@ -61,6 +70,27 @@
<input type="url" name="RecipeUrl" value="@Model.RecipeUrl" class="form-control" /> <input type="url" name="RecipeUrl" value="@Model.RecipeUrl" class="form-control" />
</div> </div>
<div id="recipe-section">
<h2>Så här gör du</h2>
<div class="form-group">
<label for="Instructions">Tillagningsinstruktioner</label>
<textarea id="Instructions" name="Instructions">@Html.Raw(Model.Instructions)</textarea>
</div>
<div class="form-group">
<label>Ingredienser</label>
<div id="ingredients-list">
@for (int i = 0; i < Model.Ingredients.Count; i++)
{
<div class="ingredient-row">
<input type="text" name="Ingredients[@i].Quantity" placeholder="Mängd" value="@Model.Ingredients[i].Quantity" class="form-control ingredient-qty" />
<input type="text" name="Ingredients[@i].Item" placeholder="Ingrediens" value="@Model.Ingredients[i].Item" class="form-control ingredient-item" />
</div>
}
</div>
<button type="button" class="btn-outline" onclick="addIngredientRow()">+ Lägg till ingrediens</button>
</div>
</div>
<div class="buttons"> <div class="buttons">
<button type="submit" class="btn">Spara</button> <button type="submit" class="btn">Spara</button>
<button type="submit" formaction="@Url.Action("DeleteMeal", new { id = Model.Id })" formmethod="post" onclick="return confirm('Vill du verkligen ta bort denna måltid?');" class="btn-outline">Ta bort</button> <button type="submit" formaction="@Url.Action("DeleteMeal", new { id = Model.Id })" formmethod="post" onclick="return confirm('Vill du verkligen ta bort denna måltid?');" class="btn-outline">Ta bort</button>
@@ -87,100 +117,118 @@
} }
<div class="buttons"> <div class="buttons">
<button type="button" class="btn-outline" onclick="toggleRecipe()">Visa Tillagning</button> @if (isChef)
<a class="btn-outline" href="@Url.Action("View", new { id = Model.Id, edit = true })">Redigera</a> {
<a class="btn-outline" href="@Url.Action("View", new { id = Model.Id, edit = true })">Redigera</a>
}
<button type="button" class="btn-outline" onclick="toggleRecipe()">Recept</button>
</div>
<div id="recipe-section" style="display:none; margin-top:1.5rem;">
<h2>Så här gör du</h2>
@if (!string.IsNullOrWhiteSpace(Model.Instructions))
{
<div class="instructions-content">
@Html.Raw(Model.Instructions)
</div>
}
else
{
<p class="placeholder">Ingen instruktion har lagts till ännu.</p>
}
@if (Model.Ingredients != null && Model.Ingredients.Any())
{
<h3>Ingredienser</h3>
<ul>
@foreach (var ing in Model.Ingredients)
{
<li>@ing.Quantity @ing.Item</li>
}
</ul>
}
</div> </div>
} }
<div id="recipe-section" style="display:none; margin-top:1.5rem;">
<h2>Så här gör du</h2>
<p class="placeholder">(Tillagningsinstruktioner kommer snart...)</p>
@if (!string.IsNullOrEmpty(Model.RecipeUrl))
{
<p><a href="@Model.RecipeUrl" class="recipe-link" target="_blank">Gå till fullständigt recept</a></p>
}
</div>
</div> </div>
<!-- Modal --> <div id="imageModal" style="display:none; position:fixed; inset:0; background:rgba(0,0,0,0.6); z-index:1000;">
<div id="imageModal" style="display: none; position: fixed; inset: 0; background: rgba(0,0,0,0.6); z-index: 1000;"> <div style="background:white; margin:5% auto; padding:2rem; max-width:400px; border-radius:8px; text-align:center; position:relative;">
<div style="background: white; margin: 5% auto; padding: 2rem; max-width: 400px; border-radius: 8px; text-align: center; position: relative;">
<h3>Klistra in eller dra in bild</h3> <h3>Klistra in eller dra in bild</h3>
<div id="dropZone" style="border: 2px dashed #aaa; padding: 2rem; cursor: pointer;"> <div id="dropZone" style="border:2px dashed #aaa; padding:2rem; cursor:pointer;">
<p>Släpp bild här eller klistra in (Ctrl+V)</p> <p>Släpp bild här eller klistra in (Ctrl+V)</p>
</div> </div>
<button onclick="closeModal()" style="margin-top: 1rem;">Stäng</button> <input type="file" id="modalImageInput" accept="image/*" style="display:none;" />
<button type="button" onclick="document.getElementById('modalImageInput').click()">Välj bild från enhet</button>
<button onclick="closeImageModal()" style="margin-top:1rem;">Stäng</button>
</div> </div>
</div> </div>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/trumbowyg@2.25.1/dist/ui/trumbowyg.min.css">
<script src="https://cdn.jsdelivr.net/npm/jquery@3.7.1/dist/jquery.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/trumbowyg@2.25.1/dist/trumbowyg.min.js"></script>
<script> <script>
function toggleRecipe() { function toggleRecipe() {
const section = document.getElementById('recipe-section'); const section = document.getElementById('recipe-section');
section.style.display = section.style.display === 'none' ? 'block' : 'none'; if (!section) return;
}
const imageFileInput = document.getElementById("ImageFile"); if (section.style.display === 'none' || section.style.display === '') {
const imageDropArea = document.getElementById("imageDropArea"); section.style.display = 'block';
const imagePreview = document.getElementById("imagePreview"); } else {
const imageModal = document.getElementById("imageModal"); section.style.display = 'none';
const dropZone = document.getElementById("dropZone"); }
}
if (imageDropArea && @isEditing.ToString().ToLower()) { function openImageModal() {
imageDropArea.style.cursor = "pointer"; document.getElementById('imageModal').style.display = 'block';
imageDropArea.addEventListener("click", () => { }
imageModal.style.display = "block"; function closeImageModal() {
}); document.getElementById('imageModal').style.display = 'none';
function closeModal() {
imageModal.style.display = "none";
} }
function handleImage(file) { function handleImage(file) {
const reader = new FileReader(); const reader = new FileReader();
reader.onload = e => { reader.onload = e => {
imagePreview.src = e.target.result; document.getElementById("imagePreview").src = e.target.result;
const dt = new DataTransfer(); const dt = new DataTransfer();
dt.items.add(file); dt.items.add(file);
imageFileInput.files = dt.files; document.getElementById("ImageFile").files = dt.files;
}; };
reader.readAsDataURL(file); reader.readAsDataURL(file);
} }
document.getElementById('dropZone').addEventListener('dragover', e => {
dropZone.addEventListener("dragover", e => {
e.preventDefault(); e.preventDefault();
dropZone.style.borderColor = "#4A90E2"; e.currentTarget.style.borderColor = '#6a0dad';
}); });
document.getElementById('dropZone').addEventListener('dragleave', e => {
dropZone.addEventListener("dragleave", () => { e.currentTarget.style.borderColor = '#aaa';
dropZone.style.borderColor = "#aaa";
}); });
document.getElementById('dropZone').addEventListener('drop', e => {
dropZone.addEventListener("drop", e => {
e.preventDefault(); e.preventDefault();
dropZone.style.borderColor = "#aaa";
const file = e.dataTransfer.files[0]; const file = e.dataTransfer.files[0];
if (file && file.type.startsWith("image/")) { if (file && file.type.startsWith('image/')) {
handleImage(file); handleImage(file);
closeModal(); closeImageModal();
} }
}); });
document.getElementById('modalImageInput').addEventListener('change', e => {
document.addEventListener("paste", e => { const file = e.target.files[0];
if (imageModal.style.display !== "block") return; if (file && file.type.startsWith('image/')) {
for (const item of e.clipboardData.items) { handleImage(file);
if (item.type.startsWith("image/")) { closeImageModal();
const file = item.getAsFile(); }
handleImage(file); });
closeModal(); $(document).ready(function () {
} if ($('#Instructions').length > 0) {
$('#Instructions').trumbowyg({
autogrow: true,
btns: [['strong', 'em'], ['unorderedList', 'orderedList'], ['link'], ['removeformat']]
});
} }
}); });
}
</script> </script>
<style> <style>
.dragging {
outline: 2px dashed #6a0dad;
}
.meal-image-wrapper { .meal-image-wrapper {
position: relative; position: relative;
} }
@@ -274,4 +322,18 @@ if (imageDropArea && @isEditing.ToString().ToLower()) {
color: #999; color: #999;
font-style: italic; font-style: italic;
} }
.ingredient-row {
display: flex;
gap: 0.5rem;
margin-bottom: 0.5rem;
}
.ingredient-qty {
flex: 1 0 30%;
}
.ingredient-item {
flex: 2 0 60%;
}
</style> </style>

View File

@@ -269,3 +269,4 @@ h1 {
.top-buttons button:hover { .top-buttons button:hover {
background-color: rgba(0, 0, 0, 0.1); background-color: rgba(0, 0, 0, 0.1);
} }