Wish and veckomeny fixes
All checks were successful
continuous-integration/drone/push Build is passing
All checks were successful
continuous-integration/drone/push Build is passing
This commit is contained in:
@@ -230,6 +230,11 @@ namespace Aberwyn.Controllers
|
||||
WeekNumber = resolvedWeek,
|
||||
Year = resolvedYear,
|
||||
WeeklyMenus = menus,
|
||||
WishList = _context.MealWishes
|
||||
.Include(w => w.RequestedByUser)
|
||||
.Where(w => !w.IsArchived)
|
||||
.OrderByDescending(w => w.CreatedAt)
|
||||
.ToList()
|
||||
};
|
||||
|
||||
var recent = menuService
|
||||
@@ -501,6 +506,66 @@ namespace Aberwyn.Controllers
|
||||
|
||||
return View(new DoughPlan { AntalPizzor = 8, ViktPerPizza = 220 });
|
||||
}
|
||||
[HttpGet]
|
||||
public IActionResult GetRecentMenuEntries(int weeksBack = 4)
|
||||
{
|
||||
var today = DateTime.Today;
|
||||
|
||||
// Datum för måndag i nuvarande vecka
|
||||
int deltaToMonday = ((int)today.DayOfWeek + 6) % 7; // måndag=0, söndag=6
|
||||
var thisWeekMonday = today.AddDays(-deltaToMonday);
|
||||
|
||||
// Startdatum: måndag X veckor bak
|
||||
var startMonday = thisWeekMonday.AddDays(-7 * weeksBack);
|
||||
|
||||
// Slutdatum: söndag för förra veckan
|
||||
var endSunday = thisWeekMonday.AddDays(-1);
|
||||
|
||||
// Hämta alla veckomenyer inom intervallet
|
||||
var allMenus = _context.WeeklyMenus
|
||||
.Where(w => w.Date >= startMonday && w.Date <= endSunday)
|
||||
.OrderBy(w => w.Date)
|
||||
.ToList();
|
||||
|
||||
// Hämta alla relevanta meal IDs
|
||||
var mealIds = allMenus
|
||||
.SelectMany(w => new[] { w.BreakfastMealId, w.LunchMealId, w.DinnerMealId })
|
||||
.Where(id => id.HasValue)
|
||||
.Select(id => id!.Value)
|
||||
.Distinct()
|
||||
.ToList();
|
||||
|
||||
var meals = _context.Meals
|
||||
.Where(m => mealIds.Contains(m.Id))
|
||||
.ToDictionary(m => m.Id, m => m.Name);
|
||||
|
||||
// Skapa entries
|
||||
var entries = allMenus
|
||||
.Select(w => new RecentMenuEntry
|
||||
{
|
||||
Date = w.Date,
|
||||
WeekNumber = ISOWeek.GetWeekOfYear(w.Date),
|
||||
Year = w.Date.Year,
|
||||
BreakfastMealName = w.BreakfastMealId.HasValue && meals.ContainsKey(w.BreakfastMealId.Value)
|
||||
? meals[w.BreakfastMealId.Value]
|
||||
: "—",
|
||||
LunchMealName = w.LunchMealId.HasValue && meals.ContainsKey(w.LunchMealId.Value)
|
||||
? meals[w.LunchMealId.Value]
|
||||
: "—",
|
||||
DinnerMealName = w.DinnerMealId.HasValue && meals.ContainsKey(w.DinnerMealId.Value)
|
||||
? meals[w.DinnerMealId.Value]
|
||||
: "—"
|
||||
})
|
||||
.OrderBy(e => e.Date)
|
||||
.ToList();
|
||||
|
||||
return Json(entries);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
// MealMenuApiController.cs
|
||||
using Aberwyn.Models;
|
||||
using Aberwyn.Data;
|
||||
using Aberwyn.Models;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using System.Net.Http.Headers;
|
||||
@@ -104,8 +105,12 @@ namespace Aberwyn.Controllers
|
||||
return StatusCode(500, "Failed to add meal.");
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
#region Skolmat
|
||||
[HttpGet("skolmat")]
|
||||
[HttpGet("skolmat")]
|
||||
public async Task<IActionResult> GetSkolmat(int week, [FromQuery] string sensor = "sensor.engelbrektsskolan")
|
||||
{
|
||||
var token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiI3M2Q5ODIyYzU4ZWI0MjM4OWEyMGQ2MWQ2MWVhOWYzYyIsImlhdCI6MTc0OTE1MzY1MCwiZXhwIjoyMDY0NTEzNjUwfQ.8C_dKm7P1BbFVJKc_wT76YnQqiZxkP9EzrsLbfD0Ml8";
|
||||
|
||||
124
Aberwyn/Controllers/MealWishController.cs
Normal file
124
Aberwyn/Controllers/MealWishController.cs
Normal file
@@ -0,0 +1,124 @@
|
||||
using Aberwyn.Data;
|
||||
using Aberwyn.Models; // Byt till din namespace
|
||||
using Humanizer;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using System;
|
||||
using System.Security.Claims;
|
||||
|
||||
|
||||
[ApiController]
|
||||
[Route("api/[controller]")]
|
||||
public class MealWishController : ControllerBase
|
||||
{
|
||||
private readonly ApplicationDbContext _context;
|
||||
|
||||
public MealWishController(ApplicationDbContext context)
|
||||
{
|
||||
_context = context;
|
||||
}
|
||||
|
||||
// 1. Skapa en ny önskan
|
||||
[HttpPost("create")]
|
||||
public async Task<ActionResult<MealWishDto>> Create([FromBody] CreateMealWishDto dto)
|
||||
{
|
||||
var wish = new MealWish
|
||||
{
|
||||
Name = dto.Name,
|
||||
Recipe = dto.Recipe
|
||||
};
|
||||
|
||||
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier) ?? "anonymous";
|
||||
wish.RequestedByUserId = userId;
|
||||
wish.CreatedAt = DateTime.UtcNow;
|
||||
wish.IsArchived = false;
|
||||
wish.IsImported = false;
|
||||
|
||||
_context.MealWishes.Add(wish);
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
return Ok(MealWishDto.FromEntity(wish)); // ✅ returnera DTO
|
||||
}
|
||||
|
||||
// 2. Hämta alla önskningar (admin)
|
||||
[HttpGet("all")]
|
||||
public async Task<IActionResult> GetAll(bool includeArchived = false)
|
||||
{
|
||||
var wishes = await _context.MealWishes
|
||||
.Include(w => w.RequestedByUser) // hämta användaren
|
||||
.Where(w => includeArchived || !w.IsArchived)
|
||||
.OrderByDescending(w => w.CreatedAt)
|
||||
.Select(w => new {
|
||||
w.Id,
|
||||
w.Name,
|
||||
w.Recipe,
|
||||
RequestedByUserName = w.RequestedByUser != null ? w.RequestedByUser.UserName : "Okänd",
|
||||
CreatedAt = w.CreatedAt.ToLocalTime().ToString("yyyy-MM-dd HH:mm"),
|
||||
w.IsArchived,
|
||||
w.IsImported
|
||||
})
|
||||
.ToListAsync();
|
||||
|
||||
return Ok(wishes);
|
||||
}
|
||||
|
||||
|
||||
// 3. Avfärda / arkivera
|
||||
[HttpPost("{id}/archive")]
|
||||
public async Task<IActionResult> Archive(int id)
|
||||
{
|
||||
var wish = await _context.MealWishes.FindAsync(id);
|
||||
if (wish == null) return NotFound();
|
||||
|
||||
wish.IsArchived = true;
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
return Ok(wish);
|
||||
}
|
||||
|
||||
// 4. Importera till Meals
|
||||
[HttpPost("{id}/import")]
|
||||
public async Task<IActionResult> Import(int id)
|
||||
{
|
||||
var wish = await _context.MealWishes.FindAsync(id);
|
||||
if (wish == null) return NotFound();
|
||||
|
||||
if (wish.IsImported)
|
||||
return BadRequest("Denna rätt har redan importerats.");
|
||||
|
||||
// Skapa en ny Meal
|
||||
var meal = new Meal
|
||||
{
|
||||
Name = wish.Name,
|
||||
Instructions = wish.Recipe,
|
||||
IsAvailable = true,
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
IsPublished = false
|
||||
};
|
||||
|
||||
_context.Meals.Add(meal);
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
wish.LinkedMealId = meal.Id;
|
||||
wish.IsImported = true;
|
||||
wish.IsArchived = true;
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
return Ok(wish);
|
||||
}
|
||||
|
||||
[HttpGet("search")]
|
||||
public async Task<IActionResult> Search([FromQuery] string name)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(name)) return BadRequest();
|
||||
|
||||
var meals = await _context.Meals
|
||||
.Where(m => m.Name.Contains(name))
|
||||
.OrderBy(m => m.Name)
|
||||
.Take(10)
|
||||
.Select(m => new { m.Id, m.Name })
|
||||
.ToListAsync();
|
||||
|
||||
return Ok(meals);
|
||||
}
|
||||
}
|
||||
@@ -37,6 +37,7 @@ namespace Aberwyn.Data
|
||||
);
|
||||
builder.Entity<TorrentItem>()
|
||||
.OwnsOne(t => t.Metadata);
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -66,6 +67,8 @@ namespace Aberwyn.Data
|
||||
public DbSet<RssFeed> RssFeeds { get; set; }
|
||||
public DbSet<DownloadRule> DownloadRules { get; set; }
|
||||
public DbSet<UserTorrentSeen> UserTorrentSeen { get; set; }
|
||||
public DbSet<MealWish> MealWishes { get; set; }
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -435,6 +435,8 @@ public List<WeeklyMenu> GetWeeklyMenu(int weekNumber, int year)
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
|
||||
public List<MealCategory> GetMealCategories()
|
||||
{
|
||||
return _context.MealCategories.OrderBy(c => c.DisplayOrder).ToList();
|
||||
|
||||
1349
Aberwyn/Migrations/20250921133610_AddMealWish.Designer.cs
generated
Normal file
1349
Aberwyn/Migrations/20250921133610_AddMealWish.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
53
Aberwyn/Migrations/20250921133610_AddMealWish.cs
Normal file
53
Aberwyn/Migrations/20250921133610_AddMealWish.cs
Normal file
@@ -0,0 +1,53 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Metadata;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Aberwyn.Migrations
|
||||
{
|
||||
public partial class AddMealWish : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "MealWishes",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "int", nullable: false)
|
||||
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
|
||||
Name = table.Column<string>(type: "longtext", nullable: false)
|
||||
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||
Recipe = table.Column<string>(type: "longtext", nullable: true)
|
||||
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||
LinkedMealId = table.Column<int>(type: "int", nullable: true),
|
||||
RequestedByUserId = table.Column<string>(type: "longtext", nullable: false)
|
||||
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||
CreatedAt = table.Column<DateTime>(type: "datetime(6)", nullable: false),
|
||||
IsArchived = table.Column<bool>(type: "tinyint(1)", nullable: false),
|
||||
IsImported = table.Column<bool>(type: "tinyint(1)", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_MealWishes", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_MealWishes_Meals_LinkedMealId",
|
||||
column: x => x.LinkedMealId,
|
||||
principalTable: "Meals",
|
||||
principalColumn: "Id");
|
||||
})
|
||||
.Annotation("MySql:CharSet", "utf8mb4");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_MealWishes_LinkedMealId",
|
||||
table: "MealWishes",
|
||||
column: "LinkedMealId");
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "MealWishes");
|
||||
}
|
||||
}
|
||||
}
|
||||
1359
Aberwyn/Migrations/20250921161413_AddMealWishUserRelation.Designer.cs
generated
Normal file
1359
Aberwyn/Migrations/20250921161413_AddMealWishUserRelation.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
56
Aberwyn/Migrations/20250921161413_AddMealWishUserRelation.cs
Normal file
56
Aberwyn/Migrations/20250921161413_AddMealWishUserRelation.cs
Normal file
@@ -0,0 +1,56 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Aberwyn.Migrations
|
||||
{
|
||||
public partial class AddMealWishUserRelation : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "RequestedByUserId",
|
||||
table: "MealWishes",
|
||||
type: "varchar(255)",
|
||||
nullable: false,
|
||||
oldClrType: typeof(string),
|
||||
oldType: "longtext")
|
||||
.Annotation("MySql:CharSet", "utf8mb4")
|
||||
.OldAnnotation("MySql:CharSet", "utf8mb4");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_MealWishes_RequestedByUserId",
|
||||
table: "MealWishes",
|
||||
column: "RequestedByUserId");
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_MealWishes_AspNetUsers_RequestedByUserId",
|
||||
table: "MealWishes",
|
||||
column: "RequestedByUserId",
|
||||
principalTable: "AspNetUsers",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_MealWishes_AspNetUsers_RequestedByUserId",
|
||||
table: "MealWishes");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_MealWishes_RequestedByUserId",
|
||||
table: "MealWishes");
|
||||
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "RequestedByUserId",
|
||||
table: "MealWishes",
|
||||
type: "longtext",
|
||||
nullable: false,
|
||||
oldClrType: typeof(string),
|
||||
oldType: "varchar(255)")
|
||||
.Annotation("MySql:CharSet", "utf8mb4")
|
||||
.OldAnnotation("MySql:CharSet", "utf8mb4");
|
||||
}
|
||||
}
|
||||
}
|
||||
1362
Aberwyn/Migrations/20250921170530_AddDateToWeeklyMenu.Designer.cs
generated
Normal file
1362
Aberwyn/Migrations/20250921170530_AddDateToWeeklyMenu.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
27
Aberwyn/Migrations/20250921170530_AddDateToWeeklyMenu.cs
Normal file
27
Aberwyn/Migrations/20250921170530_AddDateToWeeklyMenu.cs
Normal file
@@ -0,0 +1,27 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Aberwyn.Migrations
|
||||
{
|
||||
public partial class AddDateToWeeklyMenu : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<DateTime>(
|
||||
name: "Date",
|
||||
table: "WeeklyMenu",
|
||||
type: "datetime(6)",
|
||||
nullable: false,
|
||||
defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified));
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "Date",
|
||||
table: "WeeklyMenu");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -484,6 +484,44 @@ namespace Aberwyn.Migrations
|
||||
b.ToTable("MealRatings");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Aberwyn.Models.MealWish", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.Property<bool>("IsArchived")
|
||||
.HasColumnType("tinyint(1)");
|
||||
|
||||
b.Property<bool>("IsImported")
|
||||
.HasColumnType("tinyint(1)");
|
||||
|
||||
b.Property<int?>("LinkedMealId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("longtext");
|
||||
|
||||
b.Property<string>("Recipe")
|
||||
.HasColumnType("longtext");
|
||||
|
||||
b.Property<string>("RequestedByUserId")
|
||||
.IsRequired()
|
||||
.HasColumnType("varchar(255)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("LinkedMealId");
|
||||
|
||||
b.HasIndex("RequestedByUserId");
|
||||
|
||||
b.ToTable("MealWishes");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Aberwyn.Models.PizzaOrder", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
@@ -719,6 +757,9 @@ namespace Aberwyn.Migrations
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.Property<DateTime>("Date")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.Property<int>("DayOfWeek")
|
||||
.HasColumnType("int");
|
||||
|
||||
@@ -1097,6 +1138,23 @@ namespace Aberwyn.Migrations
|
||||
b.Navigation("Meal");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Aberwyn.Models.MealWish", b =>
|
||||
{
|
||||
b.HasOne("Aberwyn.Models.Meal", "LinkedMeal")
|
||||
.WithMany()
|
||||
.HasForeignKey("LinkedMealId");
|
||||
|
||||
b.HasOne("Aberwyn.Models.ApplicationUser", "RequestedByUser")
|
||||
.WithMany()
|
||||
.HasForeignKey("RequestedByUserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("LinkedMeal");
|
||||
|
||||
b.Navigation("RequestedByUser");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Aberwyn.Models.PushSubscriber", b =>
|
||||
{
|
||||
b.HasOne("Aberwyn.Models.PizzaOrder", "PizzaOrder")
|
||||
|
||||
@@ -12,6 +12,7 @@ namespace Aberwyn.Models
|
||||
public List<WeeklyMenu> WeeklyMenus { get; set; } // List of weekly menu entries
|
||||
public int WeekNumber { get; set; } // Week number for the menu
|
||||
public int Year { get; set; } // Year for the menu
|
||||
|
||||
}
|
||||
public class WeeklyMenuDto
|
||||
{
|
||||
@@ -40,6 +41,7 @@ public class WeeklyMenu
|
||||
public int? LunchMealId { get; set; }
|
||||
public int? DinnerMealId { get; set; }
|
||||
public string? Cook { get; set; }
|
||||
public DateTime Date { get; set; }
|
||||
public int WeekNumber { get; set; }
|
||||
public int Year { get; set; }
|
||||
public DateTime CreatedAt { get; set; }
|
||||
@@ -59,6 +61,8 @@ public class WeeklyMenu
|
||||
public class RecentMenuEntry
|
||||
{
|
||||
public DateTime Date { get; set; }
|
||||
public int WeekNumber { get; set; } // Lägg till vecka
|
||||
public int Year { get; set; } // Lägg till år
|
||||
public string BreakfastMealName { get; set; }
|
||||
public string LunchMealName { get; set; }
|
||||
public string DinnerMealName { get; set; }
|
||||
@@ -176,6 +180,51 @@ public class WeeklyMenu
|
||||
|
||||
public List<Meal> Meals { get; set; } = new();
|
||||
}
|
||||
public class MealWish
|
||||
{
|
||||
[Key]
|
||||
public int Id { get; set; }
|
||||
[Required]
|
||||
public string Name { get; set; }
|
||||
public string? Recipe { get; set; }
|
||||
public int? LinkedMealId { get; set; }
|
||||
[ForeignKey("LinkedMealId")]
|
||||
public Meal? LinkedMeal { get; set; }
|
||||
[Required]
|
||||
public string RequestedByUserId { get; set; }
|
||||
[ForeignKey(nameof(RequestedByUserId))]
|
||||
public ApplicationUser? RequestedByUser { get; set; }
|
||||
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||
public bool IsArchived { get; set; } = false;
|
||||
public bool IsImported { get; set; } = false;
|
||||
}
|
||||
|
||||
|
||||
public class CreateMealWishDto
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public string? Recipe { get; set; }
|
||||
}
|
||||
public class MealWishDto
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string Name { get; set; } = "";
|
||||
public string? Recipe { get; set; }
|
||||
public bool IsArchived { get; set; }
|
||||
public bool IsImported { get; set; }
|
||||
|
||||
public static MealWishDto FromEntity(MealWish wish)
|
||||
{
|
||||
return new MealWishDto
|
||||
{
|
||||
Id = wish.Id,
|
||||
Name = wish.Name,
|
||||
Recipe = wish.Recipe,
|
||||
IsArchived = wish.IsArchived,
|
||||
IsImported = wish.IsImported
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public class MealRating
|
||||
{
|
||||
|
||||
@@ -14,6 +14,7 @@ namespace Aberwyn.Models
|
||||
public List<RecentMenuEntry> RecentEntries { get; set; } = new();
|
||||
public List<WeeklyMenu> WeeklyMenus { get; set; } = new();
|
||||
public List<UserModel> AvailableCooks { get; set; } = new();
|
||||
public List<MealWish> WishList { get; set; } = new();
|
||||
|
||||
public List<WeeklyMenuViewModel> PreviousWeeks { get; set; } = new();
|
||||
public class RecentMenuEntry
|
||||
@@ -37,6 +38,8 @@ namespace Aberwyn.Models
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,153 +1,278 @@
|
||||
@using System.Globalization
|
||||
@model Aberwyn.Models.WeeklyMenuViewModel
|
||||
|
||||
@{
|
||||
ViewData["Title"] = "Veckomeny";
|
||||
var days = new[] { "Måndag", "Tisdag", "Onsdag", "Torsdag", "Fredag", "Lördag", "Söndag" };
|
||||
}
|
||||
<html lang="sv" ng-app="app">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Veckomeny</title>
|
||||
<link rel="stylesheet" href="~/css/Veckomeny.css" asp-append-version="true" />
|
||||
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
|
||||
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.8.2/angular.min.js"></script>
|
||||
<script>
|
||||
window.knownMeals = @Html.Raw(System.Text.Json.JsonSerializer.Serialize(
|
||||
(ViewBag.AvailableMeals as List<Aberwyn.Models.Meal>)?.Select(m => m.Name.Trim()).ToList() ?? new List<string>()));
|
||||
</script>
|
||||
</head>
|
||||
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
|
||||
<script>
|
||||
window.knownMeals = @Html.Raw(System.Text.Json.JsonSerializer.Serialize(
|
||||
(ViewBag.AvailableMeals as List<Aberwyn.Models.Meal>)?.Select(m => m.Name.Trim()).ToList() ?? new List<string>()));
|
||||
</script>
|
||||
<body x-data="recentHistory()" x-init="init()">
|
||||
|
||||
<div class="weekly-menu-wrapper"
|
||||
x-data="{
|
||||
highlightNew(event) {
|
||||
const input = event.target;
|
||||
const val = input.value?.trim().toLowerCase();
|
||||
input.classList.remove('new-entry', 'existing-entry');
|
||||
<div class="menu-wishlist-wrapper">
|
||||
|
||||
if (!val) return;
|
||||
<!-- Veckomenyn -->
|
||||
<section class="weekly-editor">
|
||||
<h1>Veckomeny - Vecka @Model.WeekNumber</h1>
|
||||
<div class="week-nav">
|
||||
<a asp-action="Veckomeny" asp-route-week="@(Model.WeekNumber - 1)" asp-route-year="@Model.Year">← Föregående vecka</a>
|
||||
<span>Vecka @Model.WeekNumber</span>
|
||||
<a asp-action="Veckomeny" asp-route-week="@(Model.WeekNumber + 1)" asp-route-year="@Model.Year">Nästa vecka →</a>
|
||||
</div>
|
||||
|
||||
<form method="post" asp-action="SaveVeckomeny">
|
||||
<input type="hidden" name="week" value="@Model.WeekNumber" />
|
||||
<input type="hidden" name="year" value="@Model.Year" />
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="day-cell">Dag</th>
|
||||
<th>Måltider</th>
|
||||
<th>Kock</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@for (int i = 0; i < 7; i++)
|
||||
{
|
||||
var dinnerEntry = Model.GetMealEntry(i, "Middag");
|
||||
var lunchEntry = Model.GetMealEntry(i, "Lunch");
|
||||
var breakfastEntry = Model.GetMealEntry(i, "Frukost");
|
||||
<tbody x-data="{ showExtra: false }">
|
||||
<tr>
|
||||
<td class="day-cell">
|
||||
<button type="button" x-on:click="showExtra = !showExtra">
|
||||
<i :class="showExtra ? 'fa-solid fa-chevron-right rotate-chevron open' : 'fa-solid fa-chevron-right rotate-chevron'"></i>
|
||||
</button>
|
||||
@days[i]
|
||||
</td>
|
||||
<td class="meal-cell">
|
||||
<div class="meal-entry-group">
|
||||
<div class="meal-input-group">
|
||||
<input type="text"
|
||||
name="Meal[@i][Middag]"
|
||||
value="@dinnerEntry?.DinnerMealName"
|
||||
placeholder="Lägg till middag..."
|
||||
list="meals-list"
|
||||
class="meal-input"
|
||||
:tabindex="showExtra ? 0 : -1"
|
||||
x-on:input="highlightFromCurrentWeek()" />
|
||||
<button type="button" class="delete-btn" title="Rensa middag"
|
||||
onclick="this.previousElementSibling.value=''; highlightMeal('');">
|
||||
<i class="fa-solid fa-xmark"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="cook-cell">
|
||||
<select name="Cook[@i]" :tabindex="showExtra ? 0 : -1" class="meal-input">
|
||||
<option value="">Välj kock</option>
|
||||
@foreach (var user in Model.AvailableCooks)
|
||||
{
|
||||
var selected = Model.WeeklyMenus.FirstOrDefault(m => m.DayOfWeek == i + 1)?.Cook == user.Username;
|
||||
<option value="@user.Username" selected="@selected">@user.Name</option>
|
||||
}
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="extra-row" x-show="showExtra" x-transition x-cloak>
|
||||
<td></td>
|
||||
<td colspan="2">
|
||||
<div class="extra-meals">
|
||||
<div class="meal-input-group">
|
||||
<label>Frukost:</label>
|
||||
<input type="text"
|
||||
name="Meal[@i][Frukost]"
|
||||
value="@breakfastEntry?.BreakfastMealName"
|
||||
placeholder="Lägg till frukost..."
|
||||
list="meals-list"
|
||||
class="meal-input"
|
||||
x-on:input="highlightFromCurrentWeek()" />
|
||||
<button type="button" class="delete-btn" title="Rensa frukost"
|
||||
onclick="this.previousElementSibling.value=''; highlightMeal('');">
|
||||
<i class="fa-solid fa-xmark"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="meal-input-group">
|
||||
<label>Lunch:</label>
|
||||
<input type="text"
|
||||
name="Meal[@i][Lunch]"
|
||||
value="@lunchEntry?.LunchMealName"
|
||||
placeholder="Lägg till lunch..."
|
||||
list="meals-list"
|
||||
class="meal-input"
|
||||
x-on:input="highlightFromCurrentWeek()" />
|
||||
<button type="button" class="delete-btn" title="Rensa lunch"
|
||||
onclick="this.previousElementSibling.value=''; highlightMeal('');">
|
||||
<i class="fa-solid fa-xmark"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<!-- Wishlist -->
|
||||
<aside class="wishlist" ng-controller="WishListCtrl as vm" ng-init="vm.loadWishes()">
|
||||
<h2>Önskemåltider</h2>
|
||||
<div class="wishlist-grid">
|
||||
<div class="wishlist-card" ng-repeat="wish in vm.WishList track by wish.Id">
|
||||
<div class="wishlist-header">
|
||||
<strong>{{wish.Name}}</strong>
|
||||
<span class="wishlist-meta">
|
||||
av {{wish.RequestedByUserName}} • {{wish.CreatedAt}}
|
||||
</span>
|
||||
</div>
|
||||
<div class="wishlist-body" ng-if="wish.Recipe">
|
||||
<p>{{wish.Recipe | limitTo:100}}{{wish.Recipe.length > 100 ? '...' : ''}}</p>
|
||||
</div>
|
||||
<div class="wishlist-actions">
|
||||
<button type="button" ng-click="vm.importWish(wish.Id)" class="btn import">
|
||||
<i class="fa-solid fa-plus"></i> Importera
|
||||
</button>
|
||||
<button type="button" ng-click="vm.archiveWish(wish.Id)" class="btn archive">
|
||||
<i class="fa-solid fa-box-archive"></i> Arkivera
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p ng-if="!vm.WishList.length">Inga önskemål just nu.</p>
|
||||
</aside>
|
||||
|
||||
const isKnown = window.knownMeals.some(name => name.toLowerCase() === val);
|
||||
input.classList.add(isKnown ? 'existing-entry' : 'new-entry');
|
||||
}
|
||||
}">
|
||||
<section class="weekly-editor">
|
||||
<h1>Veckomeny - Vecka @Model.WeekNumber</h1>
|
||||
<div class="week-nav">
|
||||
<a asp-action="Veckomeny" asp-route-week="@(Model.WeekNumber - 1)" asp-route-year="@Model.Year">← Föregående vecka</a>
|
||||
<span>Vecka @Model.WeekNumber</span>
|
||||
<a asp-action="Veckomeny" asp-route-week="@(Model.WeekNumber + 1)" asp-route-year="@Model.Year">Nästa vecka →</a>
|
||||
</div>
|
||||
|
||||
<form method="post" asp-action="SaveVeckomeny">
|
||||
<input type="hidden" name="week" value="@Model.WeekNumber" />
|
||||
<input type="hidden" name="year" value="@Model.Year" />
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="day-cell">Dag</th>
|
||||
<th>Måltider</th>
|
||||
<th>Kock</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@for (int i = 0; i < 7; i++) {
|
||||
var dinnerEntry = Model.GetMealEntry(i, "Middag");
|
||||
var lunchEntry = Model.GetMealEntry(i, "Lunch");
|
||||
var breakfastEntry = Model.GetMealEntry(i, "Frukost");
|
||||
<tbody x-data="{ showExtra: false }">
|
||||
<tr>
|
||||
<td class="day-cell">
|
||||
<button type="button" x-on:click="showExtra = !showExtra">
|
||||
<i :class="showExtra ? 'fa-solid fa-chevron-right rotate-chevron open' : 'fa-solid fa-chevron-right rotate-chevron'"></i>
|
||||
</button>
|
||||
@days[i]
|
||||
</td>
|
||||
<td class="meal-cell">
|
||||
<div class="meal-entry-group">
|
||||
<div class="meal-input-group">
|
||||
<input type="text" name="Meal[@i][Middag]" value="@dinnerEntry?.DinnerMealName" placeholder="Lägg till middag..." list="meals-list" class="meal-input" :tabindex="showExtra ? 0 : -1" x-on:input="highlightNew($event)" />
|
||||
<button type="button" class="delete-btn" title="Rensa middag" onclick="this.previousElementSibling.value='';">
|
||||
<i class="fa-solid fa-xmark"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="cook-cell">
|
||||
<select name="Cook[@i]" :tabindex="showExtra ? 0 : -1" class="meal-input">
|
||||
<option value="">Välj kock</option>
|
||||
@foreach(var user in Model.AvailableCooks) {
|
||||
var selected = Model.WeeklyMenus.FirstOrDefault(m => m.DayOfWeek == i+1)?.Cook == user.Username;
|
||||
<option value="@user.Username" selected="@selected">@user.Name</option>
|
||||
}
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="extra-row" x-show="showExtra" x-transition x-cloak>
|
||||
<td></td>
|
||||
<td colspan="2">
|
||||
<div class="extra-meals">
|
||||
<div class="meal-input-group">
|
||||
<label>Frukost:</label>
|
||||
<input type="text" name="Meal[@i][Frukost]" value="@breakfastEntry?.BreakfastMealName" placeholder="Lägg till frukost..." list="meals-list" class="meal-input" x-on:input="highlightNew($event)" />
|
||||
<button type="button" class="delete-btn" title="Rensa frukost" onclick="this.previousElementSibling.value='';">
|
||||
<i class="fa-solid fa-xmark"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="meal-input-group">
|
||||
<label>Lunch:</label>
|
||||
<input type="text" name="Meal[@i][Lunch]" value="@lunchEntry?.LunchMealName" placeholder="Lägg till lunch..." list="meals-list" class="meal-input" x-on:input="highlightNew($event)" />
|
||||
<button type="button" class="delete-btn" title="Rensa lunch" onclick="this.previousElementSibling.value='';">
|
||||
<i class="fa-solid fa-xmark"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
<!-- Översikt senaste 4 veckor, full width -->
|
||||
<aside class="recent-history-fullwidth">
|
||||
<h2>Översikt senaste 4 veckor (middag)</h2>
|
||||
<div class="recent-table-wrapper">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Vecka</th>
|
||||
@foreach (var d in days)
|
||||
{
|
||||
<th>@d</th>
|
||||
}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<template x-for="week in weeks" :key="week.weekNumber">
|
||||
<tr>
|
||||
<td x-text="week.weekNumber"></td>
|
||||
<template x-for="day in week.days">
|
||||
<td x-text="day.dinner" :class="day.highlight ? 'highlight' : ''"></td>
|
||||
</template>
|
||||
</tr>
|
||||
</template>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</aside>
|
||||
<script>
|
||||
function recentHistory() {
|
||||
return {
|
||||
weeks: [],
|
||||
init() {
|
||||
fetch('/FoodMenu/GetRecentMenuEntries?weeksBack=4')
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
const grouped = {};
|
||||
data.forEach(e => {
|
||||
const dt = new Date(e.Date);
|
||||
const weekNo = getISOWeek(dt);
|
||||
const dayIdx = dt.getDay() === 0 ? 6 : dt.getDay() - 1;
|
||||
const key = `W${weekNo}`;
|
||||
if (!grouped[key]) grouped[key] = Array(7).fill().map(() => ({ dinner: '—', highlight: false }));
|
||||
grouped[key][dayIdx] = { dinner: e.DinnerMealName?.trim() || '—', highlight: false };
|
||||
});
|
||||
|
||||
<div class="add-meal-wrapper">
|
||||
<button type="submit" class="save-menu-btn">Spara veckomeny</button>
|
||||
</div>
|
||||
this.weeks = Object.keys(grouped)
|
||||
.sort((a,b) => b.localeCompare(a))
|
||||
.map(weekKey => ({ weekNumber: parseInt(weekKey.replace('W','')), days: grouped[weekKey] }));
|
||||
|
||||
<aside class="recent-history">
|
||||
<h2>Översikt senaste 4 veckor</h2>
|
||||
@{
|
||||
var lastWeeks = Enumerable.Range(1, 4)
|
||||
.Select(i => DateTime.Now.AddDays(-7 * i))
|
||||
.Select(dt => new { Year = dt.Year, Week = ISOWeek.GetWeekOfYear(dt) })
|
||||
.Distinct().ToList();
|
||||
}
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Vecka</th>
|
||||
@foreach (var d in days) {
|
||||
<th>@d</th>
|
||||
}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var w in lastWeeks) {
|
||||
<tr>
|
||||
<td>@w.Week</td>
|
||||
@for (int idx = 1; idx <= 7; idx++) {
|
||||
var entry = Model.RecentEntries?.FirstOrDefault(e =>
|
||||
e.Date.Year == w.Year &&
|
||||
ISOWeek.GetWeekOfYear(e.Date) == w.Week &&
|
||||
((int)e.Date.DayOfWeek == (idx % 7)));
|
||||
<td>@(entry?.DinnerMealName ?? "—")</td>
|
||||
this.highlightFromCurrentWeek();
|
||||
});
|
||||
|
||||
function getISOWeek(date) {
|
||||
const tmpDate = new Date(date.getTime());
|
||||
tmpDate.setHours(0,0,0,0);
|
||||
tmpDate.setDate(tmpDate.getDate() + 4 - (tmpDate.getDay()||7));
|
||||
const yearStart = new Date(tmpDate.getFullYear(),0,1);
|
||||
return Math.ceil((((tmpDate - yearStart) / 86400000) + 1)/7);
|
||||
}
|
||||
</tr>
|
||||
},
|
||||
|
||||
highlightMeal(inputValue) {
|
||||
const val = inputValue?.trim().toLowerCase();
|
||||
this.weeks.forEach(week => {
|
||||
week.days.forEach(day => {
|
||||
day.highlight = val && day.dinner?.toLowerCase() === val;
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
highlightFromCurrentWeek() {
|
||||
// hämta alla inputs som har något värde
|
||||
const values = Array.from(document.querySelectorAll(".meal-input"))
|
||||
.map(i => i.value?.trim().toLowerCase())
|
||||
.filter(v => v);
|
||||
|
||||
this.weeks.forEach(week => {
|
||||
week.days.forEach(day => {
|
||||
day.highlight = values.includes(day.dinner?.toLowerCase());
|
||||
});
|
||||
});
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</aside>
|
||||
|
||||
<datalist id="meals-list">
|
||||
@foreach (var meal in (List<Meal>)ViewBag.AvailableMeals)
|
||||
{
|
||||
<option value="@meal.Name">@meal.Name</option>
|
||||
}
|
||||
</datalist>
|
||||
</form>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
}
|
||||
angular.module('app', [])
|
||||
.controller('WishListCtrl', ['$http', function($http){
|
||||
var vm = this;
|
||||
vm.WishList = [];
|
||||
|
||||
vm.loadWishes = function(){
|
||||
$http.get('/api/MealWish/all')
|
||||
.then(function(res){
|
||||
vm.WishList = res.data;
|
||||
}, function(err){
|
||||
console.error(err);
|
||||
});
|
||||
};
|
||||
|
||||
vm.archiveWish = function(id){
|
||||
$http.post('/api/MealWish/' + id + '/archive')
|
||||
.then(function(){
|
||||
vm.WishList = vm.WishList.filter(function(w){ return w.Id !== id; });
|
||||
}, function(err){
|
||||
console.error(err);
|
||||
});
|
||||
};
|
||||
|
||||
vm.importWish = function(id){
|
||||
$http.post('/api/MealWish/' + id + '/import')
|
||||
.then(function(){
|
||||
vm.WishList = vm.WishList.filter(function(w){ return w.Id !== id; });
|
||||
}, function(err){
|
||||
console.error(err);
|
||||
});
|
||||
};
|
||||
}]);
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -65,7 +65,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!--
|
||||
|
||||
<div class="school-meals card-view">
|
||||
<h2>Skolmat</h2>
|
||||
|
||||
@@ -86,7 +86,33 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
-->
|
||||
|
||||
<!-- Floating button för att öppna popup -->
|
||||
<button class="floating-btn" ng-click="toggleWishFlyout()" ng-if="isLoggedIn">+</button>
|
||||
<!-- Popup för önskad rätt -->
|
||||
<div class="wish-flyout" ng-show="showWishFlyout">
|
||||
<form ng-submit="submitWish()">
|
||||
<h3>Önska mat</h3>
|
||||
<label for="wishName">Namn</label>
|
||||
<input type="text"
|
||||
ng-model="newWishName"
|
||||
placeholder="Skriv eller sök rätt"
|
||||
ng-change="checkExistingMeal()"
|
||||
autocomplete="off">
|
||||
|
||||
<!-- Autocomplete-lista -->
|
||||
<ul class="autocomplete-list" ng-if="existingMeals.length > 0">
|
||||
<li ng-repeat="meal in existingMeals" ng-click="selectExistingMeal(meal)">
|
||||
{{ meal.Name }}
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<!-- Receptfält visas bara om ingen befintlig rätt hittas -->
|
||||
<textarea ng-if="showRecipeField" ng-model="newWishRecipe" placeholder="Skriv recept"></textarea>
|
||||
|
||||
<button type="submit">Skicka önskan</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</body>
|
||||
|
||||
@@ -51,12 +51,11 @@
|
||||
@if (User.IsInRole("Admin"))
|
||||
{
|
||||
<li>
|
||||
<a asp-controller="torrent" asp-action="Index"> Torrents
|
||||
|
||||
@if (ViewBag.NewTorrentCount > 0)
|
||||
{
|
||||
<span class="new-badge">@ViewBag.NewTorrentCount</span>
|
||||
}
|
||||
<a asp-controller="torrent" asp-action="Index"> T
|
||||
@if (ViewBag.NewTorrentCount > 0)
|
||||
{
|
||||
<span class="new-badge">@ViewBag.NewTorrentCount</span>
|
||||
}
|
||||
</a>
|
||||
|
||||
</li>
|
||||
|
||||
@@ -184,3 +184,144 @@
|
||||
padding: 8px 12px;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
|
||||
.highlight {
|
||||
background: linear-gradient(270deg, #ff6ec4, #7873f5, #4ade80, #facc15, #f87171);
|
||||
background-size: 1000% 1000%;
|
||||
animation: rainbow 6s linear infinite;
|
||||
border-radius: 4px;
|
||||
color: white;
|
||||
padding: 2px 4px;
|
||||
}
|
||||
|
||||
@keyframes rainbow {
|
||||
0% {
|
||||
background-position: 0% 50%;
|
||||
}
|
||||
|
||||
50% {
|
||||
background-position: 100% 50%;
|
||||
}
|
||||
|
||||
100% {
|
||||
background-position: 0% 50%;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.menu-wishlist-wrapper {
|
||||
display: flex;
|
||||
gap: 2rem;
|
||||
align-items: flex-start;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Veckomenyn tar 2/3 av bredden */
|
||||
.weekly-editor {
|
||||
flex: 2;
|
||||
min-width: 400px;
|
||||
}
|
||||
|
||||
.wishlist {
|
||||
max-width: 100%;
|
||||
position: sticky;
|
||||
top: 1rem;
|
||||
}
|
||||
|
||||
.wishlist-grid {
|
||||
display: grid;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.wishlist-card {
|
||||
background: #fff;
|
||||
border-radius: 12px;
|
||||
padding: 1rem;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
|
||||
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
||||
}
|
||||
|
||||
.wishlist-card:hover {
|
||||
transform: translateY(-3px);
|
||||
box-shadow: 0 6px 16px rgba(0,0,0,0.15);
|
||||
}
|
||||
|
||||
.wishlist-header {
|
||||
font-size: 1.1rem;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.wishlist-meta {
|
||||
display: block;
|
||||
font-size: 0.85rem;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.wishlist-body {
|
||||
margin: 0.5rem 0;
|
||||
font-size: 0.9rem;
|
||||
color: #444;
|
||||
}
|
||||
|
||||
.wishlist-actions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.wishlist-actions .btn {
|
||||
border: none;
|
||||
padding: 0.4rem 0.8rem;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
font-size: 0.85rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.3rem;
|
||||
}
|
||||
|
||||
.wishlist-actions .btn.import {
|
||||
background: #4ade80;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.wishlist-actions .btn.import:hover {
|
||||
background: #22c55e;
|
||||
}
|
||||
|
||||
.wishlist-actions .btn.archive {
|
||||
background: #f87171;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.wishlist-actions .btn.archive:hover {
|
||||
background: #dc2626;
|
||||
}
|
||||
|
||||
/* Wishlist + Recent history till höger */
|
||||
.wishlist-wrapper {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.recent-history {
|
||||
background: var(--header);
|
||||
border-radius: 8px;
|
||||
color: var(--text);
|
||||
padding: 16px;
|
||||
}
|
||||
/* Wrap för horisontell scroll */
|
||||
.recent-table-wrapper {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.recent-history-fullwidth table {
|
||||
width: 100%;
|
||||
min-width: 600px; /* säkerställer scroll om skärm <600px */
|
||||
border-collapse: collapse;
|
||||
}
|
||||
@@ -322,4 +322,116 @@ h1 {
|
||||
.school-meal-title .chevron.rotated {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
|
||||
/* Flyout popup */
|
||||
.wish-flyout {
|
||||
position: fixed;
|
||||
bottom: 80px; /* ovanför plustecknet */
|
||||
right: 20px;
|
||||
width: 300px;
|
||||
max-width: 90%;
|
||||
background-color: var(--card-bg);
|
||||
border-radius: var(--border-radius);
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
|
||||
padding: 12px;
|
||||
z-index: 1001;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* Responsiv på mobil */
|
||||
@media (max-width: 480px) {
|
||||
.wish-flyout {
|
||||
right: 10px;
|
||||
width: calc(100% - 20px);
|
||||
bottom: 70px;
|
||||
}
|
||||
}
|
||||
|
||||
.wish-flyout label {
|
||||
font-weight: bold;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.wish-flyout input,
|
||||
.wish-flyout textarea {
|
||||
width: 100%;
|
||||
margin-top: 4px;
|
||||
padding: 6px;
|
||||
border-radius: var(--border-radius);
|
||||
border: 1px solid #ccc;
|
||||
font-size: 0.9rem;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.wish-flyout textarea {
|
||||
resize: vertical;
|
||||
min-height: 60px;
|
||||
}
|
||||
|
||||
.flyout-buttons {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 6px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.flyout-buttons button {
|
||||
padding: 6px 12px;
|
||||
border-radius: var(--border-radius);
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.flyout-buttons button[type="submit"] {
|
||||
background-color: var(--accent);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.flyout-buttons button[type="button"] {
|
||||
background-color: #ccc;
|
||||
color: var(--text);
|
||||
}
|
||||
/* Floating button fixed i nedre högra hörnet */
|
||||
.floating-btn {
|
||||
position: fixed; /* fixed istället för relative/absolute */
|
||||
bottom: 20px; /* avstånd från botten */
|
||||
right: 20px; /* avstånd från höger */
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
border-radius: 50%;
|
||||
background-color: var(--accent);
|
||||
color: #fff;
|
||||
font-size: 28px;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 4px 8px rgba(0,0,0,0.3);
|
||||
z-index: 1000; /* högre än andra element så den ligger ovanpå */
|
||||
}
|
||||
|
||||
.floating-btn:hover {
|
||||
background-color: #2B6CB0;
|
||||
}
|
||||
.autocomplete-list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 4px 0 0 0;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
max-height: 150px;
|
||||
overflow-y: auto;
|
||||
background: var(--card-bg);
|
||||
position: absolute;
|
||||
width: 90%; /* Anpassa efter input */
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.autocomplete-list li {
|
||||
padding: 6px 10px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.autocomplete-list li:hover {
|
||||
background-color: var(--accent);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ angular.module('mealMenuApp', ['ngSanitize'])
|
||||
$scope.daysOfWeek = ["Måndag", "Tisdag", "Onsdag", "Torsdag", "Fredag", "Lördag", "Söndag"];
|
||||
|
||||
$scope.loadMenu = function () {
|
||||
console.log("Hämtar meny för vecka:", $scope.selectedWeek, $scope.selectedYear);
|
||||
//console.log("Hämtar meny för vecka:", $scope.selectedWeek, $scope.selectedYear);
|
||||
|
||||
$http.get('/api/mealMenuApi/getWeeklyMenu', {
|
||||
params: { weekNumber: $scope.selectedWeek, year: $scope.selectedYear }
|
||||
@@ -28,8 +28,8 @@ angular.module('mealMenuApp', ['ngSanitize'])
|
||||
rawMenu.forEach(item => {
|
||||
const dayIndex = item.DayOfWeek - 1;
|
||||
const day = $scope.daysOfWeek[dayIndex];
|
||||
console.warn("Item:", item);
|
||||
console.log("day: " + day + "(" + dayIndex + " ) item: " + item.DinnerMealName);
|
||||
//console.warn("Item:", item);
|
||||
//console.log("day: " + day + "(" + dayIndex + " ) item: " + item.DinnerMealName);
|
||||
const thumb = item.DinnerMealThumbnail;
|
||||
|
||||
$scope.menu[day] = {
|
||||
@@ -39,12 +39,12 @@ angular.module('mealMenuApp', ['ngSanitize'])
|
||||
lunchMealName: item.LunchMealName,
|
||||
dinnerMealId: item.DinnerMealId,
|
||||
dinnerMealName: item.DinnerMealName,
|
||||
imageUrl: thumb ? `data:image/webp;base64,${thumb}` : '/img/default-thumbnail.webp'
|
||||
imageUrl: thumb ? `data:image/webp;base64,${thumb}` : ''
|
||||
};
|
||||
});
|
||||
|
||||
}).catch(err => console.error("Fel vid hämtning av meny:", err));
|
||||
//$scope.loadSchoolMeals(); // Lägg till här
|
||||
$scope.loadSchoolMeals();
|
||||
|
||||
};
|
||||
$scope.schoolMeals = [];
|
||||
@@ -104,7 +104,7 @@ angular.module('mealMenuApp', ['ngSanitize'])
|
||||
};
|
||||
|
||||
$scope.getDayImage = function (day) {
|
||||
return $scope.menu[day]?.imageUrl || '/img/default-thumbnail.webp';
|
||||
return $scope.menu[day]?.imageUrl || '';
|
||||
};
|
||||
|
||||
$scope.getMealIdByDay = function (day) {
|
||||
@@ -152,6 +152,76 @@ angular.module('mealMenuApp', ['ngSanitize'])
|
||||
if (viewBtn) viewBtn.textContent = $scope.getViewIcon();
|
||||
}, 0);
|
||||
};
|
||||
$scope.isLoggedIn = true; // sätt till true/false beroende på användarstatus
|
||||
$scope.showWishForm = false;
|
||||
$scope.showRecipeField = true; // kan styras av autocomplete
|
||||
$scope.newWishName = "";
|
||||
$scope.newWishRecipe = "";
|
||||
|
||||
$scope.showWishFlyout = false;
|
||||
|
||||
$scope.toggleWishFlyout = function () {
|
||||
$scope.showWishFlyout = !$scope.showWishFlyout;
|
||||
}
|
||||
|
||||
$scope.closeWishFlyout = function () {
|
||||
$scope.showWishFlyout = false;
|
||||
$scope.newWishName = "";
|
||||
$scope.newWishRecipe = "";
|
||||
$scope.showRecipeField = true;
|
||||
}
|
||||
|
||||
$scope.existingMeals = [];
|
||||
$scope.selectedMeal = null;
|
||||
|
||||
$scope.checkExistingMeal = function () {
|
||||
if (!$scope.newWishName || $scope.newWishName.length < 2) {
|
||||
$scope.existingMeals = [];
|
||||
$scope.showRecipeField = true;
|
||||
return;
|
||||
}
|
||||
|
||||
fetch('/api/MealWish/search?name=' + encodeURIComponent($scope.newWishName))
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
$scope.existingMeals = data;
|
||||
$scope.$apply(); // viktigt annars uppdateras inte ng-repeat
|
||||
})
|
||||
.catch(err => console.error(err));
|
||||
}
|
||||
|
||||
$scope.selectExistingMeal = function (meal) {
|
||||
$scope.newWishName = meal.Name;
|
||||
$scope.selectedMeal = meal;
|
||||
$scope.showRecipeField = true;
|
||||
$scope.existingMeals = [];
|
||||
}
|
||||
|
||||
$scope.submitWish = function () {
|
||||
if (!$scope.newWishName) return;
|
||||
if ($scope.existingMeal) {
|
||||
wish.LinkedMealId = $scope.existingMeal.Id;
|
||||
wish.Recipe = $scope.Recipe;
|
||||
}
|
||||
const payload = {
|
||||
Name: $scope.newWishName,
|
||||
Recipe: $scope.newWishRecipe
|
||||
};
|
||||
|
||||
fetch('/api/MealWish/create', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload)
|
||||
})
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
console.log("Önskan skickad:", data);
|
||||
$scope.closeWishFlyout();
|
||||
$scope.$apply();
|
||||
})
|
||||
.catch(err => console.error(err));
|
||||
}
|
||||
|
||||
|
||||
console.log("Initierar meny och måltidsladdning...");
|
||||
$scope.loadMenu();
|
||||
|
||||
Reference in New Issue
Block a user