24 Commits
Dev ... backup

Author SHA1 Message Date
Tai
57bea7b54c Meal updates
Some checks failed
continuous-integration/drone/push Build is failing
2025-06-11 13:06:08 +02:00
Elias Jansson
465f9afc99 Bunch of fixes and session extensions
All checks were successful
continuous-integration/drone/push Build is passing
2025-06-10 18:59:04 +02:00
Elias Jansson
e3eb2dc7cb Budget page improvements
All checks were successful
continuous-integration/drone/push Build is passing
2025-06-09 16:11:59 +02:00
elias
256ce76af1 Pizza notice
All checks were successful
continuous-integration/drone/push Build is passing
2025-06-06 12:57:49 +02:00
elias
b286fed88a Ny meals!
All checks were successful
continuous-integration/drone/push Build is passing
2025-06-05 23:23:36 +02:00
elias
bc77e39c72 Optimized menu
All checks were successful
continuous-integration/drone/push Build is passing
2025-06-05 20:15:11 +02:00
elias
eba00ffbf2 Todo fixes
All checks were successful
continuous-integration/drone/push Build is passing
2025-06-05 00:20:17 +02:00
Elias Jansson
c44fbfdca9 Woohoo working version gooo
All checks were successful
continuous-integration/drone/push Build is passing
2025-06-04 18:01:05 +02:00
Elias Jansson
84c6c45a0b Another attempt
All checks were successful
continuous-integration/drone/push Build is passing
2025-06-04 11:51:08 +02:00
Elias Jansson
83a71a6f1d Setup.json password usage
All checks were successful
continuous-integration/drone/push Build is passing
2025-06-04 09:42:03 +02:00
Elias Jansson
fcd27943bf Drone fix
All checks were successful
continuous-integration/drone/push Build is passing
2025-06-04 09:08:36 +02:00
Elias Jansson
acbb72de3f DRONE
Some checks failed
continuous-integration/drone/push Build is failing
2025-06-03 23:12:15 +02:00
Elias Jansson
c4efcc249b Drone
Some checks failed
continuous-integration/drone/push Build encountered an error
2025-06-03 23:05:53 +02:00
Elias Jansson
4da4a34443 Passwordfix
All checks were successful
continuous-integration/drone/push Build is passing
2025-06-03 22:47:17 +02:00
Elias Jansson
77d3c741b1 Stringbuilder to handle passwords
All checks were successful
continuous-integration/drone/push Build is passing
2025-06-03 22:35:37 +02:00
Elias Jansson
e626daa7bc Gogogo
All checks were successful
continuous-integration/drone/push Build is passing
2025-06-03 22:11:17 +02:00
Elias Jansson
fe8e54b868 Git
All checks were successful
continuous-integration/drone/push Build is passing
2025-06-03 21:57:44 +02:00
Elias Jansson
908bc469c6 Setup!
All checks were successful
continuous-integration/drone/push Build is passing
2025-06-03 21:36:16 +02:00
Elias Jansson
3d6aa2d424 Use old db
All checks were successful
continuous-integration/drone/push Build is passing
2025-06-03 00:26:13 +02:00
Elias Jansson
fd6759e3d0 Gogogo
All checks were successful
continuous-integration/drone/push Build is passing
2025-06-03 00:19:40 +02:00
Elias Jansson
4c60508d6d Fixes!
All checks were successful
continuous-integration/drone/push Build is passing
2025-06-03 00:04:28 +02:00
Elias Jansson
628f25a8be More
All checks were successful
continuous-integration/drone/push Build is passing
2025-06-02 23:52:17 +02:00
Elias Jansson
2e1e0f4670 Clearing up proj file
Some checks failed
continuous-integration/drone/push Build is failing
2025-06-02 23:48:43 +02:00
Elias Jansson
a33ed400f1 Dockerfile fix
Some checks failed
continuous-integration/drone/push Build is failing
2025-06-02 23:40:47 +02:00
71 changed files with 6035 additions and 744 deletions

View File

@@ -4,13 +4,9 @@ name: default
steps:
- name: build-dotnet
image: mcr.microsoft.com/dotnet/sdk:6.0
environment:
LANG: en_US.UTF-8
LC_ALL: en_US.UTF-8
DOTNET_CLI_UI_LANGUAGE: en-US
image: alpine
commands:
- dotnet publish Aberwyn/Aberwyn.csproj -c Release -o out
- echo "Docker build will handle dotnet publish"
- name: build-docker
image: plugins/docker
@@ -38,9 +34,10 @@ steps:
from_secret: unraid_ssh_private_key
script:
- docker pull 192.168.1.9:3000/tai/aberwyn/aberwyn:latest
- docker stop Aberwyn || true
- docker rm Aberwyn || true
- docker run -d --name='Aberwyn' --net='br0' -e TZ='Europe/Berlin' -p 80:80 192.168.1.9:3000/tai/aberwyn/aberwyn:latest
- docker stop aberwyn || true
- docker rm aberwyn || true
- docker volume create aberwyn_config || true
- docker run -d --name=aberwyn --net=br0 -e TZ=Europe/Berlin -p 80:80 -v aberwyn_config:/app/infrastructure 192.168.1.9:3000/tai/aberwyn/aberwyn:latest
- name: notify-result
image: alpine

6
.gitignore vendored
View File

@@ -360,4 +360,8 @@ MigrationBackup/
.ionide/
# Fody - auto-generated XML schema
FodyWeavers.xsd
FodyWeavers.xsd
# Setupfil för Aberwyn
infrastructure/setup.json
Aberwyn/Infrastructure/setup.json

View File

@@ -11,10 +11,6 @@ Global
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{423CD237-7404-4355-868F-CE5861835258}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{423CD237-7404-4355-868F-CE5861835258}.Debug|Any CPU.Build.0 = Debug|Any CPU
{423CD237-7404-4355-868F-CE5861835258}.Release|Any CPU.ActiveCfg = Release|Any CPU
{423CD237-7404-4355-868F-CE5861835258}.Release|Any CPU.Build.0 = Release|Any CPU
{F5586986-B726-4E05-B31B-2E24CA5B2B89}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F5586986-B726-4E05-B31B-2E24CA5B2B89}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F5586986-B726-4E05-B31B-2E24CA5B2B89}.Release|Any CPU.ActiveCfg = Release|Any CPU

View File

@@ -4,9 +4,9 @@ name: default
steps:
- name: build-dotnet
image: mcr.microsoft.com/dotnet/sdk:6.0
image: alpine
commands:
- dotnet publish Aberwyn/Aberwyn.csproj -c Release -o out
- echo "Docker build will handle dotnet publish"
- name: build-docker
image: plugins/docker
@@ -18,6 +18,37 @@ steps:
password:
from_secret: gitea_token
dockerfile: Aberwyn/Dockerfile
context: Aberwyn
context: .
tags:
- latest
insecure: true
- name: restart-unraid-container
image: appleboy/drone-ssh
settings:
host: 192.168.1.108
port: 22
username:
from_secret: unraid_ssh_user
key:
from_secret: unraid_ssh_private_key
script:
- docker pull 192.168.1.9:3000/tai/aberwyn/aberwyn:latest
- docker stop Aberwyn || true
- docker rm Aberwyn || true
- docker run -d --name='Aberwyn' --net='br0' -e TZ='Europe/Berlin' -p 80:80 192.168.1.9:3000/tai/aberwyn/aberwyn:latest
- name: notify-result
image: alpine
when:
status:
- success
- failure
commands:
- apk add --no-cache curl
- |
if [ "$DRONE_BUILD_STATUS" = "success" ]; then
curl -X POST http://192.168.1.196:8123/api/webhook/aberwyn_update_success
else
curl -X POST http://192.168.1.196:8123/api/webhook/aberwyn_update_failed
fi

View File

@@ -9,19 +9,11 @@
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
</PropertyGroup>
<ItemGroup>
<Compile Remove="NewFolder\**" />
<Content Remove="NewFolder\**" />
<EmbeddedResource Remove="NewFolder\**" />
<None Remove="NewFolder\**" />
</ItemGroup>
<ItemGroup>
<Compile Remove="Data\BudgetService.cs" />
</ItemGroup>
<ItemGroup>
<Content Remove="Views\Home\Budget.cshtml" />
<Compile Remove="Views\NewFolder\**" />
<Content Remove="Views\NewFolder\**" />
<EmbeddedResource Remove="Views\NewFolder\**" />
<None Remove="Views\NewFolder\**" />
</ItemGroup>
<ItemGroup>
@@ -35,6 +27,7 @@
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="6.0.36" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="6.0.36" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="6.0.36">
<PrivateAssets>all</PrivateAssets>
@@ -52,12 +45,12 @@
<!-- Övrigt -->
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.15.1" />
<PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="6.0.18" />
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.9" />
<PackageReference Include="WebPush" Version="1.0.12" />
</ItemGroup>
<ItemGroup>
<Folder Include="Migrations\" />
<Folder Include="Views\NewFolder\" />
<Folder Include="wwwroot\images\meals\" />
</ItemGroup>

View File

@@ -99,7 +99,7 @@ namespace Aberwyn.Areas.Identity.Pages.Account
{
// This doesn't count login failures towards account lockout
// To enable password failures to trigger account lockout, set lockoutOnFailure: true
var result = await _signInManager.PasswordSignInAsync(Input.UserName, Input.Password, Input.RememberMe, lockoutOnFailure: false);
var result = await _signInManager.PasswordSignInAsync(Input.UserName, Input.Password, isPersistent: true, lockoutOnFailure: false);
if (result.Succeeded)
{
_logger.LogInformation("User logged in.");

View File

@@ -11,6 +11,7 @@ namespace Aberwyn.Controllers
[Authorize(Roles = "Admin")]
public class AdminController : Controller
{
private readonly MenuService _menuService;
private readonly UserManager<ApplicationUser> _userManager;
private readonly RoleManager<IdentityRole> _roleManager;
private readonly IConfiguration _configuration;
@@ -18,12 +19,14 @@ namespace Aberwyn.Controllers
private readonly ApplicationDbContext _context;
public AdminController(
MenuService menuService,
UserManager<ApplicationUser> userManager,
RoleManager<IdentityRole> roleManager,
IConfiguration configuration,
IHostEnvironment env,
ApplicationDbContext context)
{
_menuService = menuService;
_userManager = userManager;
_roleManager = roleManager;
_configuration = configuration;
@@ -102,54 +105,20 @@ namespace Aberwyn.Controllers
}
[HttpPost]
[Authorize(Roles = "Admin")]
public IActionResult ImportMealsFromProd()
public IActionResult ImportMenusFromCustom(string dbHost, int dbPort, string dbName, string dbUser, string dbPassword)
{
var prodService = MenuService.CreateWithConfig(_configuration, _env, useProdDb: true);
var devService = new MenuService(_context); // injicerad context används
var connStr = $"Server={dbHost};Port={dbPort};Database={dbName};Uid={dbUser};Pwd={dbPassword};";
var optionsBuilder = new DbContextOptionsBuilder<ApplicationDbContext>();
optionsBuilder.UseMySql(connStr, ServerVersion.AutoDetect(connStr));
var prodMeals = prodService.GetMealsDetailed();
foreach (var meal in prodMeals)
{
// Kopiera utan ID (för att undvika konflikt) och spara
var newMeal = new Meal
{
Name = meal.Name,
Description = meal.Description,
ProteinType = meal.ProteinType,
Category = meal.Category,
CarbType = meal.CarbType,
RecipeUrl = meal.RecipeUrl,
ImageUrl = meal.ImageUrl,
IsAvailable = meal.IsAvailable,
CreatedAt = meal.CreatedAt,
Instructions = meal.Instructions,
ImageData = meal.ImageData,
ImageMimeType = meal.ImageMimeType,
Ingredients = meal.Ingredients.Select(i => new Ingredient
{
Quantity = i.Quantity,
Item = i.Item
}).ToList()
};
devService.SaveOrUpdateMealWithIngredients(newMeal);
}
return Content("Import klar!");
}
[HttpPost]
[Authorize(Roles = "Admin")]
public IActionResult ImportMenusFromProd()
{
var prodService = MenuService.CreateWithConfig(_configuration, _env, useProdDb: true);
using var customContext = new ApplicationDbContext(optionsBuilder.Options);
var sourceService = new MenuService(customContext);
var devService = new MenuService(_context);
var allProdMenus = prodService.GetAllWeeklyMenus();
var allMeals = devService.GetMeals();
var sourceMenus = sourceService.GetAllWeeklyMenus();
var devMeals = devService.GetMeals();
foreach (var menu in allProdMenus)
foreach (var menu in sourceMenus)
{
var newMenu = new WeeklyMenu
{
@@ -163,34 +132,99 @@ namespace Aberwyn.Controllers
};
if (!string.IsNullOrEmpty(menu.BreakfastMealName))
newMenu.BreakfastMealId = allMeals.FirstOrDefault(m => m.Name == menu.BreakfastMealName)?.Id;
newMenu.BreakfastMealId = devMeals.FirstOrDefault(m => m.Name == menu.BreakfastMealName)?.Id;
if (!string.IsNullOrEmpty(menu.LunchMealName))
newMenu.LunchMealId = allMeals.FirstOrDefault(m => m.Name == menu.LunchMealName)?.Id;
newMenu.LunchMealId = devMeals.FirstOrDefault(m => m.Name == menu.LunchMealName)?.Id;
if (!string.IsNullOrEmpty(menu.DinnerMealName))
newMenu.DinnerMealId = allMeals.FirstOrDefault(m => m.Name == menu.DinnerMealName)?.Id;
newMenu.DinnerMealId = devMeals.FirstOrDefault(m => m.Name == menu.DinnerMealName)?.Id;
_context.WeeklyMenus.Add(newMenu);
}
_context.SaveChanges();
TempData["Message"] = "Import av veckomenyer klar.";
TempData["Message"] = $"✅ Import av veckomenyer från extern databas klar ({sourceMenus.Count}).";
return RedirectToAction("Index");
}
[HttpPost]
[Authorize(Roles = "Admin")]
public IActionResult GenerateThumbnails()
{
var count = _menuService.GenerateMissingThumbnails();
return Ok($"{count} thumbnails skapades.");
}
[HttpPost]
[Authorize(Roles = "Admin")]
public IActionResult ImportBudgetFromProd()
public IActionResult ImportMealsFromCustom(string dbHost, int dbPort, string dbName, string dbUser, string dbPassword)
{
// Hämta connection till produktion
using var prodContext = ApplicationDbContextFactory.CreateWithConfig(_configuration, _env, useProdDb: true);
var connStr = $"Server={dbHost};Port={dbPort};Database={dbName};Uid={dbUser};Pwd={dbPassword};";
var optionsBuilder = new DbContextOptionsBuilder<ApplicationDbContext>();
optionsBuilder.UseMySql(connStr, ServerVersion.AutoDetect(connStr), mySqlOptions => mySqlOptions.CommandTimeout(180));
using var customContext = new ApplicationDbContext(optionsBuilder.Options);
// Importera definitioner först
var prodCategoryDefs = prodContext.BudgetCategoryDefinitions.ToList();
var prodItemDefs = prodContext.BudgetItemDefinitions.ToList();
var customService = new MenuService(customContext);
var devService = new MenuService(_context);
foreach (var def in prodCategoryDefs)
try
{
var importedMeals = customService.GetMealsDetailed(); // Ska inkludera Ingredients
foreach (var meal in importedMeals)
{
var newMeal = new Meal
{
Id = meal.Id, // 👈 Viktigt!
Name = meal.Name,
Description = meal.Description,
ProteinType = meal.ProteinType,
Category = meal.Category,
CarbType = meal.CarbType,
RecipeUrl = meal.RecipeUrl,
ImageUrl = meal.ImageUrl,
IsAvailable = meal.IsAvailable,
CreatedAt = meal.CreatedAt,
Instructions = meal.Instructions,
ImageData = meal.ImageData,
ImageMimeType = meal.ImageMimeType,
Ingredients = meal.Ingredients.Select(i => new Ingredient
{
MealId = meal.Id, // 👈 Koppla till rätt måltid
Quantity = i.Quantity,
Item = i.Item
}).ToList()
};
devService.SaveOrUpdateMealWithIngredients(newMeal);
}
TempData["Message"] = $"✅ {importedMeals.Count} måltider importerade från extern databas.";
}
catch (Exception ex)
{
TempData["Message"] = $"❌ Fel vid import: {ex.Message}";
}
return RedirectToAction("Index");
}
[HttpPost]
[Authorize(Roles = "Admin")]
public IActionResult ImportBudgetFromCustom(string dbHost, int dbPort, string dbName, string dbUser, string dbPassword)
{
var connStr = $"Server={dbHost};Port={dbPort};Database={dbName};Uid={dbUser};Pwd={dbPassword};";
var optionsBuilder = new DbContextOptionsBuilder<ApplicationDbContext>();
optionsBuilder.UseMySql(connStr, ServerVersion.AutoDetect(connStr));
using var sourceContext = new ApplicationDbContext(optionsBuilder.Options);
var categoryDefs = sourceContext.BudgetCategoryDefinitions.ToList();
var itemDefs = sourceContext.BudgetItemDefinitions.ToList();
foreach (var def in categoryDefs)
{
if (!_context.BudgetCategoryDefinitions.Any(d => d.Name == def.Name))
{
@@ -199,11 +233,10 @@ namespace Aberwyn.Controllers
Name = def.Name,
Color = def.Color ?? "#cccccc"
});
}
}
foreach (var def in prodItemDefs)
foreach (var def in itemDefs)
{
if (!_context.BudgetItemDefinitions.Any(d => d.Name == def.Name))
{
@@ -211,19 +244,17 @@ namespace Aberwyn.Controllers
}
}
_context.SaveChanges(); // Se till att ID:n finns för FK:n nedan
_context.SaveChanges();
// Ladda definitioner i minnet för snabb lookup
var devCategoryDefs = _context.BudgetCategoryDefinitions.ToList();
var devItemDefs = _context.BudgetItemDefinitions.ToList();
// Importera budgetperioder med kategorier och items
var prodPeriods = prodContext.BudgetPeriods
var periods = sourceContext.BudgetPeriods
.Include(p => p.Categories)
.ThenInclude(c => c.Items)
.ToList();
foreach (var period in prodPeriods)
foreach (var period in periods)
{
var exists = _context.BudgetPeriods
.Any(p => p.Year == period.Year && p.Month == period.Month);
@@ -259,11 +290,13 @@ namespace Aberwyn.Controllers
}
_context.SaveChanges();
TempData["Message"] = "✅ Import av budgetdata från produktion är klar.";
TempData["Message"] = $"✅ Import av budgetdata från extern databas klar ({periods.Count} månader).";
return RedirectToAction("Index");
}
//Todo
[HttpGet]
@@ -285,16 +318,31 @@ namespace Aberwyn.Controllers
[HttpPost]
public IActionResult AddTodoTask([FromBody] TodoTask task)
{
if (string.IsNullOrWhiteSpace(task?.Title))
return BadRequest("Titel krävs");
try
{
if (string.IsNullOrWhiteSpace(task?.Title))
return BadRequest("Titel krävs");
task.CreatedAt = DateTime.UtcNow;
_context.TodoTasks.Add(task);
_context.SaveChanges();
task.CreatedAt = DateTime.UtcNow;
return Json(task);
task.Status ??= "ideas";
task.Tags ??= "";
task.AssignedTo ??= null;
task.Description ??= "";
_context.TodoTasks.Add(task);
_context.SaveChanges();
return Json(task);
}
catch (Exception ex)
{
// 👇 Returnera hela stacktracen som JSON för felsökning
return StatusCode(500, ex.ToString());
}
}
[HttpPost]
public IActionResult UpdateTodoTask([FromBody] TodoTask task)
{
@@ -305,18 +353,41 @@ namespace Aberwyn.Controllers
existing.Title = task.Title;
existing.Status = task.Status;
existing.Priority = task.Priority;
existing.Description = task.Description;
existing.Tags = task.Tags;
existing.AssignedTo = task.AssignedTo;
existing.IsArchived = task.IsArchived;
_context.SaveChanges();
return Ok();
}
[HttpPost]
[Authorize(Roles = "Admin")]
public IActionResult TestDbConnection(string dbHost, int dbPort, string dbName, string dbUser, string dbPassword)
{
var connStr = $"Server={dbHost};Port={dbPort};Database={dbName};Uid={dbUser};Pwd={dbPassword};";
try
{
var builder = new DbContextOptionsBuilder<ApplicationDbContext>();
builder.UseMySql(connStr, ServerVersion.AutoDetect(connStr));
using var context = new ApplicationDbContext(builder.Options);
context.Database.OpenConnection();
context.Database.CloseConnection();
return Json(new { success = true, message = "✅ Anslutning lyckades!" });
}
catch (Exception ex)
{
return Json(new { success = false, message = $"❌ Anslutning misslyckades: {ex.Message}" });
}
}
}
public class AdminUserViewModel
public class AdminUserViewModel
{
public string UserId { get; set; }
public string Email { get; set; }

View File

@@ -2,6 +2,8 @@
using Aberwyn.Models;
using Aberwyn.Data;
using Microsoft.AspNetCore.Authorization;
using SixLabors.ImageSharp.Processing;
using SixLabors.ImageSharp;
namespace Aberwyn.Controllers
{
@@ -10,28 +12,45 @@ namespace Aberwyn.Controllers
private readonly IConfiguration _configuration;
private readonly IHostEnvironment _env;
private readonly MenuService _menuService;
public MealController(IConfiguration configuration, IHostEnvironment env)
public MealController(MenuService menuService, IConfiguration configuration, IHostEnvironment env)
{
_menuService = menuService;
_configuration = configuration;
_env = env;
}
[HttpGet]
public IActionResult View(int id, bool edit = false)
[HttpGet("/meal")]
public IActionResult Index()
{
var service = _menuService;
var meal = service.GetMealById(id);
return View("Index");
}
[HttpGet]
public IActionResult View(int? id, bool edit = false)
{
Meal meal;
ViewData["IsEditing"] = edit; // → här
if (meal == null)
if (id.HasValue)
{
return NotFound();
meal = _menuService.GetMealById(id.Value);
if (meal == null)
return NotFound();
}
else
{
meal = new Meal
{
Name = "",
Description = "",
Ingredients = new List<Ingredient>(),
IsAvailable = true,
CreatedAt = DateTime.Now
};
}
ViewData["IsEditing"] = edit;
return View("View", meal);
}
[HttpGet]
[Route("Meal/Tooltip/{id}")]
public IActionResult Tooltip(int id)
@@ -69,6 +88,15 @@ namespace Aberwyn.Controllers
return StatusCode(500, $"<pre>{ex.Message}</pre>");
}
}
[HttpGet("Meal/Thumbnail/{id}")]
public IActionResult Thumbnail(int id)
{
var meal = _menuService.GetMealById(id);
if (meal == null || meal.ThumbnailData == null)
return NotFound();
return File(meal.ThumbnailData, "image/webp"); // eller image/jpeg om du använder det
}
@@ -92,6 +120,8 @@ namespace Aberwyn.Controllers
ImageFile.CopyTo(ms);
meal.ImageData = ms.ToArray();
meal.ImageMimeType = ImageFile.ContentType;
meal.ThumbnailData = GenerateThumbnail(ImageFile);
}
else
{
@@ -110,8 +140,21 @@ namespace Aberwyn.Controllers
}
private byte[] GenerateThumbnail(IFormFile file)
{
using var image = SixLabors.ImageSharp.Image.Load(file.OpenReadStream());
image.Mutate(x => x.Resize(new ResizeOptions
{
Mode = ResizeMode.Max,
Size = new Size(300, 300) // eller vad du vill för korten
}));
using var ms = new MemoryStream();
image.SaveAsWebp(ms); // kräver ImageSharp.Webp-paketet
return ms.ToArray();
}
[HttpPost]

View File

@@ -4,6 +4,8 @@ using Aberwyn.Data;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;
using System.Net.Http.Headers;
using System.Text.Json;
namespace Aberwyn.Controllers
{
@@ -28,12 +30,23 @@ namespace Aberwyn.Controllers
[HttpGet("getMeals")]
public IActionResult GetMeals()
{
var meals = _menuService.GetMealsDetailed(); // Hämtar med ImageData
var mealDtos = meals.Select(MealDto.FromMeal).ToList();
var meals = _menuService.GetMealsSlim(true);
var mealDtos = meals.Select(m => MealDto.FromMeal(m, includeThumbnail: true)).ToList(); // 👈 fix
return Ok(mealDtos);
}
[HttpGet("getWeeklyMenu")]
public IActionResult GetWeeklyMenu(int weekNumber, int year)
{
var menuDtos = _menuService.GetWeeklyMenuDto(weekNumber, year);
Console.WriteLine("Hämtar meals: " + menuDtos);
return Ok(menuDtos);
}
[HttpPut("menu")]
public IActionResult SaveMenu([FromBody] MenuViewModel weeklyMenu)
{
@@ -55,5 +68,44 @@ namespace Aberwyn.Controllers
return StatusCode(500, "Failed to add meal.");
}
#region Skolmat
[HttpGet("skolmat")]
public async Task<IActionResult> GetSkolmat(int week, [FromQuery] string sensor = "sensor.engelbrektsskolan")
{
var token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiI3M2Q5ODIyYzU4ZWI0MjM4OWEyMGQ2MWQ2MWVhOWYzYyIsImlhdCI6MTc0OTE1MzY1MCwiZXhwIjoyMDY0NTEzNjUwfQ.8C_dKm7P1BbFVJKc_wT76YnQqiZxkP9EzrsLbfD0Ml8";
var client = new HttpClient();
client.BaseAddress = new Uri("http://192.168.1.196:8123/");
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
var res = await client.GetAsync($"/api/states/{sensor}");
if (!res.IsSuccessStatusCode)
return StatusCode((int)res.StatusCode, $"Kunde inte hämta data för {sensor}");
var json = await res.Content.ReadAsStringAsync();
var doc = JsonDocument.Parse(json);
var attr = doc.RootElement.GetProperty("attributes");
if (!attr.TryGetProperty("calendar", out var calendar))
return NotFound("Ingen kalender hittades i attributen");
if (!calendar.TryGetProperty(week.ToString(), out var weekData))
return NotFound("Ingen skolmat för vecka " + week);
var result = new List<object>();
foreach (var day in weekData.EnumerateArray())
{
var weekday = day.GetProperty("weekday").GetString();
var date = day.GetProperty("date").GetString();
var courses = day.GetProperty("courses").EnumerateArray().Select(c => c.GetString()).ToList();
result.Add(new { weekday, date, courses });
}
return Ok(result);
}
#endregion
}
}

View File

@@ -2,6 +2,7 @@
using Aberwyn.Data;
using Aberwyn.Models;
using System.Linq;
using Aberwyn.Services;
namespace Aberwyn.Controllers
{
@@ -9,11 +10,14 @@ namespace Aberwyn.Controllers
{
private readonly MenuService _menuService;
private readonly ApplicationDbContext _context;
private readonly PizzaNotificationService _pizzaNotifier;
public PizzaController(MenuService menuService, ApplicationDbContext context)
public PizzaController(PizzaNotificationService pizzaNotifier,MenuService menuService, ApplicationDbContext context)
{
_menuService = menuService;
_context = context;
_pizzaNotifier = pizzaNotifier;
}
[HttpGet]
@@ -50,6 +54,7 @@ namespace Aberwyn.Controllers
_context.PizzaOrders.Add(order);
_context.SaveChanges();
_pizzaNotifier.NotifyPizzaSubscribersAsync(pizzaName, customerName);
TempData["Success"] = "Beställningen har lagts!";
return RedirectToAction("Order");

View File

@@ -3,6 +3,9 @@ using Aberwyn.Models;
using Lib.Net.Http.WebPush;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.AspNetCore.Identity;
using Aberwyn.Services;
namespace Aberwyn.Controllers
{
@@ -12,19 +15,37 @@ namespace Aberwyn.Controllers
{
private readonly ApplicationDbContext _context;
private readonly PushNotificationService _notificationService;
private readonly UserManager<ApplicationUser> _userManager;
private readonly PizzaNotificationService _pizzaNotifier;
public PushController(ApplicationDbContext context, PushNotificationService notificationService)
public PushController(ApplicationDbContext context,
PushNotificationService notificationService,
UserManager<ApplicationUser> userManager,
PizzaNotificationService pizzaNotifier)
{
_context = context;
_notificationService = notificationService;
_userManager = userManager;
_pizzaNotifier = pizzaNotifier;
}
[HttpPost("notify-pizza")]
public async Task<IActionResult> NotifyPizza()
{
var count = await _pizzaNotifier.NotifyPizzaSubscribersAsync("Capricciosa", User.Identity.Name);
return Ok(new { message = $"Skickade pizzanotiser till {count} användare." });
}
[HttpPost("subscribe")]
public async Task<IActionResult> Subscribe([FromBody] PushSubscription subscription)
{
var existing = await _context.PushSubscribers
.FirstOrDefaultAsync(s => s.Endpoint == subscription.Endpoint);
var user = await _userManager.GetUserAsync(User);
if (user == null) return Unauthorized();
if (existing == null)
{
@@ -32,7 +53,8 @@ namespace Aberwyn.Controllers
{
Endpoint = subscription.Endpoint,
P256DH = subscription.Keys["p256dh"],
Auth = subscription.Keys["auth"]
Auth = subscription.Keys["auth"],
UserId = user.Id
};
_context.PushSubscribers.Add(newSubscriber);
@@ -62,7 +84,12 @@ namespace Aberwyn.Controllers
return BadRequest("Titel och meddelande krävs.");
}
var subscribers = await _context.PushSubscribers.ToListAsync();
var subscribers = await _context.PushSubscribers
.Include(s => s.User)
.ThenInclude(u => u.Preferences)
.Where(s => s.User != null && s.User.Preferences.NotifyPizza)
.ToListAsync();
var payload = $"{{\"title\":\"{message.Title}\",\"body\":\"{message.Body}\"}}";
int successCount = 0;

View File

@@ -0,0 +1,78 @@
using Microsoft.AspNetCore.Mvc;
using MySql.Data.MySqlClient;
namespace Aberwyn.Controllers
{
[ApiController]
[Route("api/setup")]
public class SetupApiController : ControllerBase
{
[HttpPost("testdb")]
public IActionResult TestDbConnection([FromBody] DbTestRequest request)
{
try
{
var builder = new MySqlConnectionStringBuilder
{
Server = request.Host,
Port = uint.Parse(request.Port),
UserID = request.User,
Password = request.Pass,
Database = "information_schema"
};
using (var conn = new MySqlConnection(builder.ConnectionString))
{
conn.Open();
// Kontrollera om databasen redan finns
var checkCmd = new MySqlCommand("SELECT SCHEMA_NAME FROM SCHEMATA WHERE SCHEMA_NAME = @dbName", conn);
checkCmd.Parameters.AddWithValue("@dbName", request.Db);
var exists = checkCmd.ExecuteScalar();
if (exists != null)
{
return Ok(new
{
success = true,
message = "Anslutning OK och databasen finns redan."
});
}
// Testa skapa en temporär databas
var testDbName = $"testcheck_{Guid.NewGuid():N}".Substring(0, 12);
var createCmd = new MySqlCommand($"CREATE DATABASE `{testDbName}`", conn);
createCmd.ExecuteNonQuery();
var dropCmd = new MySqlCommand($"DROP DATABASE `{testDbName}`", conn);
dropCmd.ExecuteNonQuery();
return Ok(new
{
success = true,
message = "Anslutning OK. Databasen finns inte, men CREATE DATABASE är tillåten."
});
}
}
catch (Exception ex)
{
return Ok(new
{
success = false,
message = ex.Message
});
}
}
public class DbTestRequest
{
public string Host { get; set; }
public string Port { get; set; }
public string Db { get; set; }
public string User { get; set; }
public string Pass { get; set; }
}
}
}

View File

@@ -0,0 +1,170 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.EntityFrameworkCore;
using MySql.Data.MySqlClient;
using System.Text.Json;
using Aberwyn.Data;
using Aberwyn.Models;
namespace Aberwyn.Controllers
{
[Route("setup")]
public class SetupController : Controller
{
private readonly IWebHostEnvironment _env;
private readonly ILogger<SetupController> _logger;
public SetupController(IWebHostEnvironment env, ILogger<SetupController> logger)
{
_env = env;
_logger = logger;
}
public override void OnActionExecuting(ActionExecutingContext context)
{
ViewBag.IsSetupMode = true;
base.OnActionExecuting(context);
}
[HttpGet]
public IActionResult Index() => View(new SetupSettings());
[Authorize(Roles = "Admin")]
[HttpPost("reset")]
public IActionResult Reset()
{
var path = Path.Combine(_env.ContentRootPath, "infrastructure", "setup.json");
var resetSettings = new SetupSettings
{
IsConfigured = false,
DbHost = "",
DbPort = 3306,
DbName = "",
DbUser = "",
DbPassword = "",
AdminUsername = "admin",
AdminEmail = "admin@localhost",
AdminPassword = "Admin123!"
};
var json = JsonSerializer.Serialize(resetSettings, new JsonSerializerOptions { WriteIndented = true });
System.IO.File.WriteAllText(path, json);
return RedirectToAction("Index");
}
[HttpPost("")]
public async Task<IActionResult> Setup([FromBody] SetupSettings model)
{
if (!ModelState.IsValid)
{
var allErrors = ModelState
.Where(e => e.Value.Errors.Count > 0)
.Select(e => new { Field = e.Key, Errors = e.Value.Errors.Select(x => x.ErrorMessage) });
return BadRequest(new { error = "Modellen är ogiltig", details = allErrors });
}
try
{
// Bygg connection string säkert
var baseConnBuilder = new MySqlConnectionStringBuilder
{
Server = model.DbHost,
Port = (uint)model.DbPort,
UserID = model.DbUser,
Password = model.DbPassword,
Database = "information_schema"
};
// Kontrollera om databasen redan finns
using (var conn = new MySqlConnection(baseConnBuilder.ConnectionString))
{
conn.Open();
var cmd = new MySqlCommand("SELECT SCHEMA_NAME FROM SCHEMATA WHERE SCHEMA_NAME = @dbName", conn);
cmd.Parameters.AddWithValue("@dbName", model.DbName);
var exists = cmd.ExecuteScalar();
if (exists == null)
{
try
{
var createCmd = new MySqlCommand($"CREATE DATABASE `{model.DbName}`", conn);
createCmd.ExecuteNonQuery();
}
catch (Exception ex)
{
_logger.LogError(ex, "Kunde inte skapa databasen.");
return BadRequest(new { error = "Databasen finns inte och kunde inte skapas.", details = ex.Message });
}
}
}
// Bygg EF-connection
var efConnBuilder = new MySqlConnectionStringBuilder
{
Server = model.DbHost,
Port = (uint)model.DbPort,
UserID = model.DbUser,
Password = model.DbPassword,
Database = model.DbName
};
var tempProvider = SetupService.BuildTemporaryServices(efConnBuilder.ConnectionString);
using var scope = tempProvider.CreateScope();
// Skapa databastabeller
var db = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
await db.Database.MigrateAsync();
// Sätt konfig-flagga tidigt
model.IsConfigured = true;
// Spara setup.json
var filePath = Path.Combine(_env.ContentRootPath, "infrastructure", "setup.json");
var json = JsonSerializer.Serialize(model, new JsonSerializerOptions { WriteIndented = true });
System.IO.File.WriteAllText(filePath, json);
// Roller och admin
var userManager = scope.ServiceProvider.GetRequiredService<UserManager<ApplicationUser>>();
var roleManager = scope.ServiceProvider.GetRequiredService<RoleManager<IdentityRole>>();
string[] roles = { "Admin", "Chef", "Budget" };
foreach (var role in roles)
{
if (!await roleManager.RoleExistsAsync(role))
await roleManager.CreateAsync(new IdentityRole(role));
}
var existingUser = await userManager.FindByNameAsync(model.AdminUsername);
if (existingUser == null)
{
var adminUser = new ApplicationUser
{
UserName = model.AdminUsername,
Email = model.AdminEmail,
EmailConfirmed = true
};
var result = await userManager.CreateAsync(adminUser, model.AdminPassword);
if (!result.Succeeded)
return BadRequest(new { error = "Kunde inte skapa administratör", details = result.Errors });
await userManager.AddToRoleAsync(adminUser, "Admin");
}
return Ok(new { message = "Installation slutförd!" });
}
catch (Exception ex)
{
_logger.LogError(ex, "Fel vid installation.");
return BadRequest(new { error = "Fel vid installation", details = ex.Message });
}
}
public IActionResult SetupComplete() => View();
}
}

View File

@@ -0,0 +1,61 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Aberwyn.Models;
using System.Threading.Tasks;
using Aberwyn.Data;
namespace Aberwyn.Controllers
{
[Authorize]
public class UserController : Controller
{
private readonly UserManager<ApplicationUser> _userManager;
private readonly ApplicationDbContext _context;
public UserController(UserManager<ApplicationUser> userManager, ApplicationDbContext context)
{
_userManager = userManager;
_context = context;
}
[HttpGet]
public async Task<IActionResult> Profile()
{
var user = await _userManager.GetUserAsync(User);
var prefs = await _context.UserPreferences.FindAsync(user.Id) ?? new UserPreferences();
var model = new UserProfileViewModel
{
Name = user.UserName,
Email = user.Email,
NotifyPizza = prefs.NotifyPizza,
NotifyMenu = prefs.NotifyMenu,
NotifyBudget = prefs.NotifyBudget
};
return View(model);
}
[HttpPost]
public async Task<IActionResult> SaveProfile(UserProfileViewModel model)
{
var user = await _userManager.GetUserAsync(User);
var prefs = await _context.UserPreferences.FindAsync(user.Id);
if (prefs == null)
{
prefs = new UserPreferences { UserId = user.Id };
_context.UserPreferences.Add(prefs);
}
prefs.NotifyPizza = model.NotifyPizza;
prefs.NotifyMenu = model.NotifyMenu;
prefs.NotifyBudget = model.NotifyBudget;
await _context.SaveChangesAsync();
return RedirectToAction("Profile");
}
}
}

View File

@@ -31,6 +31,8 @@ namespace Aberwyn.Data
public DbSet<Meal> Meals { get; set; }
public DbSet<WeeklyMenu> WeeklyMenus { get; set; }
public DbSet<Ingredient> Ingredients { get; set; }
public DbSet<UserPreferences> UserPreferences { get; set; }
public DbSet<StoredPushSubscription> PushSubscriptions { get; set; }
}
}

View File

@@ -1,8 +1,11 @@
using Microsoft.EntityFrameworkCore;
using Aberwyn.Models;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Design;
using Microsoft.Extensions.Configuration;
using System;
using System.IO;
using System.Text.Json;
using static Aberwyn.Data.SetupService;
namespace Aberwyn.Data
{
@@ -10,47 +13,51 @@ namespace Aberwyn.Data
{
public ApplicationDbContext CreateDbContext(string[] args)
{
var basePath = Directory.GetCurrentDirectory();
var config = new ConfigurationBuilder()
.SetBasePath(basePath)
.AddJsonFile("appsettings.json", optional: false)
.AddJsonFile("appsettings.Development.json", optional: true)
.AddEnvironmentVariables()
.Build();
var setup = LoadSetup();
var connectionString = config.GetConnectionString("DefaultConnection");
File.WriteAllText("connection-log.txt", $"Connection string: {connectionString}");
Console.WriteLine($"Anslutningssträng: {connectionString}");
if (string.IsNullOrEmpty(connectionString))
var csBuilder = new MySqlConnector.MySqlConnectionStringBuilder
{
throw new InvalidOperationException("Connection string 'DefaultConnection' not found.");
}
Server = setup.DbHost,
Port = (uint)setup.DbPort,
Database = setup.DbName,
UserID = setup.DbUser,
Password = setup.DbPassword,
AllowUserVariables = true
};
var optionsBuilder = new DbContextOptionsBuilder<ApplicationDbContext>();
optionsBuilder.UseMySql(
connectionString,
new MySqlServerVersion(new Version(8, 0, 36)));
optionsBuilder.UseMySql(csBuilder.ConnectionString, new MySqlServerVersion(new Version(8, 0, 36)));
return new ApplicationDbContext(optionsBuilder.Options);
}
public static ApplicationDbContext CreateWithConfig(IConfiguration config, IHostEnvironment env, bool useProdDb = false)
public static ApplicationDbContext CreateWithConfig(IHostEnvironment env, bool useProdDb = false)
{
var connectionString = useProdDb
? config.GetConnectionString("ProdConnection") // <--- FIX HÄR
: config.GetConnectionString("DefaultConnection");
if (string.IsNullOrWhiteSpace(connectionString))
throw new InvalidOperationException("Connection string saknas.");
var setup = SetupLoader.Load(env);
var connStr = SetupLoader.GetConnectionString(setup);
var optionsBuilder = new DbContextOptionsBuilder<ApplicationDbContext>();
optionsBuilder.UseMySql(connectionString, new MySqlServerVersion(new Version(8, 0, 36)));
optionsBuilder.UseMySql(connStr, new MySqlServerVersion(new Version(8, 0, 36)));
return new ApplicationDbContext(optionsBuilder.Options);
}
private static SetupSettings LoadSetup()
{
var basePath = Directory.GetCurrentDirectory();
var setupPath = Path.Combine(basePath, "infrastructure", "setup.json");
if (!File.Exists(setupPath))
throw new FileNotFoundException("setup.json saknas i infrastructure-mappen.");
var json = File.ReadAllText(setupPath);
var setup = JsonSerializer.Deserialize<SetupSettings>(json);
if (setup == null || !setup.IsConfigured)
throw new InvalidOperationException("setup.json är inte korrekt konfigurerad.");
return setup;
}
}
}

View File

@@ -1,148 +0,0 @@
using MySql.Data.MySqlClient;
using System.Collections.Generic;
using Aberwyn.Models;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting; // Add this namespace
namespace Aberwyn.Data
{
public class BudgetService
{
private readonly IConfiguration _configuration;
private readonly IHostEnvironment _env; // Add this field
public BudgetService(IConfiguration configuration, IHostEnvironment env) // Update constructor
{
_configuration = configuration;
_env = env; // Initialize the environment field
}
public MySqlConnection GetConnection()
{
var connectionString = _env.IsDevelopment() // Use the injected environment variable
? _configuration.GetConnectionString("DefaultConnection")
: _configuration.GetConnectionString("ProductionConnection");
return new MySqlConnection(connectionString);
}
public bool UpdateBudgetItem(BudgetItem item)
{
using (var connection = GetConnection())
{
connection.Open();
string query = @"
UPDATE tblBudgetItems
SET Name = @name, Amount = @amount
WHERE idtblBudgetItems = @id";
using (var cmd = new MySqlCommand(query, connection))
{
cmd.Parameters.AddWithValue("@name", item.Name);
cmd.Parameters.AddWithValue("@amount", item.Amount);
cmd.Parameters.AddWithValue("@id", item.ID);
return cmd.ExecuteNonQuery() > 0; // Returns true if one or more rows are updated
}
}
}
public List<BudgetItem> GetBudgetItems(int month, int year)
{
var budgetItems = new List<BudgetItem>();
using (var connection = GetConnection())
{
connection.Open();
string query = @"
SELECT
b.idtblBudgetItems AS id,
b.Name AS item_name,
b.Amount AS amount,
c1.Name AS category,
b.Month,
b.Year,
b.Description AS description
FROM tblBudgetItems b
LEFT JOIN tblCategories c1 ON b.Category = c1.idtblCategories
WHERE b.Month = @month AND b.Year = @year";
using (var cmd = new MySqlCommand(query, connection))
{
cmd.Parameters.AddWithValue("@month", month);
cmd.Parameters.AddWithValue("@year", year);
using (var reader = cmd.ExecuteReader())
{
while (reader.Read())
{
budgetItems.Add(new BudgetItem
{
ID = reader.GetInt32("id"),
Name = reader.GetString("item_name"), // Updated alias
Amount = reader.GetDecimal("amount"),
Category = reader.GetString("category"),
Month = reader.GetInt32("Month"),
Year = reader.GetInt32("Year"),
Description = reader.IsDBNull(reader.GetOrdinal("description"))
? null
: reader.GetString("description")
});
}
}
}
}
return budgetItems;
}
public bool AddBudgetItem(BudgetItem item)
{
using (var connection = GetConnection())
{
connection.Open();
string query = @"
INSERT INTO tblBudgetItems (Name, Amount, Category, Month, Year)
VALUES (@name, @amount, @category, @month, @year)";
using (var cmd = new MySqlCommand(query, connection))
{
cmd.Parameters.AddWithValue("@name", item.Name);
cmd.Parameters.AddWithValue("@amount", item.Amount);
cmd.Parameters.AddWithValue("@category", item.Category);
cmd.Parameters.AddWithValue("@month", item.Month);
cmd.Parameters.AddWithValue("@year", item.Year);
return cmd.ExecuteNonQuery() > 0; // Returns true if a row was inserted
}
}
}*/
// New method to fetch all categories
public List<string> GetCategories()
{
var categories = new List<string>();
using (var connection = GetConnection())
{
connection.Open();
string query = "SELECT Name FROM tblCategories"; // Adjust based on your table structure
using (var cmd = new MySqlCommand(query, connection))
{
using (var reader = cmd.ExecuteReader())
{
while (reader.Read())
{
categories.Add(reader.GetString("Name"));
}
}
}
}
return categories;
}
}
}

View File

@@ -1,4 +1,5 @@
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Aberwyn.Models;
@@ -6,10 +7,16 @@ namespace Aberwyn.Data
{
public static class IdentityDataInitializer
{
public static async Task SeedData(IServiceProvider serviceProvider)
public static async Task<IdentityResult> SeedData(IServiceProvider serviceProvider, SetupSettings? setup = null)
{
var userManager = serviceProvider.GetRequiredService<UserManager<ApplicationUser>>();
var roleManager = serviceProvider.GetRequiredService<RoleManager<IdentityRole>>();
var config = serviceProvider.GetService<IConfiguration>();
if (setup == null && config != null)
{
setup = config.GetSection("SetupSettings").Get<SetupSettings>() ?? new SetupSettings();
}
string[] roles = { "Admin", "Chef", "Budget" };
@@ -19,26 +26,38 @@ namespace Aberwyn.Data
await roleManager.CreateAsync(new IdentityRole(role));
}
string adminUsername = "admin";
string adminEmail = "admin@localhost";
string password = "Admin123!";
if (await userManager.FindByEmailAsync(adminEmail) == null)
var existingUser = await userManager.FindByEmailAsync(setup.AdminEmail);
if (existingUser == null)
{
var user = new ApplicationUser
{
UserName = adminUsername,
Email = adminEmail,
UserName = setup.AdminUsername,
Email = setup.AdminEmail,
EmailConfirmed = true
};
var result = await userManager.CreateAsync(user, password);
var result = await userManager.CreateAsync(user, setup.AdminPassword);
if (result.Succeeded)
await userManager.AddToRoleAsync(user, "Admin");
return result;
}
else
{
var token = await userManager.GeneratePasswordResetTokenAsync(existingUser);
var result = await userManager.ResetPasswordAsync(existingUser, token, setup.AdminPassword);
if (result.Succeeded)
{
await userManager.AddToRoleAsync(user, "Admin");
var rolesForUser = await userManager.GetRolesAsync(existingUser);
if (!rolesForUser.Contains("Admin"))
await userManager.AddToRoleAsync(existingUser, "Admin");
}
return result;
}
}
}
}

View File

@@ -1,8 +1,11 @@
// Nya versionen av MenuService med Entity Framework
using Aberwyn.Models;
using Microsoft.EntityFrameworkCore;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Formats.Webp;
using SixLabors.ImageSharp.Processing;
using System.Globalization;
using static Aberwyn.Data.SetupService;
namespace Aberwyn.Data
{
@@ -16,20 +19,47 @@ public class MenuService
_context = context;
}
// Detta är en alternativ konstruktör används manuellt vid t.ex. import
public static MenuService CreateWithConfig(IConfiguration config, IHostEnvironment env, bool useProdDb = false)
{
var builder = new DbContextOptionsBuilder<ApplicationDbContext>();
public static MenuService CreateWithSetup(IHostEnvironment env)
{
var setup = SetupLoader.Load(env);
var connStr = SetupLoader.GetConnectionString(setup);
var builder = new DbContextOptionsBuilder<ApplicationDbContext>();
builder.UseMySql(connStr, ServerVersion.AutoDetect(connStr));
var context = new ApplicationDbContext(builder.Options);
return new MenuService(context);
}
public List<WeeklyMenuDto> GetWeeklyMenuDto(int weekNumber, int year)
{
var query = from wm in _context.WeeklyMenus
where wm.WeekNumber == weekNumber && wm.Year == year
join mDinner in _context.Meals on wm.DinnerMealId equals mDinner.Id into dinnerJoin
from mDinner in dinnerJoin.DefaultIfEmpty()
join mLunch in _context.Meals on wm.LunchMealId equals mLunch.Id into lunchJoin
from mLunch in lunchJoin.DefaultIfEmpty()
join mBreakfast in _context.Meals on wm.BreakfastMealId equals mBreakfast.Id into breakfastJoin
from mBreakfast in breakfastJoin.DefaultIfEmpty()
select new WeeklyMenuDto
{
Id = wm.Id,
DayOfWeek = wm.DayOfWeek,
WeekNumber = wm.WeekNumber,
Year = wm.Year,
BreakfastMealId = wm.BreakfastMealId,
LunchMealId = wm.LunchMealId,
DinnerMealId = wm.DinnerMealId,
BreakfastMealName = mBreakfast.Name,
LunchMealName = mLunch.Name,
DinnerMealName = mDinner.Name,
DinnerMealThumbnail = mDinner.ThumbnailData
};
return query.ToList();
}
var connStr = useProdDb
? config.GetConnectionString("ProdConnection")
: config.GetConnectionString("DefaultConnection");
builder.UseMySql(connStr, ServerVersion.AutoDetect(connStr));
var context = new ApplicationDbContext(builder.Options);
return new MenuService(context);
}
public void UpdateWeeklyMenu(MenuViewModel model)
{
var existing = _context.WeeklyMenus
@@ -47,7 +77,30 @@ public class MenuService
}
public void SaveMeal2(Meal meal)
{
if (string.IsNullOrWhiteSpace(meal?.Name)) return;
meal.Name = meal.Name.Trim();
meal.CreatedAt = meal.CreatedAt == default ? DateTime.Now : meal.CreatedAt;
var existing = _context.Meals
.AsNoTracking()
.FirstOrDefault(m => m.Id == meal.Id);
if (existing == null)
{
// Nytt objekt försök behålla ID:t från prod
_context.Entry(meal).State = EntityState.Added;
}
else
{
// Befintlig uppdatera
_context.Meals.Update(meal);
}
_context.SaveChanges();
}
public void SaveMeal(Meal meal)
{
@@ -57,15 +110,90 @@ public class MenuService
meal.CreatedAt = meal.CreatedAt == default ? DateTime.Now : meal.CreatedAt;
if (meal.Id == 0)
{
// Ny måltid
_context.Meals.Add(meal);
}
else
_context.Meals.Update(meal);
{
// Uppdatera existerande utan tracking-krockar
var existing = _context.Meals
.Include(m => m.Ingredients)
.FirstOrDefault(m => m.Id == meal.Id);
if (existing != null)
{
_context.Entry(existing).CurrentValues.SetValues(meal);
// OBS: Ingredienser hanteras separat
}
}
_context.SaveChanges();
}
public List<WeeklyMenu> GetAllWeeklyMenus()
public int GenerateMissingThumbnails()
{
var updatedCount = 0;
var meals = _context.Meals
.Where(m => m.ImageData != null && m.ThumbnailData == null)
.ToList();
foreach (var meal in meals)
{
using var ms = new MemoryStream(meal.ImageData);
using var image = Image.Load(ms);
image.Mutate(x => x.Resize(new ResizeOptions
{
Mode = ResizeMode.Max,
Size = new Size(300, 300)
}));
using var outStream = new MemoryStream();
var encoder = new WebpEncoder
{
Quality = 75
};
image.Save(outStream, encoder);
meal.ThumbnailData = outStream.ToArray();
updatedCount++;
}
_context.SaveChanges();
return updatedCount;
}
public List<Meal> GetMealsSlim(bool includeThumbnail = false)
{
if (includeThumbnail)
{
return _context.Meals
.Select(m => new Meal
{
Id = m.Id,
Name = m.Name,
Description = m.Description,
ThumbnailData = m.ThumbnailData
})
.ToList();
}
else
{
return _context.Meals
.Select(m => new Meal
{
Id = m.Id,
Name = m.Name,
Description = m.Description
})
.ToList();
}
}
public List<WeeklyMenu> GetAllWeeklyMenus()
{
var menus = _context.WeeklyMenus.ToList();
var allMeals = _context.Meals.ToDictionary(m => m.Id, m => m.Name);
@@ -100,10 +228,17 @@ public List<WeeklyMenu> GetAllWeeklyMenus()
{
var existing = _context.Ingredients.Where(i => i.MealId == mealId);
_context.Ingredients.RemoveRange(existing);
foreach (var ing in ingredients)
{
ing.MealId = mealId;
}
_context.Ingredients.AddRange(ingredients);
_context.SaveChanges();
}
public List<Meal> GetMeals()
{
return _context.Meals.ToList();
@@ -112,10 +247,12 @@ public List<WeeklyMenu> GetAllWeeklyMenus()
public List<Meal> GetMealsDetailed()
{
return _context.Meals
.Include(m => m.Ingredients) // 🧠 detta behövs!
.OrderByDescending(m => m.CreatedAt)
.ToList();
}
public Meal GetMealById(int id)
{
var meal = _context.Meals
@@ -141,6 +278,8 @@ public List<WeeklyMenu> GetAllWeeklyMenus()
public void SaveOrUpdateMealWithIngredients(Meal meal)
{
var isNew = meal.Id == 0;
SaveMeal(meal);
if (meal.Ingredients != null && meal.Ingredients.Count > 0)
@@ -149,6 +288,7 @@ public List<WeeklyMenu> GetAllWeeklyMenus()
}
}
public List<Meal> GetMealsByCategory(string category)
{
return _context.Meals
@@ -157,33 +297,49 @@ public List<WeeklyMenu> GetAllWeeklyMenus()
.OrderBy(m => m.Name)
.ToList();
}
public List<WeeklyMenu> GetWeeklyMenu(int weekNumber, int year)
{
var menus = _context.WeeklyMenus
.Where(m => m.WeekNumber == weekNumber && m.Year == year)
.ToList();
public List<WeeklyMenu> GetWeeklyMenu(int weekNumber, int year)
{
var menus = _context.WeeklyMenus
.Where(m => m.WeekNumber == weekNumber && m.Year == year)
.ToList();
var mealIds = menus
.SelectMany(w => new int?[] { w.BreakfastMealId, w.LunchMealId, w.DinnerMealId })
.Where(id => id.HasValue)
.Select(id => id.Value)
.Distinct()
.ToList();
var allMeals = _context.Meals
.Where(m => mealIds.Contains(m.Id))
.Select(m => new {
m.Id,
m.Name,
m.ThumbnailData // Vi tar med detta även om det bara används för middag
})
.ToList()
.ToDictionary(m => m.Id, m => m);
var allMeals = _context.Meals.ToDictionary(m => m.Id, m => m.Name);
foreach (var wm in menus)
{
wm.BreakfastMealName = wm.BreakfastMealId.HasValue && allMeals.TryGetValue(wm.BreakfastMealId.Value, out var breakfast)
? breakfast
: null;
if (wm.BreakfastMealId.HasValue && allMeals.TryGetValue(wm.BreakfastMealId.Value, out var breakfast))
wm.BreakfastMealName = breakfast.Name;
wm.LunchMealName = wm.LunchMealId.HasValue && allMeals.TryGetValue(wm.LunchMealId.Value, out var lunch)
? lunch
: null;
if (wm.LunchMealId.HasValue && allMeals.TryGetValue(wm.LunchMealId.Value, out var lunch))
wm.LunchMealName = lunch.Name;
wm.DinnerMealName = wm.DinnerMealId.HasValue && allMeals.TryGetValue(wm.DinnerMealId.Value, out var dinner)
? dinner
: null;
if (wm.DinnerMealId.HasValue && allMeals.TryGetValue(wm.DinnerMealId.Value, out var dinner))
{
wm.DinnerMealName = dinner.Name;
}
}
return menus;
}
return menus;
}
public List<WeeklyMenu> GetMenuEntriesByDateRange(DateTime startDate, DateTime endDate)
{
var results = new List<WeeklyMenu>();

View File

@@ -0,0 +1,69 @@
using Aberwyn.Data;
using Microsoft.EntityFrameworkCore;
using WebPush;
namespace Aberwyn.Services
{
public class PizzaNotificationService
{
private readonly ApplicationDbContext _context;
private readonly PushNotificationService _push;
public PizzaNotificationService(ApplicationDbContext context, PushNotificationService push)
{
Console.WriteLine("🍕 PizzaNotificationService constructor körs!");
_context = context;
_push = push;
}
public async Task<int> NotifyPizzaSubscribersAsync(string pizzaName = null, string userName = null)
{
var title = "Ny pizzabeställning 🍕";
var body = "En pizza har precis beställts!";
if (!string.IsNullOrWhiteSpace(pizzaName))
body = $"Pizzan '{pizzaName}' har precis beställts!";
if (!string.IsNullOrWhiteSpace(userName))
body += $" (av {userName})";
var payload = $@"{{""title"":""{title}"",""body"":""{body}""}}";
var subscribers = await _context.PushSubscribers
.Include(s => s.User)
.ThenInclude(u => u.Preferences)
.Where(s => s.User != null &&
s.User.Preferences != null &&
s.User.Preferences.NotifyPizza &&
!string.IsNullOrEmpty(s.Endpoint) &&
s.Endpoint.StartsWith("https://"))
.ToListAsync();
int successCount = 0;
foreach (var sub in subscribers)
{
try
{
_push.SendNotification(sub.Endpoint, sub.P256DH, sub.Auth, payload);
successCount++;
}
catch (WebPushException ex) when (ex.StatusCode == System.Net.HttpStatusCode.Gone || ex.StatusCode == System.Net.HttpStatusCode.NotFound)
{
Console.WriteLine($"🗑️ Ogiltig prenumeration tas bort: {sub.Endpoint}");
_context.PushSubscribers.Remove(sub);
await _context.SaveChangesAsync();
}
catch (Exception ex)
{
Console.WriteLine($"❌ Misslyckades att skicka till {sub.Endpoint}: {ex.Message}");
}
}
return successCount;
}
}
}

View File

@@ -0,0 +1,73 @@
using Aberwyn.Models;
using System.Text.Json;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.EntityFrameworkCore;
using Microsoft.AspNetCore.Identity;
using System.IO;
using System.Text.Json;
using Microsoft.Extensions.Hosting;
namespace Aberwyn.Data
{
// SetupService.cs
public class SetupService
{
private readonly IWebHostEnvironment _env;
private readonly string _filePath;
public SetupService(IWebHostEnvironment env)
{
_env = env;
_filePath = Path.Combine(_env.ContentRootPath, "infrastructure", "setup.json");
}
public SetupSettings GetSetup()
{
if (!File.Exists(_filePath))
return new SetupSettings { IsConfigured = false };
var json = File.ReadAllText(_filePath);
return JsonSerializer.Deserialize<SetupSettings>(json) ?? new SetupSettings { IsConfigured = false };
}
internal static IServiceProvider BuildTemporaryServices(string connectionString)
{
var services = new ServiceCollection();
// Konfigurera EF + Identity
services.AddDbContext<ApplicationDbContext>(options =>
options.UseMySql(connectionString, ServerVersion.AutoDetect(connectionString)));
services.AddIdentity<ApplicationUser, IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
// Lägg till en tom konfiguration för att undvika null
services.AddSingleton<IConfiguration>(new ConfigurationBuilder().Build());
// Valfritt: Lägg till loggning om något kräver det
services.AddLogging();
return services.BuildServiceProvider();
}
public static class SetupLoader
{
public static SetupSettings Load(IHostEnvironment env)
{
var path = Path.Combine(env.ContentRootPath, "infrastructure", "setup.json");
var json = File.ReadAllText(path);
return JsonSerializer.Deserialize<SetupSettings>(json)!;
}
public static string GetConnectionString(SetupSettings setup)
{
return $"server={setup.DbHost};port={setup.DbPort};database={setup.DbName};user={setup.DbUser};password={setup.DbPassword}";
}
}
}
}

View File

@@ -15,9 +15,10 @@ EXPOSE 443
FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build
WORKDIR /src
COPY ["Aberwyn.csproj", "."]
COPY ["Aberwyn/Aberwyn.csproj", "Aberwyn/"]
WORKDIR /src/Aberwyn
RUN dotnet restore "Aberwyn.csproj"
COPY . .
COPY Aberwyn/. .
RUN dotnet build "Aberwyn.csproj" -c Release -o /app/build
FROM build AS publish

View File

@@ -2,22 +2,22 @@ version: '3.8'
services:
aberwyn-app:
image: aberwyn:latest
build:
context: ../
dockerfile: Dockerfile
image: 192.168.1.9:3000/tai/aberwyn/aberwyn:latest
container_name: aberwyn-app-prod
ports:
- "8080:80"
environment:
ASPNETCORE_ENVIRONMENT: Production
DB_HOST: aberwyn-mysql-prod
DB_NAME: aberwyn_prod
DB_USER: aberwyn
DB_PASSWORD: 3edc4RFV
depends_on:
- mysql
- aberwyn-mysql-prod
networks:
- aberwyn-net
mysql:
aberwyn-mysql-prod:
image: mysql:8
container_name: aberwyn-mysql-prod
restart: always

View File

@@ -0,0 +1,11 @@
{
"AdminUsername": "admin",
"AdminEmail": "admin@localhost",
"AdminPassword": "Admin123!",
"IsConfigured": true,
"DbHost": "192.168.1.108",
"DbPort": 3306,
"DbName": "lewel_prod",
"DbUser": "lewel",
"DbPassword": "W542.Hl;)%ta"
}

View File

@@ -0,0 +1,676 @@
// <auto-generated />
using System;
using Aberwyn.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace Aberwyn.Migrations
{
[DbContext(typeof(ApplicationDbContext))]
[Migration("20250604220420_AssignedToNullable")]
partial class AssignedToNullable
{
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "6.0.36")
.HasAnnotation("Relational:MaxIdentifierLength", 64);
modelBuilder.Entity("Aberwyn.Models.ApplicationUser", b =>
{
b.Property<string>("Id")
.HasColumnType("varchar(255)");
b.Property<int>("AccessFailedCount")
.HasColumnType("int");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken()
.HasColumnType("longtext");
b.Property<string>("Email")
.HasMaxLength(256)
.HasColumnType("varchar(256)");
b.Property<bool>("EmailConfirmed")
.HasColumnType("tinyint(1)");
b.Property<bool>("LockoutEnabled")
.HasColumnType("tinyint(1)");
b.Property<DateTimeOffset?>("LockoutEnd")
.HasColumnType("datetime(6)");
b.Property<string>("NormalizedEmail")
.HasMaxLength(256)
.HasColumnType("varchar(256)");
b.Property<string>("NormalizedUserName")
.HasMaxLength(256)
.HasColumnType("varchar(256)");
b.Property<string>("PasswordHash")
.HasColumnType("longtext");
b.Property<string>("PhoneNumber")
.HasColumnType("longtext");
b.Property<bool>("PhoneNumberConfirmed")
.HasColumnType("tinyint(1)");
b.Property<string>("SecurityStamp")
.HasColumnType("longtext");
b.Property<bool>("TwoFactorEnabled")
.HasColumnType("tinyint(1)");
b.Property<string>("UserName")
.HasMaxLength(256)
.HasColumnType("varchar(256)");
b.HasKey("Id");
b.HasIndex("NormalizedEmail")
.HasDatabaseName("EmailIndex");
b.HasIndex("NormalizedUserName")
.IsUnique()
.HasDatabaseName("UserNameIndex");
b.ToTable("AspNetUsers", (string)null);
});
modelBuilder.Entity("Aberwyn.Models.AppSetting", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<string>("Key")
.IsRequired()
.HasColumnType("longtext");
b.Property<string>("Value")
.IsRequired()
.HasColumnType("longtext");
b.HasKey("Id");
b.ToTable("AppSettings");
});
modelBuilder.Entity("Aberwyn.Models.BudgetCategory", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<int?>("BudgetCategoryDefinitionId")
.HasColumnType("int");
b.Property<int>("BudgetPeriodId")
.HasColumnType("int");
b.Property<string>("Color")
.IsRequired()
.HasColumnType("longtext");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("longtext");
b.Property<int>("Order")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("BudgetCategoryDefinitionId");
b.HasIndex("BudgetPeriodId");
b.ToTable("BudgetCategories");
});
modelBuilder.Entity("Aberwyn.Models.BudgetCategoryDefinition", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<string>("Color")
.IsRequired()
.HasColumnType("longtext");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("longtext");
b.HasKey("Id");
b.ToTable("BudgetCategoryDefinitions");
});
modelBuilder.Entity("Aberwyn.Models.BudgetItem", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<decimal>("Amount")
.HasColumnType("decimal(65,30)");
b.Property<int>("BudgetCategoryId")
.HasColumnType("int");
b.Property<int?>("BudgetItemDefinitionId")
.HasColumnType("int");
b.Property<bool>("IncludeInSummary")
.HasColumnType("tinyint(1)");
b.Property<bool>("IsExpense")
.HasColumnType("tinyint(1)");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("longtext");
b.Property<int>("Order")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("BudgetCategoryId");
b.HasIndex("BudgetItemDefinitionId");
b.ToTable("BudgetItems");
});
modelBuilder.Entity("Aberwyn.Models.BudgetItemDefinition", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<string>("DefaultCategory")
.HasColumnType("longtext");
b.Property<bool>("IncludeInSummary")
.HasColumnType("tinyint(1)");
b.Property<bool>("IsExpense")
.HasColumnType("tinyint(1)");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("longtext");
b.HasKey("Id");
b.ToTable("BudgetItemDefinitions");
});
modelBuilder.Entity("Aberwyn.Models.BudgetPeriod", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<int>("Month")
.HasColumnType("int");
b.Property<int>("Order")
.HasColumnType("int");
b.Property<int>("Year")
.HasColumnType("int");
b.HasKey("Id");
b.ToTable("BudgetPeriods");
});
modelBuilder.Entity("Aberwyn.Models.Ingredient", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<string>("Item")
.IsRequired()
.HasColumnType("longtext");
b.Property<int>("MealId")
.HasColumnType("int");
b.Property<string>("Quantity")
.IsRequired()
.HasColumnType("longtext");
b.HasKey("Id");
b.HasIndex("MealId");
b.ToTable("Ingredients");
});
modelBuilder.Entity("Aberwyn.Models.Meal", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<string>("CarbType")
.HasColumnType("longtext");
b.Property<string>("Category")
.HasColumnType("longtext");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime(6)");
b.Property<string>("Description")
.HasColumnType("longtext");
b.Property<byte[]>("ImageData")
.HasColumnType("longblob");
b.Property<string>("ImageMimeType")
.HasColumnType("longtext");
b.Property<string>("ImageUrl")
.HasColumnType("longtext");
b.Property<string>("Instructions")
.HasColumnType("longtext");
b.Property<bool>("IsAvailable")
.HasColumnType("tinyint(1)");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("longtext");
b.Property<string>("ProteinType")
.HasColumnType("longtext");
b.Property<string>("RecipeUrl")
.HasColumnType("longtext");
b.HasKey("Id");
b.ToTable("Meals");
});
modelBuilder.Entity("Aberwyn.Models.PizzaOrder", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<string>("CustomerName")
.IsRequired()
.HasColumnType("longtext");
b.Property<string>("IngredientsJson")
.HasColumnType("longtext");
b.Property<DateTime>("OrderedAt")
.HasColumnType("datetime(6)");
b.Property<string>("PizzaName")
.IsRequired()
.HasColumnType("longtext");
b.Property<string>("Status")
.IsRequired()
.HasColumnType("longtext");
b.HasKey("Id");
b.ToTable("PizzaOrders");
});
modelBuilder.Entity("Aberwyn.Models.PushSubscriber", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<string>("Auth")
.IsRequired()
.HasColumnType("longtext");
b.Property<string>("Endpoint")
.IsRequired()
.HasColumnType("longtext");
b.Property<string>("P256DH")
.IsRequired()
.HasColumnType("longtext");
b.HasKey("Id");
b.ToTable("PushSubscribers");
});
modelBuilder.Entity("Aberwyn.Models.TodoTask", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<string>("AssignedTo")
.HasColumnType("longtext");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime(6)");
b.Property<string>("Description")
.IsRequired()
.HasColumnType("longtext");
b.Property<bool>("IsArchived")
.HasColumnType("tinyint(1)");
b.Property<int>("Priority")
.HasColumnType("int");
b.Property<string>("Status")
.IsRequired()
.HasColumnType("longtext");
b.Property<string>("Tags")
.IsRequired()
.HasColumnType("longtext");
b.Property<string>("Title")
.IsRequired()
.HasColumnType("longtext");
b.HasKey("Id");
b.ToTable("TodoTasks");
});
modelBuilder.Entity("Aberwyn.Models.WeeklyMenu", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<int?>("BreakfastMealId")
.HasColumnType("int");
b.Property<string>("Cook")
.HasColumnType("longtext");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime(6)");
b.Property<int>("DayOfWeek")
.HasColumnType("int");
b.Property<int?>("DinnerMealId")
.HasColumnType("int");
b.Property<int?>("LunchMealId")
.HasColumnType("int");
b.Property<int>("WeekNumber")
.HasColumnType("int");
b.Property<int>("Year")
.HasColumnType("int");
b.HasKey("Id");
b.ToTable("WeeklyMenu", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b =>
{
b.Property<string>("Id")
.HasColumnType("varchar(255)");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken()
.HasColumnType("longtext");
b.Property<string>("Name")
.HasMaxLength(256)
.HasColumnType("varchar(256)");
b.Property<string>("NormalizedName")
.HasMaxLength(256)
.HasColumnType("varchar(256)");
b.HasKey("Id");
b.HasIndex("NormalizedName")
.IsUnique()
.HasDatabaseName("RoleNameIndex");
b.ToTable("AspNetRoles", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<string>("ClaimType")
.HasColumnType("longtext");
b.Property<string>("ClaimValue")
.HasColumnType("longtext");
b.Property<string>("RoleId")
.IsRequired()
.HasColumnType("varchar(255)");
b.HasKey("Id");
b.HasIndex("RoleId");
b.ToTable("AspNetRoleClaims", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<string>("ClaimType")
.HasColumnType("longtext");
b.Property<string>("ClaimValue")
.HasColumnType("longtext");
b.Property<string>("UserId")
.IsRequired()
.HasColumnType("varchar(255)");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("AspNetUserClaims", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
{
b.Property<string>("LoginProvider")
.HasColumnType("varchar(255)");
b.Property<string>("ProviderKey")
.HasColumnType("varchar(255)");
b.Property<string>("ProviderDisplayName")
.HasColumnType("longtext");
b.Property<string>("UserId")
.IsRequired()
.HasColumnType("varchar(255)");
b.HasKey("LoginProvider", "ProviderKey");
b.HasIndex("UserId");
b.ToTable("AspNetUserLogins", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
{
b.Property<string>("UserId")
.HasColumnType("varchar(255)");
b.Property<string>("RoleId")
.HasColumnType("varchar(255)");
b.HasKey("UserId", "RoleId");
b.HasIndex("RoleId");
b.ToTable("AspNetUserRoles", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
{
b.Property<string>("UserId")
.HasColumnType("varchar(255)");
b.Property<string>("LoginProvider")
.HasColumnType("varchar(255)");
b.Property<string>("Name")
.HasColumnType("varchar(255)");
b.Property<string>("Value")
.HasColumnType("longtext");
b.HasKey("UserId", "LoginProvider", "Name");
b.ToTable("AspNetUserTokens", (string)null);
});
modelBuilder.Entity("Aberwyn.Models.BudgetCategory", b =>
{
b.HasOne("Aberwyn.Models.BudgetCategoryDefinition", "Definition")
.WithMany()
.HasForeignKey("BudgetCategoryDefinitionId");
b.HasOne("Aberwyn.Models.BudgetPeriod", "BudgetPeriod")
.WithMany("Categories")
.HasForeignKey("BudgetPeriodId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("BudgetPeriod");
b.Navigation("Definition");
});
modelBuilder.Entity("Aberwyn.Models.BudgetItem", b =>
{
b.HasOne("Aberwyn.Models.BudgetCategory", "BudgetCategory")
.WithMany("Items")
.HasForeignKey("BudgetCategoryId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Aberwyn.Models.BudgetItemDefinition", "BudgetItemDefinition")
.WithMany()
.HasForeignKey("BudgetItemDefinitionId");
b.Navigation("BudgetCategory");
b.Navigation("BudgetItemDefinition");
});
modelBuilder.Entity("Aberwyn.Models.Ingredient", b =>
{
b.HasOne("Aberwyn.Models.Meal", null)
.WithMany("Ingredients")
.HasForeignKey("MealId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
{
b.HasOne("Aberwyn.Models.ApplicationUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
{
b.HasOne("Aberwyn.Models.ApplicationUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Aberwyn.Models.ApplicationUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
{
b.HasOne("Aberwyn.Models.ApplicationUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Aberwyn.Models.BudgetCategory", b =>
{
b.Navigation("Items");
});
modelBuilder.Entity("Aberwyn.Models.BudgetPeriod", b =>
{
b.Navigation("Categories");
});
modelBuilder.Entity("Aberwyn.Models.Meal", b =>
{
b.Navigation("Ingredients");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,43 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Aberwyn.Migrations
{
public partial class AssignedToNullable : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AlterColumn<string>(
name: "AssignedTo",
table: "TodoTasks",
type: "longtext",
nullable: true,
oldClrType: typeof(string),
oldType: "longtext")
.Annotation("MySql:CharSet", "utf8mb4")
.OldAnnotation("MySql:CharSet", "utf8mb4");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.UpdateData(
table: "TodoTasks",
keyColumn: "AssignedTo",
keyValue: null,
column: "AssignedTo",
value: "");
migrationBuilder.AlterColumn<string>(
name: "AssignedTo",
table: "TodoTasks",
type: "longtext",
nullable: false,
oldClrType: typeof(string),
oldType: "longtext",
oldNullable: true)
.Annotation("MySql:CharSet", "utf8mb4")
.OldAnnotation("MySql:CharSet", "utf8mb4");
}
}
}

View File

@@ -0,0 +1,679 @@
// <auto-generated />
using System;
using Aberwyn.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace Aberwyn.Migrations
{
[DbContext(typeof(ApplicationDbContext))]
[Migration("20250605062804_AddThumbnailToMeal")]
partial class AddThumbnailToMeal
{
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "6.0.36")
.HasAnnotation("Relational:MaxIdentifierLength", 64);
modelBuilder.Entity("Aberwyn.Models.ApplicationUser", b =>
{
b.Property<string>("Id")
.HasColumnType("varchar(255)");
b.Property<int>("AccessFailedCount")
.HasColumnType("int");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken()
.HasColumnType("longtext");
b.Property<string>("Email")
.HasMaxLength(256)
.HasColumnType("varchar(256)");
b.Property<bool>("EmailConfirmed")
.HasColumnType("tinyint(1)");
b.Property<bool>("LockoutEnabled")
.HasColumnType("tinyint(1)");
b.Property<DateTimeOffset?>("LockoutEnd")
.HasColumnType("datetime(6)");
b.Property<string>("NormalizedEmail")
.HasMaxLength(256)
.HasColumnType("varchar(256)");
b.Property<string>("NormalizedUserName")
.HasMaxLength(256)
.HasColumnType("varchar(256)");
b.Property<string>("PasswordHash")
.HasColumnType("longtext");
b.Property<string>("PhoneNumber")
.HasColumnType("longtext");
b.Property<bool>("PhoneNumberConfirmed")
.HasColumnType("tinyint(1)");
b.Property<string>("SecurityStamp")
.HasColumnType("longtext");
b.Property<bool>("TwoFactorEnabled")
.HasColumnType("tinyint(1)");
b.Property<string>("UserName")
.HasMaxLength(256)
.HasColumnType("varchar(256)");
b.HasKey("Id");
b.HasIndex("NormalizedEmail")
.HasDatabaseName("EmailIndex");
b.HasIndex("NormalizedUserName")
.IsUnique()
.HasDatabaseName("UserNameIndex");
b.ToTable("AspNetUsers", (string)null);
});
modelBuilder.Entity("Aberwyn.Models.AppSetting", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<string>("Key")
.IsRequired()
.HasColumnType("longtext");
b.Property<string>("Value")
.IsRequired()
.HasColumnType("longtext");
b.HasKey("Id");
b.ToTable("AppSettings");
});
modelBuilder.Entity("Aberwyn.Models.BudgetCategory", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<int?>("BudgetCategoryDefinitionId")
.HasColumnType("int");
b.Property<int>("BudgetPeriodId")
.HasColumnType("int");
b.Property<string>("Color")
.IsRequired()
.HasColumnType("longtext");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("longtext");
b.Property<int>("Order")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("BudgetCategoryDefinitionId");
b.HasIndex("BudgetPeriodId");
b.ToTable("BudgetCategories");
});
modelBuilder.Entity("Aberwyn.Models.BudgetCategoryDefinition", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<string>("Color")
.IsRequired()
.HasColumnType("longtext");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("longtext");
b.HasKey("Id");
b.ToTable("BudgetCategoryDefinitions");
});
modelBuilder.Entity("Aberwyn.Models.BudgetItem", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<decimal>("Amount")
.HasColumnType("decimal(65,30)");
b.Property<int>("BudgetCategoryId")
.HasColumnType("int");
b.Property<int?>("BudgetItemDefinitionId")
.HasColumnType("int");
b.Property<bool>("IncludeInSummary")
.HasColumnType("tinyint(1)");
b.Property<bool>("IsExpense")
.HasColumnType("tinyint(1)");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("longtext");
b.Property<int>("Order")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("BudgetCategoryId");
b.HasIndex("BudgetItemDefinitionId");
b.ToTable("BudgetItems");
});
modelBuilder.Entity("Aberwyn.Models.BudgetItemDefinition", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<string>("DefaultCategory")
.HasColumnType("longtext");
b.Property<bool>("IncludeInSummary")
.HasColumnType("tinyint(1)");
b.Property<bool>("IsExpense")
.HasColumnType("tinyint(1)");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("longtext");
b.HasKey("Id");
b.ToTable("BudgetItemDefinitions");
});
modelBuilder.Entity("Aberwyn.Models.BudgetPeriod", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<int>("Month")
.HasColumnType("int");
b.Property<int>("Order")
.HasColumnType("int");
b.Property<int>("Year")
.HasColumnType("int");
b.HasKey("Id");
b.ToTable("BudgetPeriods");
});
modelBuilder.Entity("Aberwyn.Models.Ingredient", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<string>("Item")
.IsRequired()
.HasColumnType("longtext");
b.Property<int>("MealId")
.HasColumnType("int");
b.Property<string>("Quantity")
.IsRequired()
.HasColumnType("longtext");
b.HasKey("Id");
b.HasIndex("MealId");
b.ToTable("Ingredients");
});
modelBuilder.Entity("Aberwyn.Models.Meal", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<string>("CarbType")
.HasColumnType("longtext");
b.Property<string>("Category")
.HasColumnType("longtext");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime(6)");
b.Property<string>("Description")
.HasColumnType("longtext");
b.Property<byte[]>("ImageData")
.HasColumnType("longblob");
b.Property<string>("ImageMimeType")
.HasColumnType("longtext");
b.Property<string>("ImageUrl")
.HasColumnType("longtext");
b.Property<string>("Instructions")
.HasColumnType("longtext");
b.Property<bool>("IsAvailable")
.HasColumnType("tinyint(1)");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("longtext");
b.Property<string>("ProteinType")
.HasColumnType("longtext");
b.Property<string>("RecipeUrl")
.HasColumnType("longtext");
b.Property<byte[]>("ThumbnailData")
.HasColumnType("longblob");
b.HasKey("Id");
b.ToTable("Meals");
});
modelBuilder.Entity("Aberwyn.Models.PizzaOrder", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<string>("CustomerName")
.IsRequired()
.HasColumnType("longtext");
b.Property<string>("IngredientsJson")
.HasColumnType("longtext");
b.Property<DateTime>("OrderedAt")
.HasColumnType("datetime(6)");
b.Property<string>("PizzaName")
.IsRequired()
.HasColumnType("longtext");
b.Property<string>("Status")
.IsRequired()
.HasColumnType("longtext");
b.HasKey("Id");
b.ToTable("PizzaOrders");
});
modelBuilder.Entity("Aberwyn.Models.PushSubscriber", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<string>("Auth")
.IsRequired()
.HasColumnType("longtext");
b.Property<string>("Endpoint")
.IsRequired()
.HasColumnType("longtext");
b.Property<string>("P256DH")
.IsRequired()
.HasColumnType("longtext");
b.HasKey("Id");
b.ToTable("PushSubscribers");
});
modelBuilder.Entity("Aberwyn.Models.TodoTask", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<string>("AssignedTo")
.HasColumnType("longtext");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime(6)");
b.Property<string>("Description")
.IsRequired()
.HasColumnType("longtext");
b.Property<bool>("IsArchived")
.HasColumnType("tinyint(1)");
b.Property<int>("Priority")
.HasColumnType("int");
b.Property<string>("Status")
.IsRequired()
.HasColumnType("longtext");
b.Property<string>("Tags")
.IsRequired()
.HasColumnType("longtext");
b.Property<string>("Title")
.IsRequired()
.HasColumnType("longtext");
b.HasKey("Id");
b.ToTable("TodoTasks");
});
modelBuilder.Entity("Aberwyn.Models.WeeklyMenu", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<int?>("BreakfastMealId")
.HasColumnType("int");
b.Property<string>("Cook")
.HasColumnType("longtext");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime(6)");
b.Property<int>("DayOfWeek")
.HasColumnType("int");
b.Property<int?>("DinnerMealId")
.HasColumnType("int");
b.Property<int?>("LunchMealId")
.HasColumnType("int");
b.Property<int>("WeekNumber")
.HasColumnType("int");
b.Property<int>("Year")
.HasColumnType("int");
b.HasKey("Id");
b.ToTable("WeeklyMenu", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b =>
{
b.Property<string>("Id")
.HasColumnType("varchar(255)");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken()
.HasColumnType("longtext");
b.Property<string>("Name")
.HasMaxLength(256)
.HasColumnType("varchar(256)");
b.Property<string>("NormalizedName")
.HasMaxLength(256)
.HasColumnType("varchar(256)");
b.HasKey("Id");
b.HasIndex("NormalizedName")
.IsUnique()
.HasDatabaseName("RoleNameIndex");
b.ToTable("AspNetRoles", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<string>("ClaimType")
.HasColumnType("longtext");
b.Property<string>("ClaimValue")
.HasColumnType("longtext");
b.Property<string>("RoleId")
.IsRequired()
.HasColumnType("varchar(255)");
b.HasKey("Id");
b.HasIndex("RoleId");
b.ToTable("AspNetRoleClaims", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<string>("ClaimType")
.HasColumnType("longtext");
b.Property<string>("ClaimValue")
.HasColumnType("longtext");
b.Property<string>("UserId")
.IsRequired()
.HasColumnType("varchar(255)");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("AspNetUserClaims", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
{
b.Property<string>("LoginProvider")
.HasColumnType("varchar(255)");
b.Property<string>("ProviderKey")
.HasColumnType("varchar(255)");
b.Property<string>("ProviderDisplayName")
.HasColumnType("longtext");
b.Property<string>("UserId")
.IsRequired()
.HasColumnType("varchar(255)");
b.HasKey("LoginProvider", "ProviderKey");
b.HasIndex("UserId");
b.ToTable("AspNetUserLogins", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
{
b.Property<string>("UserId")
.HasColumnType("varchar(255)");
b.Property<string>("RoleId")
.HasColumnType("varchar(255)");
b.HasKey("UserId", "RoleId");
b.HasIndex("RoleId");
b.ToTable("AspNetUserRoles", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
{
b.Property<string>("UserId")
.HasColumnType("varchar(255)");
b.Property<string>("LoginProvider")
.HasColumnType("varchar(255)");
b.Property<string>("Name")
.HasColumnType("varchar(255)");
b.Property<string>("Value")
.HasColumnType("longtext");
b.HasKey("UserId", "LoginProvider", "Name");
b.ToTable("AspNetUserTokens", (string)null);
});
modelBuilder.Entity("Aberwyn.Models.BudgetCategory", b =>
{
b.HasOne("Aberwyn.Models.BudgetCategoryDefinition", "Definition")
.WithMany()
.HasForeignKey("BudgetCategoryDefinitionId");
b.HasOne("Aberwyn.Models.BudgetPeriod", "BudgetPeriod")
.WithMany("Categories")
.HasForeignKey("BudgetPeriodId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("BudgetPeriod");
b.Navigation("Definition");
});
modelBuilder.Entity("Aberwyn.Models.BudgetItem", b =>
{
b.HasOne("Aberwyn.Models.BudgetCategory", "BudgetCategory")
.WithMany("Items")
.HasForeignKey("BudgetCategoryId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Aberwyn.Models.BudgetItemDefinition", "BudgetItemDefinition")
.WithMany()
.HasForeignKey("BudgetItemDefinitionId");
b.Navigation("BudgetCategory");
b.Navigation("BudgetItemDefinition");
});
modelBuilder.Entity("Aberwyn.Models.Ingredient", b =>
{
b.HasOne("Aberwyn.Models.Meal", null)
.WithMany("Ingredients")
.HasForeignKey("MealId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
{
b.HasOne("Aberwyn.Models.ApplicationUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
{
b.HasOne("Aberwyn.Models.ApplicationUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Aberwyn.Models.ApplicationUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
{
b.HasOne("Aberwyn.Models.ApplicationUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Aberwyn.Models.BudgetCategory", b =>
{
b.Navigation("Items");
});
modelBuilder.Entity("Aberwyn.Models.BudgetPeriod", b =>
{
b.Navigation("Categories");
});
modelBuilder.Entity("Aberwyn.Models.Meal", b =>
{
b.Navigation("Ingredients");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,26 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Aberwyn.Migrations
{
public partial class AddThumbnailToMeal : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<byte[]>(
name: "ThumbnailData",
table: "Meals",
type: "longblob",
nullable: true);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "ThumbnailData",
table: "Meals");
}
}
}

View File

@@ -0,0 +1,755 @@
// <auto-generated />
using System;
using Aberwyn.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace Aberwyn.Migrations
{
[DbContext(typeof(ApplicationDbContext))]
[Migration("20250606061016_AddUserPreferencesAndPushSubscriptions")]
partial class AddUserPreferencesAndPushSubscriptions
{
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "6.0.36")
.HasAnnotation("Relational:MaxIdentifierLength", 64);
modelBuilder.Entity("Aberwyn.Models.ApplicationUser", b =>
{
b.Property<string>("Id")
.HasColumnType("varchar(255)");
b.Property<int>("AccessFailedCount")
.HasColumnType("int");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken()
.HasColumnType("longtext");
b.Property<string>("Email")
.HasMaxLength(256)
.HasColumnType("varchar(256)");
b.Property<bool>("EmailConfirmed")
.HasColumnType("tinyint(1)");
b.Property<bool>("LockoutEnabled")
.HasColumnType("tinyint(1)");
b.Property<DateTimeOffset?>("LockoutEnd")
.HasColumnType("datetime(6)");
b.Property<string>("NormalizedEmail")
.HasMaxLength(256)
.HasColumnType("varchar(256)");
b.Property<string>("NormalizedUserName")
.HasMaxLength(256)
.HasColumnType("varchar(256)");
b.Property<string>("PasswordHash")
.HasColumnType("longtext");
b.Property<string>("PhoneNumber")
.HasColumnType("longtext");
b.Property<bool>("PhoneNumberConfirmed")
.HasColumnType("tinyint(1)");
b.Property<string>("SecurityStamp")
.HasColumnType("longtext");
b.Property<bool>("TwoFactorEnabled")
.HasColumnType("tinyint(1)");
b.Property<string>("UserName")
.HasMaxLength(256)
.HasColumnType("varchar(256)");
b.HasKey("Id");
b.HasIndex("NormalizedEmail")
.HasDatabaseName("EmailIndex");
b.HasIndex("NormalizedUserName")
.IsUnique()
.HasDatabaseName("UserNameIndex");
b.ToTable("AspNetUsers", (string)null);
});
modelBuilder.Entity("Aberwyn.Models.AppSetting", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<string>("Key")
.IsRequired()
.HasColumnType("longtext");
b.Property<string>("Value")
.IsRequired()
.HasColumnType("longtext");
b.HasKey("Id");
b.ToTable("AppSettings");
});
modelBuilder.Entity("Aberwyn.Models.BudgetCategory", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<int?>("BudgetCategoryDefinitionId")
.HasColumnType("int");
b.Property<int>("BudgetPeriodId")
.HasColumnType("int");
b.Property<string>("Color")
.IsRequired()
.HasColumnType("longtext");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("longtext");
b.Property<int>("Order")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("BudgetCategoryDefinitionId");
b.HasIndex("BudgetPeriodId");
b.ToTable("BudgetCategories");
});
modelBuilder.Entity("Aberwyn.Models.BudgetCategoryDefinition", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<string>("Color")
.IsRequired()
.HasColumnType("longtext");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("longtext");
b.HasKey("Id");
b.ToTable("BudgetCategoryDefinitions");
});
modelBuilder.Entity("Aberwyn.Models.BudgetItem", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<decimal>("Amount")
.HasColumnType("decimal(65,30)");
b.Property<int>("BudgetCategoryId")
.HasColumnType("int");
b.Property<int?>("BudgetItemDefinitionId")
.HasColumnType("int");
b.Property<bool>("IncludeInSummary")
.HasColumnType("tinyint(1)");
b.Property<bool>("IsExpense")
.HasColumnType("tinyint(1)");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("longtext");
b.Property<int>("Order")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("BudgetCategoryId");
b.HasIndex("BudgetItemDefinitionId");
b.ToTable("BudgetItems");
});
modelBuilder.Entity("Aberwyn.Models.BudgetItemDefinition", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<string>("DefaultCategory")
.HasColumnType("longtext");
b.Property<bool>("IncludeInSummary")
.HasColumnType("tinyint(1)");
b.Property<bool>("IsExpense")
.HasColumnType("tinyint(1)");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("longtext");
b.HasKey("Id");
b.ToTable("BudgetItemDefinitions");
});
modelBuilder.Entity("Aberwyn.Models.BudgetPeriod", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<int>("Month")
.HasColumnType("int");
b.Property<int>("Order")
.HasColumnType("int");
b.Property<int>("Year")
.HasColumnType("int");
b.HasKey("Id");
b.ToTable("BudgetPeriods");
});
modelBuilder.Entity("Aberwyn.Models.Ingredient", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<string>("Item")
.IsRequired()
.HasColumnType("longtext");
b.Property<int>("MealId")
.HasColumnType("int");
b.Property<string>("Quantity")
.IsRequired()
.HasColumnType("longtext");
b.HasKey("Id");
b.HasIndex("MealId");
b.ToTable("Ingredients");
});
modelBuilder.Entity("Aberwyn.Models.Meal", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<string>("CarbType")
.HasColumnType("longtext");
b.Property<string>("Category")
.HasColumnType("longtext");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime(6)");
b.Property<string>("Description")
.HasColumnType("longtext");
b.Property<byte[]>("ImageData")
.HasColumnType("longblob");
b.Property<string>("ImageMimeType")
.HasColumnType("longtext");
b.Property<string>("ImageUrl")
.HasColumnType("longtext");
b.Property<string>("Instructions")
.HasColumnType("longtext");
b.Property<bool>("IsAvailable")
.HasColumnType("tinyint(1)");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("longtext");
b.Property<string>("ProteinType")
.HasColumnType("longtext");
b.Property<string>("RecipeUrl")
.HasColumnType("longtext");
b.Property<byte[]>("ThumbnailData")
.HasColumnType("longblob");
b.HasKey("Id");
b.ToTable("Meals");
});
modelBuilder.Entity("Aberwyn.Models.PizzaOrder", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<string>("CustomerName")
.IsRequired()
.HasColumnType("longtext");
b.Property<string>("IngredientsJson")
.HasColumnType("longtext");
b.Property<DateTime>("OrderedAt")
.HasColumnType("datetime(6)");
b.Property<string>("PizzaName")
.IsRequired()
.HasColumnType("longtext");
b.Property<string>("Status")
.IsRequired()
.HasColumnType("longtext");
b.HasKey("Id");
b.ToTable("PizzaOrders");
});
modelBuilder.Entity("Aberwyn.Models.PushSubscriber", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<string>("Auth")
.IsRequired()
.HasColumnType("longtext");
b.Property<string>("Endpoint")
.IsRequired()
.HasColumnType("longtext");
b.Property<string>("P256DH")
.IsRequired()
.HasColumnType("longtext");
b.HasKey("Id");
b.ToTable("PushSubscribers");
});
modelBuilder.Entity("Aberwyn.Models.StoredPushSubscription", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<string>("Auth")
.IsRequired()
.HasColumnType("longtext");
b.Property<string>("Endpoint")
.IsRequired()
.HasColumnType("longtext");
b.Property<string>("P256DH")
.IsRequired()
.HasColumnType("longtext");
b.Property<string>("UserId")
.IsRequired()
.HasColumnType("varchar(255)");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("PushSubscriptions");
});
modelBuilder.Entity("Aberwyn.Models.TodoTask", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<string>("AssignedTo")
.HasColumnType("longtext");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime(6)");
b.Property<string>("Description")
.IsRequired()
.HasColumnType("longtext");
b.Property<bool>("IsArchived")
.HasColumnType("tinyint(1)");
b.Property<int>("Priority")
.HasColumnType("int");
b.Property<string>("Status")
.IsRequired()
.HasColumnType("longtext");
b.Property<string>("Tags")
.IsRequired()
.HasColumnType("longtext");
b.Property<string>("Title")
.IsRequired()
.HasColumnType("longtext");
b.HasKey("Id");
b.ToTable("TodoTasks");
});
modelBuilder.Entity("Aberwyn.Models.UserPreferences", b =>
{
b.Property<string>("UserId")
.HasColumnType("varchar(255)");
b.Property<bool>("NotifyBudget")
.HasColumnType("tinyint(1)");
b.Property<bool>("NotifyMenu")
.HasColumnType("tinyint(1)");
b.Property<bool>("NotifyPizza")
.HasColumnType("tinyint(1)");
b.HasKey("UserId");
b.ToTable("UserPreferences");
});
modelBuilder.Entity("Aberwyn.Models.WeeklyMenu", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<int?>("BreakfastMealId")
.HasColumnType("int");
b.Property<string>("Cook")
.HasColumnType("longtext");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime(6)");
b.Property<int>("DayOfWeek")
.HasColumnType("int");
b.Property<int?>("DinnerMealId")
.HasColumnType("int");
b.Property<int?>("LunchMealId")
.HasColumnType("int");
b.Property<int>("WeekNumber")
.HasColumnType("int");
b.Property<int>("Year")
.HasColumnType("int");
b.HasKey("Id");
b.ToTable("WeeklyMenu", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b =>
{
b.Property<string>("Id")
.HasColumnType("varchar(255)");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken()
.HasColumnType("longtext");
b.Property<string>("Name")
.HasMaxLength(256)
.HasColumnType("varchar(256)");
b.Property<string>("NormalizedName")
.HasMaxLength(256)
.HasColumnType("varchar(256)");
b.HasKey("Id");
b.HasIndex("NormalizedName")
.IsUnique()
.HasDatabaseName("RoleNameIndex");
b.ToTable("AspNetRoles", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<string>("ClaimType")
.HasColumnType("longtext");
b.Property<string>("ClaimValue")
.HasColumnType("longtext");
b.Property<string>("RoleId")
.IsRequired()
.HasColumnType("varchar(255)");
b.HasKey("Id");
b.HasIndex("RoleId");
b.ToTable("AspNetRoleClaims", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<string>("ClaimType")
.HasColumnType("longtext");
b.Property<string>("ClaimValue")
.HasColumnType("longtext");
b.Property<string>("UserId")
.IsRequired()
.HasColumnType("varchar(255)");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("AspNetUserClaims", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
{
b.Property<string>("LoginProvider")
.HasColumnType("varchar(255)");
b.Property<string>("ProviderKey")
.HasColumnType("varchar(255)");
b.Property<string>("ProviderDisplayName")
.HasColumnType("longtext");
b.Property<string>("UserId")
.IsRequired()
.HasColumnType("varchar(255)");
b.HasKey("LoginProvider", "ProviderKey");
b.HasIndex("UserId");
b.ToTable("AspNetUserLogins", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
{
b.Property<string>("UserId")
.HasColumnType("varchar(255)");
b.Property<string>("RoleId")
.HasColumnType("varchar(255)");
b.HasKey("UserId", "RoleId");
b.HasIndex("RoleId");
b.ToTable("AspNetUserRoles", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
{
b.Property<string>("UserId")
.HasColumnType("varchar(255)");
b.Property<string>("LoginProvider")
.HasColumnType("varchar(255)");
b.Property<string>("Name")
.HasColumnType("varchar(255)");
b.Property<string>("Value")
.HasColumnType("longtext");
b.HasKey("UserId", "LoginProvider", "Name");
b.ToTable("AspNetUserTokens", (string)null);
});
modelBuilder.Entity("Aberwyn.Models.BudgetCategory", b =>
{
b.HasOne("Aberwyn.Models.BudgetCategoryDefinition", "Definition")
.WithMany()
.HasForeignKey("BudgetCategoryDefinitionId");
b.HasOne("Aberwyn.Models.BudgetPeriod", "BudgetPeriod")
.WithMany("Categories")
.HasForeignKey("BudgetPeriodId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("BudgetPeriod");
b.Navigation("Definition");
});
modelBuilder.Entity("Aberwyn.Models.BudgetItem", b =>
{
b.HasOne("Aberwyn.Models.BudgetCategory", "BudgetCategory")
.WithMany("Items")
.HasForeignKey("BudgetCategoryId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Aberwyn.Models.BudgetItemDefinition", "BudgetItemDefinition")
.WithMany()
.HasForeignKey("BudgetItemDefinitionId");
b.Navigation("BudgetCategory");
b.Navigation("BudgetItemDefinition");
});
modelBuilder.Entity("Aberwyn.Models.Ingredient", b =>
{
b.HasOne("Aberwyn.Models.Meal", null)
.WithMany("Ingredients")
.HasForeignKey("MealId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Aberwyn.Models.StoredPushSubscription", b =>
{
b.HasOne("Aberwyn.Models.ApplicationUser", "User")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("User");
});
modelBuilder.Entity("Aberwyn.Models.UserPreferences", b =>
{
b.HasOne("Aberwyn.Models.ApplicationUser", "User")
.WithOne("Preferences")
.HasForeignKey("Aberwyn.Models.UserPreferences", "UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("User");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
{
b.HasOne("Aberwyn.Models.ApplicationUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
{
b.HasOne("Aberwyn.Models.ApplicationUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Aberwyn.Models.ApplicationUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
{
b.HasOne("Aberwyn.Models.ApplicationUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Aberwyn.Models.ApplicationUser", b =>
{
b.Navigation("Preferences")
.IsRequired();
});
modelBuilder.Entity("Aberwyn.Models.BudgetCategory", b =>
{
b.Navigation("Items");
});
modelBuilder.Entity("Aberwyn.Models.BudgetPeriod", b =>
{
b.Navigation("Categories");
});
modelBuilder.Entity("Aberwyn.Models.Meal", b =>
{
b.Navigation("Ingredients");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,76 @@
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Aberwyn.Migrations
{
public partial class AddUserPreferencesAndPushSubscriptions : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "PushSubscriptions",
columns: table => new
{
Id = table.Column<int>(type: "int", nullable: false)
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
UserId = table.Column<string>(type: "varchar(255)", nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
Endpoint = table.Column<string>(type: "longtext", nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
P256DH = table.Column<string>(type: "longtext", nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
Auth = table.Column<string>(type: "longtext", nullable: false)
.Annotation("MySql:CharSet", "utf8mb4")
},
constraints: table =>
{
table.PrimaryKey("PK_PushSubscriptions", x => x.Id);
table.ForeignKey(
name: "FK_PushSubscriptions_AspNetUsers_UserId",
column: x => x.UserId,
principalTable: "AspNetUsers",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
})
.Annotation("MySql:CharSet", "utf8mb4");
migrationBuilder.CreateTable(
name: "UserPreferences",
columns: table => new
{
UserId = table.Column<string>(type: "varchar(255)", nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
NotifyPizza = table.Column<bool>(type: "tinyint(1)", nullable: false),
NotifyMenu = table.Column<bool>(type: "tinyint(1)", nullable: false),
NotifyBudget = table.Column<bool>(type: "tinyint(1)", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_UserPreferences", x => x.UserId);
table.ForeignKey(
name: "FK_UserPreferences_AspNetUsers_UserId",
column: x => x.UserId,
principalTable: "AspNetUsers",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
})
.Annotation("MySql:CharSet", "utf8mb4");
migrationBuilder.CreateIndex(
name: "IX_PushSubscriptions_UserId",
table: "PushSubscriptions",
column: "UserId");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "PushSubscriptions");
migrationBuilder.DropTable(
name: "UserPreferences");
}
}
}

View File

@@ -0,0 +1,772 @@
// <auto-generated />
using System;
using Aberwyn.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace Aberwyn.Migrations
{
[DbContext(typeof(ApplicationDbContext))]
[Migration("20250606100439_AddPushSubscriberUserLink")]
partial class AddPushSubscriberUserLink
{
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "6.0.36")
.HasAnnotation("Relational:MaxIdentifierLength", 64);
modelBuilder.Entity("Aberwyn.Models.ApplicationUser", b =>
{
b.Property<string>("Id")
.HasColumnType("varchar(255)");
b.Property<int>("AccessFailedCount")
.HasColumnType("int");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken()
.HasColumnType("longtext");
b.Property<string>("Email")
.HasMaxLength(256)
.HasColumnType("varchar(256)");
b.Property<bool>("EmailConfirmed")
.HasColumnType("tinyint(1)");
b.Property<bool>("LockoutEnabled")
.HasColumnType("tinyint(1)");
b.Property<DateTimeOffset?>("LockoutEnd")
.HasColumnType("datetime(6)");
b.Property<string>("NormalizedEmail")
.HasMaxLength(256)
.HasColumnType("varchar(256)");
b.Property<string>("NormalizedUserName")
.HasMaxLength(256)
.HasColumnType("varchar(256)");
b.Property<string>("PasswordHash")
.HasColumnType("longtext");
b.Property<string>("PhoneNumber")
.HasColumnType("longtext");
b.Property<bool>("PhoneNumberConfirmed")
.HasColumnType("tinyint(1)");
b.Property<string>("SecurityStamp")
.HasColumnType("longtext");
b.Property<bool>("TwoFactorEnabled")
.HasColumnType("tinyint(1)");
b.Property<string>("UserName")
.HasMaxLength(256)
.HasColumnType("varchar(256)");
b.HasKey("Id");
b.HasIndex("NormalizedEmail")
.HasDatabaseName("EmailIndex");
b.HasIndex("NormalizedUserName")
.IsUnique()
.HasDatabaseName("UserNameIndex");
b.ToTable("AspNetUsers", (string)null);
});
modelBuilder.Entity("Aberwyn.Models.AppSetting", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<string>("Key")
.IsRequired()
.HasColumnType("longtext");
b.Property<string>("Value")
.IsRequired()
.HasColumnType("longtext");
b.HasKey("Id");
b.ToTable("AppSettings");
});
modelBuilder.Entity("Aberwyn.Models.BudgetCategory", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<int?>("BudgetCategoryDefinitionId")
.HasColumnType("int");
b.Property<int>("BudgetPeriodId")
.HasColumnType("int");
b.Property<string>("Color")
.IsRequired()
.HasColumnType("longtext");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("longtext");
b.Property<int>("Order")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("BudgetCategoryDefinitionId");
b.HasIndex("BudgetPeriodId");
b.ToTable("BudgetCategories");
});
modelBuilder.Entity("Aberwyn.Models.BudgetCategoryDefinition", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<string>("Color")
.IsRequired()
.HasColumnType("longtext");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("longtext");
b.HasKey("Id");
b.ToTable("BudgetCategoryDefinitions");
});
modelBuilder.Entity("Aberwyn.Models.BudgetItem", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<decimal>("Amount")
.HasColumnType("decimal(65,30)");
b.Property<int>("BudgetCategoryId")
.HasColumnType("int");
b.Property<int?>("BudgetItemDefinitionId")
.HasColumnType("int");
b.Property<bool>("IncludeInSummary")
.HasColumnType("tinyint(1)");
b.Property<bool>("IsExpense")
.HasColumnType("tinyint(1)");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("longtext");
b.Property<int>("Order")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("BudgetCategoryId");
b.HasIndex("BudgetItemDefinitionId");
b.ToTable("BudgetItems");
});
modelBuilder.Entity("Aberwyn.Models.BudgetItemDefinition", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<string>("DefaultCategory")
.HasColumnType("longtext");
b.Property<bool>("IncludeInSummary")
.HasColumnType("tinyint(1)");
b.Property<bool>("IsExpense")
.HasColumnType("tinyint(1)");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("longtext");
b.HasKey("Id");
b.ToTable("BudgetItemDefinitions");
});
modelBuilder.Entity("Aberwyn.Models.BudgetPeriod", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<int>("Month")
.HasColumnType("int");
b.Property<int>("Order")
.HasColumnType("int");
b.Property<int>("Year")
.HasColumnType("int");
b.HasKey("Id");
b.ToTable("BudgetPeriods");
});
modelBuilder.Entity("Aberwyn.Models.Ingredient", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<string>("Item")
.IsRequired()
.HasColumnType("longtext");
b.Property<int>("MealId")
.HasColumnType("int");
b.Property<string>("Quantity")
.IsRequired()
.HasColumnType("longtext");
b.HasKey("Id");
b.HasIndex("MealId");
b.ToTable("Ingredients");
});
modelBuilder.Entity("Aberwyn.Models.Meal", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<string>("CarbType")
.HasColumnType("longtext");
b.Property<string>("Category")
.HasColumnType("longtext");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime(6)");
b.Property<string>("Description")
.HasColumnType("longtext");
b.Property<byte[]>("ImageData")
.HasColumnType("longblob");
b.Property<string>("ImageMimeType")
.HasColumnType("longtext");
b.Property<string>("ImageUrl")
.HasColumnType("longtext");
b.Property<string>("Instructions")
.HasColumnType("longtext");
b.Property<bool>("IsAvailable")
.HasColumnType("tinyint(1)");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("longtext");
b.Property<string>("ProteinType")
.HasColumnType("longtext");
b.Property<string>("RecipeUrl")
.HasColumnType("longtext");
b.Property<byte[]>("ThumbnailData")
.HasColumnType("longblob");
b.HasKey("Id");
b.ToTable("Meals");
});
modelBuilder.Entity("Aberwyn.Models.PizzaOrder", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<string>("CustomerName")
.IsRequired()
.HasColumnType("longtext");
b.Property<string>("IngredientsJson")
.HasColumnType("longtext");
b.Property<DateTime>("OrderedAt")
.HasColumnType("datetime(6)");
b.Property<string>("PizzaName")
.IsRequired()
.HasColumnType("longtext");
b.Property<string>("Status")
.IsRequired()
.HasColumnType("longtext");
b.HasKey("Id");
b.ToTable("PizzaOrders");
});
modelBuilder.Entity("Aberwyn.Models.PushSubscriber", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<string>("Auth")
.IsRequired()
.HasColumnType("longtext");
b.Property<string>("Endpoint")
.IsRequired()
.HasColumnType("longtext");
b.Property<string>("P256DH")
.IsRequired()
.HasColumnType("longtext");
b.Property<string>("UserId")
.IsRequired()
.HasColumnType("varchar(255)");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("PushSubscribers");
});
modelBuilder.Entity("Aberwyn.Models.StoredPushSubscription", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<string>("Auth")
.IsRequired()
.HasColumnType("longtext");
b.Property<string>("Endpoint")
.IsRequired()
.HasColumnType("longtext");
b.Property<string>("P256DH")
.IsRequired()
.HasColumnType("longtext");
b.Property<string>("UserId")
.IsRequired()
.HasColumnType("varchar(255)");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("PushSubscriptions");
});
modelBuilder.Entity("Aberwyn.Models.TodoTask", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<string>("AssignedTo")
.HasColumnType("longtext");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime(6)");
b.Property<string>("Description")
.IsRequired()
.HasColumnType("longtext");
b.Property<bool>("IsArchived")
.HasColumnType("tinyint(1)");
b.Property<int>("Priority")
.HasColumnType("int");
b.Property<string>("Status")
.IsRequired()
.HasColumnType("longtext");
b.Property<string>("Tags")
.IsRequired()
.HasColumnType("longtext");
b.Property<string>("Title")
.IsRequired()
.HasColumnType("longtext");
b.HasKey("Id");
b.ToTable("TodoTasks");
});
modelBuilder.Entity("Aberwyn.Models.UserPreferences", b =>
{
b.Property<string>("UserId")
.HasColumnType("varchar(255)");
b.Property<bool>("NotifyBudget")
.HasColumnType("tinyint(1)");
b.Property<bool>("NotifyMenu")
.HasColumnType("tinyint(1)");
b.Property<bool>("NotifyPizza")
.HasColumnType("tinyint(1)");
b.HasKey("UserId");
b.ToTable("UserPreferences");
});
modelBuilder.Entity("Aberwyn.Models.WeeklyMenu", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<int?>("BreakfastMealId")
.HasColumnType("int");
b.Property<string>("Cook")
.HasColumnType("longtext");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime(6)");
b.Property<int>("DayOfWeek")
.HasColumnType("int");
b.Property<int?>("DinnerMealId")
.HasColumnType("int");
b.Property<int?>("LunchMealId")
.HasColumnType("int");
b.Property<int>("WeekNumber")
.HasColumnType("int");
b.Property<int>("Year")
.HasColumnType("int");
b.HasKey("Id");
b.ToTable("WeeklyMenu", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b =>
{
b.Property<string>("Id")
.HasColumnType("varchar(255)");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken()
.HasColumnType("longtext");
b.Property<string>("Name")
.HasMaxLength(256)
.HasColumnType("varchar(256)");
b.Property<string>("NormalizedName")
.HasMaxLength(256)
.HasColumnType("varchar(256)");
b.HasKey("Id");
b.HasIndex("NormalizedName")
.IsUnique()
.HasDatabaseName("RoleNameIndex");
b.ToTable("AspNetRoles", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<string>("ClaimType")
.HasColumnType("longtext");
b.Property<string>("ClaimValue")
.HasColumnType("longtext");
b.Property<string>("RoleId")
.IsRequired()
.HasColumnType("varchar(255)");
b.HasKey("Id");
b.HasIndex("RoleId");
b.ToTable("AspNetRoleClaims", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<string>("ClaimType")
.HasColumnType("longtext");
b.Property<string>("ClaimValue")
.HasColumnType("longtext");
b.Property<string>("UserId")
.IsRequired()
.HasColumnType("varchar(255)");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("AspNetUserClaims", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
{
b.Property<string>("LoginProvider")
.HasColumnType("varchar(255)");
b.Property<string>("ProviderKey")
.HasColumnType("varchar(255)");
b.Property<string>("ProviderDisplayName")
.HasColumnType("longtext");
b.Property<string>("UserId")
.IsRequired()
.HasColumnType("varchar(255)");
b.HasKey("LoginProvider", "ProviderKey");
b.HasIndex("UserId");
b.ToTable("AspNetUserLogins", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
{
b.Property<string>("UserId")
.HasColumnType("varchar(255)");
b.Property<string>("RoleId")
.HasColumnType("varchar(255)");
b.HasKey("UserId", "RoleId");
b.HasIndex("RoleId");
b.ToTable("AspNetUserRoles", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
{
b.Property<string>("UserId")
.HasColumnType("varchar(255)");
b.Property<string>("LoginProvider")
.HasColumnType("varchar(255)");
b.Property<string>("Name")
.HasColumnType("varchar(255)");
b.Property<string>("Value")
.HasColumnType("longtext");
b.HasKey("UserId", "LoginProvider", "Name");
b.ToTable("AspNetUserTokens", (string)null);
});
modelBuilder.Entity("Aberwyn.Models.BudgetCategory", b =>
{
b.HasOne("Aberwyn.Models.BudgetCategoryDefinition", "Definition")
.WithMany()
.HasForeignKey("BudgetCategoryDefinitionId");
b.HasOne("Aberwyn.Models.BudgetPeriod", "BudgetPeriod")
.WithMany("Categories")
.HasForeignKey("BudgetPeriodId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("BudgetPeriod");
b.Navigation("Definition");
});
modelBuilder.Entity("Aberwyn.Models.BudgetItem", b =>
{
b.HasOne("Aberwyn.Models.BudgetCategory", "BudgetCategory")
.WithMany("Items")
.HasForeignKey("BudgetCategoryId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Aberwyn.Models.BudgetItemDefinition", "BudgetItemDefinition")
.WithMany()
.HasForeignKey("BudgetItemDefinitionId");
b.Navigation("BudgetCategory");
b.Navigation("BudgetItemDefinition");
});
modelBuilder.Entity("Aberwyn.Models.Ingredient", b =>
{
b.HasOne("Aberwyn.Models.Meal", null)
.WithMany("Ingredients")
.HasForeignKey("MealId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Aberwyn.Models.PushSubscriber", b =>
{
b.HasOne("Aberwyn.Models.ApplicationUser", "User")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("User");
});
modelBuilder.Entity("Aberwyn.Models.StoredPushSubscription", b =>
{
b.HasOne("Aberwyn.Models.ApplicationUser", "User")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("User");
});
modelBuilder.Entity("Aberwyn.Models.UserPreferences", b =>
{
b.HasOne("Aberwyn.Models.ApplicationUser", "User")
.WithOne("Preferences")
.HasForeignKey("Aberwyn.Models.UserPreferences", "UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("User");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
{
b.HasOne("Aberwyn.Models.ApplicationUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
{
b.HasOne("Aberwyn.Models.ApplicationUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Aberwyn.Models.ApplicationUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
{
b.HasOne("Aberwyn.Models.ApplicationUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Aberwyn.Models.ApplicationUser", b =>
{
b.Navigation("Preferences")
.IsRequired();
});
modelBuilder.Entity("Aberwyn.Models.BudgetCategory", b =>
{
b.Navigation("Items");
});
modelBuilder.Entity("Aberwyn.Models.BudgetPeriod", b =>
{
b.Navigation("Categories");
});
modelBuilder.Entity("Aberwyn.Models.Meal", b =>
{
b.Navigation("Ingredients");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,48 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Aberwyn.Migrations
{
public partial class AddPushSubscriberUserLink : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "UserId",
table: "PushSubscribers",
type: "varchar(255)",
nullable: false,
defaultValue: "")
.Annotation("MySql:CharSet", "utf8mb4");
migrationBuilder.CreateIndex(
name: "IX_PushSubscribers_UserId",
table: "PushSubscribers",
column: "UserId");
migrationBuilder.AddForeignKey(
name: "FK_PushSubscribers_AspNetUsers_UserId",
table: "PushSubscribers",
column: "UserId",
principalTable: "AspNetUsers",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_PushSubscribers_AspNetUsers_UserId",
table: "PushSubscribers");
migrationBuilder.DropIndex(
name: "IX_PushSubscribers_UserId",
table: "PushSubscribers");
migrationBuilder.DropColumn(
name: "UserId",
table: "PushSubscribers");
}
}
}

View File

@@ -301,6 +301,9 @@ namespace Aberwyn.Migrations
b.Property<string>("RecipeUrl")
.HasColumnType("longtext");
b.Property<byte[]>("ThumbnailData")
.HasColumnType("longblob");
b.HasKey("Id");
b.ToTable("Meals");
@@ -353,11 +356,46 @@ namespace Aberwyn.Migrations
.IsRequired()
.HasColumnType("longtext");
b.Property<string>("UserId")
.IsRequired()
.HasColumnType("varchar(255)");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("PushSubscribers");
});
modelBuilder.Entity("Aberwyn.Models.StoredPushSubscription", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<string>("Auth")
.IsRequired()
.HasColumnType("longtext");
b.Property<string>("Endpoint")
.IsRequired()
.HasColumnType("longtext");
b.Property<string>("P256DH")
.IsRequired()
.HasColumnType("longtext");
b.Property<string>("UserId")
.IsRequired()
.HasColumnType("varchar(255)");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("PushSubscriptions");
});
modelBuilder.Entity("Aberwyn.Models.TodoTask", b =>
{
b.Property<int>("Id")
@@ -365,7 +403,6 @@ namespace Aberwyn.Migrations
.HasColumnType("int");
b.Property<string>("AssignedTo")
.IsRequired()
.HasColumnType("longtext");
b.Property<DateTime>("CreatedAt")
@@ -398,6 +435,25 @@ namespace Aberwyn.Migrations
b.ToTable("TodoTasks");
});
modelBuilder.Entity("Aberwyn.Models.UserPreferences", b =>
{
b.Property<string>("UserId")
.HasColumnType("varchar(255)");
b.Property<bool>("NotifyBudget")
.HasColumnType("tinyint(1)");
b.Property<bool>("NotifyMenu")
.HasColumnType("tinyint(1)");
b.Property<bool>("NotifyPizza")
.HasColumnType("tinyint(1)");
b.HasKey("UserId");
b.ToTable("UserPreferences");
});
modelBuilder.Entity("Aberwyn.Models.WeeklyMenu", b =>
{
b.Property<int>("Id")
@@ -604,6 +660,39 @@ namespace Aberwyn.Migrations
.IsRequired();
});
modelBuilder.Entity("Aberwyn.Models.PushSubscriber", b =>
{
b.HasOne("Aberwyn.Models.ApplicationUser", "User")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("User");
});
modelBuilder.Entity("Aberwyn.Models.StoredPushSubscription", b =>
{
b.HasOne("Aberwyn.Models.ApplicationUser", "User")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("User");
});
modelBuilder.Entity("Aberwyn.Models.UserPreferences", b =>
{
b.HasOne("Aberwyn.Models.ApplicationUser", "User")
.WithOne("Preferences")
.HasForeignKey("Aberwyn.Models.UserPreferences", "UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("User");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
@@ -655,6 +744,12 @@ namespace Aberwyn.Migrations
.IsRequired();
});
modelBuilder.Entity("Aberwyn.Models.ApplicationUser", b =>
{
b.Navigation("Preferences")
.IsRequired();
});
modelBuilder.Entity("Aberwyn.Models.BudgetCategory", b =>
{
b.Navigation("Items");

View File

@@ -13,9 +13,9 @@
public string Title { get; set; }
public string Description { get; set; }
public int Priority { get; set; }
public string Status { get; set; } // e.g., "ideas", "doing", "done"
public string Status { get; set; } = "ideas";
public DateTime CreatedAt { get; set; }
public string AssignedTo { get; set; } // Ex: namn eller användarnamn
public string? AssignedTo { get; set; }
public bool IsArchived { get; set; }
public string Tags { get; set; } // Komma-separerad t.ex. "frontend,bug"
}

View File

@@ -4,6 +4,8 @@
public class ApplicationUser : IdentityUser
{
public virtual UserPreferences Preferences { get; set; }
// Lägg till egna fält om du vill, t.ex. public string DisplayName { get; set; }
}
}

View File

@@ -1,4 +1,5 @@
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using Aberwyn.Data;
using Aberwyn.Models;
@@ -12,6 +13,25 @@ namespace Aberwyn.Models
public int WeekNumber { get; set; } // Week number for the menu
public int Year { get; set; } // Year for the menu
}
public class WeeklyMenuDto
{
public int Id { get; set; }
public int DayOfWeek { get; set; }
public int WeekNumber { get; set; }
public int Year { get; set; }
public int? BreakfastMealId { get; set; }
public int? LunchMealId { get; set; }
public int? DinnerMealId { get; set; }
public string? BreakfastMealName { get; set; }
public string? LunchMealName { get; set; }
public string? DinnerMealName { get; set; }
public byte[]? DinnerMealThumbnail { get; set; }
}
public class WeeklyMenu
{
public int Id { get; set; }
@@ -40,26 +60,31 @@ public class WeeklyMenu
public string LunchMealName { get; set; }
public string DinnerMealName { get; set; }
}
public class Meal
{
public int Id { get; set; }
public string Name { get; set; } // Behåll som obligatorisk
public class Meal
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int Id { get; set; }
public string? Description { get; set; }
public string? ProteinType { get; set; }
public string? Category { get; set; }
public string? CarbType { get; set; }
public string? RecipeUrl { get; set; }
public string? ImageUrl { get; set; }
public bool IsAvailable { get; set; }
public DateTime CreatedAt { get; set; }
public string Name { get; set; } // Behåll som obligatorisk
public byte[]? ImageData { get; set; } // 👈 Viktigt!
public string? ImageMimeType { get; set; } // 👈 Viktigt!
public string? Instructions { get; set; } // 👈 Viktigt!
public string? Description { get; set; }
public string? ProteinType { get; set; }
public string? Category { get; set; }
public string? CarbType { get; set; }
public string? RecipeUrl { get; set; }
public string? ImageUrl { get; set; }
public bool IsAvailable { get; set; }
public DateTime CreatedAt { get; set; }
public byte[]? ThumbnailData { get; set; }
public List<Ingredient> Ingredients { get; set; } = new();
}
public byte[]? ImageData { get; set; }
public string? ImageMimeType { get; set; }
public string? Instructions { get; set; }
public List<Ingredient> Ingredients { get; set; } = new();
}
public class Ingredient
{
@@ -79,20 +104,39 @@ public class Meal
public string? ImageData { get; set; } // base64
public string? ImageMimeType { get; set; }
public static MealDto FromMeal(Meal meal)
public static MealListDto FromMeal(Meal meal, bool includeThumbnail = false)
{
return new MealDto
return new MealListDto
{
Id = meal.Id,
Name = meal.Name,
Category = meal.Category,
IsAvailable = meal.IsAvailable,
ImageUrl = meal.ImageUrl,
ImageMimeType = meal.ImageMimeType,
ImageData = meal.ImageData != null ? Convert.ToBase64String(meal.ImageData) : null
Description = meal.Description,
ThumbnailData = includeThumbnail && meal.ThumbnailData != null
? Convert.ToBase64String(meal.ThumbnailData)
: null
};
}
}
}
public class MealListDto
{
public int Id { get; set; }
public string Name { get; set; } = "";
public string? Description { get; set; }
public string? ThumbnailData { get; set; }
public static MealListDto FromMeal(Meal meal, bool includeThumbnail = false)
{
return new MealListDto
{
Id = meal.Id,
Name = meal.Name,
Description = meal.Description,
ThumbnailData = includeThumbnail && meal.ThumbnailData != null
? Convert.ToBase64String(meal.ThumbnailData)
: null
};
}
}
}

View File

@@ -6,6 +6,8 @@
public string Endpoint { get; set; }
public string P256DH { get; set; }
public string Auth { get; set; }
public string UserId { get; set; }
public virtual ApplicationUser User { get; set; }
}
public class PushMessageDto
{

View File

@@ -0,0 +1,17 @@
namespace Aberwyn.Models
{
public class SetupSettings
{
public string AdminUsername { get; set; } = "admin";
public string AdminEmail { get; set; } = "admin@localhost";
public string AdminPassword { get; set; } = "Admin123!";
public bool IsConfigured { get; set; }
public string DbHost { get; set; }
public int DbPort { get; set; }
public string DbName { get; set; }
public string DbUser { get; set; }
public string DbPassword { get; set; }
}
}

View File

@@ -1,10 +0,0 @@
namespace Aberwyn.Models
{
public class User
{
public int UserID { get; set; }
public string Username { get; set; }
public string Name { get; set; }
}
}

View File

@@ -0,0 +1,47 @@
using System.ComponentModel.DataAnnotations;
namespace Aberwyn.Models
{
public class UserModel
{
public int UserID { get; set; }
public string Username { get; set; }
public string Name { get; set; }
}
public class UserPreferences
{
[Key]
public string UserId { get; set; }
public bool NotifyPizza { get; set; }
public bool NotifyMenu { get; set; }
public bool NotifyBudget { get; set; }
public virtual ApplicationUser User { get; set; }
}
public class StoredPushSubscription
{
[Key]
public int Id { get; set; }
public string UserId { get; set; }
public string Endpoint { get; set; }
public string P256DH { get; set; }
public string Auth { get; set; }
public virtual ApplicationUser User { get; set; }
}
public class UserProfileViewModel
{
public string Name { get; set; }
public string Email { get; set; }
public bool NotifyPizza { get; set; }
public bool NotifyMenu { get; set; }
public bool NotifyBudget { get; set; }
}
}

View File

@@ -13,9 +13,8 @@ namespace Aberwyn.Models
public List<RecentMenuEntry> RecentEntries { get; set; } = new();
public List<WeeklyMenu> WeeklyMenus { get; set; } = new();
public List<User> AvailableCooks { get; set; } = new();
public List<UserModel> AvailableCooks { get; set; } = new();
// Ny lista för översikt
public List<WeeklyMenuViewModel> PreviousWeeks { get; set; } = new();
public class RecentMenuEntry
{

View File

@@ -1,14 +1,13 @@
using Microsoft.EntityFrameworkCore;
using Aberwyn.Data;
using System.Text;
using System.Globalization;
using Microsoft.AspNetCore.Localization;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Aberwyn.Models;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using System.Text.Json;
using Aberwyn.Services;
var config = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
@@ -16,7 +15,6 @@ var config = new ConfigurationBuilder()
.AddJsonFile($"appsettings.{Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") ?? "Production"}.json", optional: true)
.AddEnvironmentVariables()
.Build();
Console.WriteLine("📦 DEBUG: laddad raw-connectionstring: " + config.GetConnectionString("DefaultConnection"));
var builder = WebApplication.CreateBuilder(new WebApplicationOptions
{
@@ -24,22 +22,60 @@ var builder = WebApplication.CreateBuilder(new WebApplicationOptions
EnvironmentName = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") ?? "Production"
});
builder.Configuration.AddConfiguration(config);
Console.WriteLine("🌍 Miljö: " + builder.Environment.EnvironmentName);
Console.WriteLine("🔗 Connection string: " + builder.Configuration.GetConnectionString("DefaultConnection"));
// Add services to the container.
// Läser setup.json eller skapar en ny tom om den inte finns
var setupFilePath = Path.Combine("infrastructure", "setup.json");
if (!File.Exists(setupFilePath))
{
var initialSettings = new SetupSettings
{
IsConfigured = false,
DbPort = 3306
};
var initialJson = JsonSerializer.Serialize(initialSettings, new JsonSerializerOptions { WriteIndented = true });
Directory.CreateDirectory(Path.GetDirectoryName(setupFilePath)!); // säkerställ att mappen finns
File.WriteAllText(setupFilePath, initialJson);
}
SetupSettings setup;
try
{
using var jsonStream = File.OpenRead(setupFilePath);
setup = JsonSerializer.Deserialize<SetupSettings>(jsonStream)!;
if (setup.IsConfigured)
{
if (string.IsNullOrWhiteSpace(setup.DbHost) ||
string.IsNullOrWhiteSpace(setup.DbName) ||
string.IsNullOrWhiteSpace(setup.DbUser) ||
string.IsNullOrWhiteSpace(setup.DbPassword))
{
throw new Exception("Databasinställningarna är ofullständiga.");
}
}
}
catch (Exception ex)
{
Console.WriteLine($"❌ Fel vid läsning av setup.json: {ex.Message}");
setup = new SetupSettings { IsConfigured = false };
}
// Add services to the container
builder.Services.AddControllersWithViews()
.AddJsonOptions(opts =>
{
// Beh<65>ll propertynamn som i C#-klassen (PascalCase)
opts.JsonSerializerOptions.PropertyNamingPolicy = null;
// Ignorera null-v<>rden vid serialisering
opts.JsonSerializerOptions.IgnoreNullValues = true;
});
builder.Services.AddSession(options =>
{
options.IdleTimeout = TimeSpan.FromHours(1);
options.IdleTimeout = TimeSpan.FromDays(30);
options.Cookie.HttpOnly = true;
options.Cookie.IsEssential = true;
});
@@ -47,24 +83,43 @@ builder.Services.AddSession(options =>
builder.Services.AddRazorPages();
builder.Services.AddHttpClient();
// Configure your DbContext with MySQLs
Console.WriteLine("🔗 Connection string: " + builder.Configuration.GetConnectionString("DefaultConnection"));
builder.Services.AddDbContext<ApplicationDbContext>(options =>
options.UseMySql(
builder.Configuration.GetConnectionString("DefaultConnection"),
new MySqlServerVersion(new Version(8, 0, 36)),
mySqlOptions => mySqlOptions.EnableRetryOnFailure()
)
);
builder.Services.AddIdentity<ApplicationUser, IdentityRole>(options =>
// Registrera rätt databas och identity beroende på om setup är klar
if (setup.IsConfigured)
{
options.SignIn.RequireConfirmedAccount = false;
})
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
var csBuilder = new MySqlConnector.MySqlConnectionStringBuilder
{
Server = setup.DbHost,
Port = (uint)setup.DbPort,
Database = setup.DbName,
UserID = setup.DbUser,
Password = setup.DbPassword,
AllowUserVariables = true // valfritt ta bort om du inte använder det
};
var connectionString = csBuilder.ConnectionString;
builder.Services.AddDbContext<ApplicationDbContext>(options =>
options.UseMySql(connectionString, ServerVersion.AutoDetect(connectionString)));
builder.Services.AddIdentity<ApplicationUser, IdentityRole>(options =>
{
options.SignIn.RequireConfirmedAccount = false;
})
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
}
else
{
builder.Services.AddDbContext<ApplicationDbContext>(options =>
options.UseInMemoryDatabase("TempSetup"));
builder.Services.AddIdentity<ApplicationUser, IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
}
// Identity inställningar
builder.Services.Configure<IdentityOptions>(options =>
{
options.Password.RequireDigit = false;
@@ -74,11 +129,10 @@ builder.Services.Configure<IdentityOptions>(options =>
options.Password.RequireUppercase = false;
});
builder.Services.AddControllersWithViews();
// Register your services
// Appens övriga tjänster
builder.Services.AddScoped<MenuService>();
builder.Services.AddSingleton<PushNotificationService>(sp =>
builder.Services.AddScoped<PushNotificationService>(sp =>
{
var config = sp.GetRequiredService<IConfiguration>();
return new PushNotificationService(
@@ -88,12 +142,20 @@ builder.Services.AddSingleton<PushNotificationService>(sp =>
);
});
builder.Services.AddScoped<PizzaNotificationService>();
builder.Services.Configure<VapidOptions>(builder.Configuration.GetSection("Vapid"));
builder.Services.ConfigureApplicationCookie(options =>
{
options.LoginPath = "/Identity/Account/Login"; // korrekt för ditt nuvarande upplägg
options.LoginPath = "/Identity/Account/Login";
options.ExpireTimeSpan = TimeSpan.FromDays(30);
options.SlidingExpiration = true;
options.Cookie.IsEssential = true;
});
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
builder.Services.Configure<RequestLocalizationOptions>(options =>
{
var supportedCultures = new[] { new CultureInfo("sv-SE") };
@@ -101,60 +163,70 @@ builder.Services.Configure<RequestLocalizationOptions>(options =>
options.SupportedCultures = supportedCultures;
options.SupportedUICultures = supportedCultures;
});
//builder.Configuration
// .SetBasePath(Directory.GetCurrentDirectory())
// .AddJsonFile($"appsettings.{builder.Environment.EnvironmentName}.json", optional: true, reloadOnChange: true)
// .AddEnvironmentVariables();
builder.Services.AddSingleton<PushNotificationService>(sp =>
{
var config = sp.GetRequiredService<IConfiguration>();
return new PushNotificationService(
config["VapidKeys:Subject"],
config["VapidKeys:PublicKey"],
config["VapidKeys:PrivateKey"]
);
});
builder.Services.AddSingleton<SetupService>();
var app = builder.Build();
// Configure the HTTP request pipeline.
app.UseStaticFiles();
// Middleware: om ej konfigurerad → redirect till /setup
app.Use(async (context, next) =>
{
var setupService = context.RequestServices.GetRequiredService<SetupService>();
var currentSetup = setupService.GetSetup();
var path = context.Request.Path;
var method = context.Request.Method;
if (!currentSetup.IsConfigured &&
!path.StartsWithSegments("/setup") &&
!(path == "/setup" && method == "POST") && // 👈 tillåt POST till /setup
!path.StartsWithSegments("/api/setup"))
{
context.Response.Redirect("/setup");
return;
}
await next();
});
// Configure the HTTP request pipeline
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Home/Error");
app.UseHsts();
}
app.UseStaticFiles();
app.UseRouting();
app.UseSession();
app.UseAuthentication();;
app.UseAuthentication();
app.UseAuthorization();
// Map controller endpoints (including AJAX JSON actions)
// Routing
app.MapControllers();
// Conventional MVC route
app.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
// Map Razor Pages
app.MapRazorPages();
using (var scope = app.Services.CreateScope())
// Init: migrera databas och skapa admin
if (setup.IsConfigured)
{
using var scope = app.Services.CreateScope();
var services = scope.ServiceProvider;
var context = services.GetRequiredService<ApplicationDbContext>();
// Vänta tills databasen är redo
int retries = 10;
while (retries > 0)
{
try
{
context.Database.OpenConnection(); // bara testa öppna anslutningen
context.Database.OpenConnection();
context.Database.CloseConnection();
break; // lyckades!
break;
}
catch
{
@@ -164,17 +236,20 @@ using (var scope = app.Services.CreateScope())
}
}
// Kör alla EF-migrationer automatiskt
context.Database.Migrate();
// Skapa standardroller och admin
await IdentityDataInitializer.SeedData(services);
var userManager = services.GetRequiredService<UserManager<ApplicationUser>>();
var anyUsers = await userManager.Users.AnyAsync();
// (valfritt) Lägg in exempelbudgetdata
// await TestDataSeeder.SeedBudget(context);
if (!anyUsers)
{
Console.WriteLine("🧩 Ingen användare hittades skapar admin...");
await IdentityDataInitializer.SeedData(services, setup);
}
}
if (app.Environment.IsDevelopment())
{
app.UseDeveloperExceptionPage(); // Detta ger stacktraces i browsern
}
app.Run();

View File

@@ -8,7 +8,7 @@
}
},
"profiles": {
"Aberwyn (Dev)": {
"Aberwyn": {
"commandName": "Project",
"launchBrowser": true,
"environmentVariables": {
@@ -16,14 +16,6 @@
},
"applicationUrl": "https://localhost:7290;http://localhost:5290"
},
"Aberwyn (Prod)": {
"commandName": "Project",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Production"
},
"applicationUrl": "https://localhost:7290;http://localhost:5290"
},
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,

View File

@@ -1,103 +1,236 @@
@model List<Aberwyn.Controllers.AdminUserViewModel>
<link rel="stylesheet" href="~/css/admin.css" />
<h2>Användarhantering</h2>
<h3>Skapa ny roll</h3>
<form method="post" asp-action="CreateRole">
<div class="form-group">
<input type="text" name="roleName" class="form-control" placeholder="Namn på ny roll" required />
</div>
<button type="submit" class="btn btn-success mt-2">Skapa roll</button>
</form>
<h3>Skapa ny användare</h3>
@if (TempData["Message"] != null)
{
<div class="alert alert-success">@TempData["Message"]</div>
}
<h2 class="mb-3">Adminpanel</h2>
<form method="post" asp-action="CreateUser">
<div class="form-group">
<label for="email">E-post:</label>
<input type="email" name="email" class="form-control" required />
</div>
<div class="form-group">
<label for="password">Lösenord:</label>
<input type="password" name="password" class="form-control" required />
</div>
<button type="submit" class="btn btn-success mt-2">Skapa användare</button>
</form>
<hr />
<h4>Alla roller</h4>
<ul>
@foreach (var role in ViewBag.AllRoles as List<string>)
{
<li>@role</li>
}
</ul>
<table class="table">
<thead>
<tr>
<th>E-post</th>
<th>Roller</th>
<th>Lägg till roll</th>
</tr>
</thead>
<tbody>
@foreach (var user in Model)
{
<tr>
<td>@user.Email</td>
<td>
@foreach (var role in user.Roles)
{
<span>@role</span>
<form method="post" asp-action="RemoveFromRole" style="display:inline;">
<input type="hidden" name="userId" value="@user.UserId" />
<input type="hidden" name="role" value="@role" />
<button type="submit" class="btn btn-sm btn-danger">Ta bort</button>
<div class="card mb-4">
<div class="card-header">Roller och användare</div>
<div class="card-body">
<div class="row">
<div>
<h4>Skapa ny roll</h4>
<form method="post" asp-action="CreateRole">
<div class="form-group">
<input type="text" name="roleName" class="form-control" placeholder="Namn på ny roll" required />
</div>
<button type="submit" class="btn btn-success mt-2">Skapa roll</button>
</form>
</div>
<div>
<h4>Skapa ny användare</h4>
@if (TempData["Message"] != null)
{
<div class="alert alert-success">@TempData["Message"]</div>
}
<form method="post" asp-action="CreateUser">
<div class="form-group">
<label for="email">E-post:</label>
<input type="email" name="email" class="form-control" required />
</div>
<div class="form-group">
<label for="password">Lösenord:</label>
<input type="password" name="password" class="form-control" required />
</div>
<button type="submit" class="btn btn-success mt-2">Skapa användare</button>
</form>
</div>
</div>
</div>
</div>
<div class="card mb-4">
<div class="card-header">Befintliga roller och användare</div>
<div class="card-body">
<h5>Alla roller</h5>
<ul>
@foreach (var role in ViewBag.AllRoles as List<string>)
{
<li>@role</li>
}
</td>
<td>
<form method="post" asp-action="AddToRole" class="form-inline">
<input type="hidden" name="userId" value="@user.UserId" />
<input type="text" name="role" class="form-control" placeholder="Rollnamn" />
<button type="submit" class="btn btn-sm btn-primary">Lägg till</button>
</form>
</td>
</tr>
}
</tbody>
</table>
</ul>
<h3>Testa Pushnotis</h3>
<form onsubmit="sendPush(event)">
<div class="form-group">
<label for="title">Titel:</label>
<input type="text" id="title" name="title" class="form-control" required />
<table class="table">
<thead>
<tr>
<th>E-post</th>
<th>Roller</th>
<th>Lägg till roll</th>
</tr>
</thead>
<tbody>
@foreach (var user in Model)
{
<tr>
<td>@user.Email</td>
<td>
@foreach (var role in user.Roles)
{
<span>@role</span>
<form method="post" asp-action="RemoveFromRole" style="display:inline;">
<input type="hidden" name="userId" value="@user.UserId" />
<input type="hidden" name="role" value="@role" />
<button type="submit" class="btn btn-sm btn-danger">Ta bort</button>
</form>
}
</td>
<td>
<form method="post" asp-action="AddToRole" class="form-inline">
<input type="hidden" name="userId" value="@user.UserId" />
<input type="text" name="role" class="form-control" placeholder="Rollnamn" />
<button type="submit" class="btn btn-sm btn-primary">Lägg till</button>
</form>
</td>
</tr>
}
</tbody>
</table>
</div>
<div class="form-group">
<label for="body">Meddelande:</label>
<input type="text" id="body" name="body" class="form-control" required />
</div>
<button type="submit" class="btn btn-warning mt-2">Skicka testnotis</button>
</form>
</div>
<div class="card mb-4">
<div class="card-header">Övriga funktioner</div>
<div class="card-body">
<button class="btn btn-secondary mb-3" onclick="generateThumbnails()">🖼 Generera thumbnails</button>
<h4>Testa Pushnotis</h4>
<form onsubmit="sendPush(event)">
<div class="form-group">
<label for="title">Titel:</label>
<input type="text" id="title" name="title" class="form-control" required />
</div>
<div class="form-group">
<label for="body">Meddelande:</label>
<input type="text" id="body" name="body" class="form-control" required />
</div>
<button type="submit" class="btn btn-warning mt-2">Skicka testnotis</button>
</form>
<hr />
<h4>Skicka Pizza-notis</h4>
<button class="btn btn-danger mt-2" onclick="sendPizzaPush()">🍕 Skicka pizzanotis</button>
<div id="pizzaPushResult" class="mt-2"></div>
</div>
</div>
<div class="card mb-5">
<div class="card-header">Importera från annan databas</div>
<div class="card-body">
<form id="customDbForm">
<div class="form-group">
<label>Server:</label>
<input type="text" name="dbHost" class="form-control" required />
</div>
<div class="form-group">
<label>Port:</label>
<input type="number" name="dbPort" class="form-control" value="3306" required />
</div>
<div class="form-group">
<label>Databas:</label>
<input type="text" name="dbName" class="form-control" required />
</div>
<div class="form-group">
<label>Användarnamn:</label>
<input type="text" name="dbUser" class="form-control" required />
</div>
<div class="form-group">
<label>Lösenord:</label>
<input type="password" name="dbPassword" class="form-control" required />
</div>
</form>
<button class="btn btn-info mt-3" onclick="testDbConnection()">🔌 Testa anslutning</button>
<div id="testResult" class="mt-2"></div>
<button class="btn btn-primary mt-3" onclick="submitTo('ImportMealsFromCustom')">🍽 Importera måltider</button>
<button class="btn btn-primary mt-3" onclick="submitTo('ImportMenusFromCustom')">🗓 Importera menyer</button>
<button class="btn btn-primary mt-3" onclick="submitTo('ImportBudgetFromCustom')">💰 Importera budget</button>
</div>
</div>
<hr />
<h3>Importera måltider från produktion</h3>
<form method="post" asp-action="ImportMealsFromProd">
<button type="submit" class="btn btn-danger">Importera alla måltider</button>
</form>
<form method="post" asp-action="ImportMenusFromProd">
<button type="submit" class="btn btn-danger mt-2">Importera veckomenyer</button>
</form>
<form method="post" asp-action="ImportBudgetFromProd">
<button type="submit" class="btn btn-danger mt-2">Importera budgetdata</button>
</form>
<script>
async function sendPizzaPush() {
const resultDiv = document.getElementById("pizzaPushResult");
resultDiv.innerText = "⏳ Skickar notis...";
try {
const response = await fetch("/api/push/notify-pizza", {
method: "POST"
});
const text = await response.text();
if (response.ok) {
resultDiv.className = "text-success mt-2";
resultDiv.innerText = "✅ " + text;
} else {
resultDiv.className = "text-danger mt-2";
resultDiv.innerText = "❌ Fel: " + text;
}
} catch (err) {
resultDiv.className = "text-danger mt-2";
resultDiv.innerText = "❌ Ett tekniskt fel uppstod.";
console.error(err);
}
}
function generateThumbnails() {
if (!confirm("Generera thumbnails för alla måltider som saknar?")) return;
fetch("/admin/GenerateThumbnails", {
method: "POST"
})
.then(res => res.text())
.then(msg => {
alert("✅ " + msg);
})
.catch(err => {
console.error("Fel vid generering av thumbnails:", err);
alert("❌ Ett fel uppstod vid generering av thumbnails.");
});
}
function collectFormData() {
const form = document.getElementById("customDbForm");
return new FormData(form); // 🔁 Byt ut till ren FormData istället för URLSearchParams
}
function submitTo(action) {
const data = collectFormData();
fetch(`/admin/${action}`, {
method: "POST",
body: data
})
.then(async res => {
const text = await res.text();
if (res.ok) {
alert(`✅ ${action} lyckades`);
} else {
alert(`❌ Fel vid ${action}:\n${text}`);
}
});
}
function testDbConnection() {
const data = collectFormData();
fetch(`/admin/TestDbConnection`, {
method: "POST",
body: data
})
.then(res => res.json())
.then(data => {
const div = document.getElementById("testResult");
div.innerText = data.message;
div.className = data.success ? "text-success mt-2" : "text-danger mt-2";
})
.catch(err => {
document.getElementById("testResult").innerText = "❌ Ett tekniskt fel uppstod.";
console.error(err);
});
}
async function sendPush(event) {
event.preventDefault(); // 🚫 stoppa formuläret från att göra vanlig POST

View File

@@ -10,6 +10,30 @@
<script src="~/js/todo.js" asp-append-version="true"></script>
<style>
.todo-column input[type="text"],
.todo-column textarea,
.todo-column select {
width: 100%;
margin-top: 6px;
margin-bottom: 6px;
padding: 6px;
border: 1px solid #ccc;
border-radius: 4px;
box-sizing: border-box;
font-family: inherit;
font-size: 0.9em;
}
.todo-column textarea {
resize: vertical;
min-height: 60px;
}
.todo-column button {
margin-top: 10px;
width: 100%;
}
.todo-board {
padding: 20px;
font-family: sans-serif;
@@ -80,8 +104,18 @@
<div class="todo-task {{ PriorityClass(task.Priority) }}"
ng-repeat="task in FilteredTasks(col.Id) track by task.Id"
draggable-task
task="task">
{{ task.Title }}
task="task"
ng-if="!task.IsArchived">
<strong>{{ task.Title }}</strong>
<p ng-if="task.Description">{{ task.Description }}</p>
<div>
<small>🏷️ {{ task.Tags }}</small><br />
<small>📅 {{ task.CreatedAt | date:'yyyy-MM-dd HH:mm' }}</small>
</div>
<div>
<small>Prioritet:
<select ng-model="task.Priority" ng-change="UpdatePriority(task)">
@@ -93,8 +127,11 @@
</div>
</div>
<div ng-if="col.Id === 'ideas'">
<input type="text" ng-model="NewTask.Title" placeholder="Ny idé..." />
<textarea ng-model="NewTask.Description" placeholder="Beskrivning"></textarea>
<input type="text" ng-model="NewTask.Tags" placeholder="Taggar (komma-separerat)" />
<select ng-model="NewTask.Priority">
<option value="1">Låg</option>
<option value="2">Medel</option>
@@ -102,6 +139,7 @@
</select>
<button type="button" ng-click="AddTask()">Lägg till</button>
</div>
</div>
</div>

View File

@@ -34,13 +34,57 @@
</div>
</div>
<div class="summary-cards" ng-if="budget && budget.categories.length > 0" style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 16px; margin: 24px 0;">
<div class="summary-card income">Totalt inkomst<br><strong>{{ getTotalIncome() | number:0 }}</strong></div>
<div class="summary-card expense">Total utgift<br><strong>({{ getTotalExpense() | number:0 }})</strong></div>
<div class="summary-card savings">Sparande<br><strong>{{ getTotalSaving() | number:0 }}</strong></div>
<div class="summary-card leftover">Pengar kvar<br><strong>{{ getLeftover() | number:0 }}</strong></div>
<div class="budget-overview-row" ng-if="budget && budget.categories.length > 0">
<!-- Vänster: Summering -->
<div class="budget-summary-box compact">
<h3>Sammanställning</h3>
<ul class="summary-list">
<li><span>💰 Inkomst:</span> {{ getTotalIncome() | number:0 }} kr</li>
<li><span>💸 Utgift:</span> {{ getTotalExpense() | number:0 }} kr</li>
<li><span>🏦 Sparande:</span> {{ getTotalSaving() | number:0 }} kr</li>
<p>
📈 Kvar:
<span class="highlight leftover" ng-class="{'plus': getLeftover() >= 0, 'minus': getLeftover() < 0}">
{{ getLeftover() | number:0 }} kr
</span>
</p>
</ul>
<hr>
<ul class="category-summary-list">
<li ng-repeat="cat in budget.categories">
<span style="color: {{ cat.color }}">{{ cat.name }}</span>
<span>{{ getCategorySum(cat) | number:0 }} kr</span>
</li>
</ul>
</div>
<!-- Höger: Diagramväxling -->
<div class="budget-chart-box compact">
<div class="chart-row">
<div class="chart-legend">
<h4>Utgifter</h4>
<p style="font-size: 12px; margin: 4px 0 10px;">
Totalt: <strong>{{ getTotalExpense() | number:0 }} kr</strong>
</p>
<p ng-if="topExpenseCategory" style="font-size: 12px; margin-bottom: 12px;">
Största kategori: <br>
<strong>{{ topExpenseCategory.name }}</strong> ({{ topExpenseCategory.percent | number:1 }}%)
</p>
</div>
<div class="chart-area">
<canvas id="expenseChart"></canvas>
</div>
</div>
</div>
</div>
<div class="budget-grid" ng-if="budget && budget.categories">
<div class="budget-card"
ng-repeat="cat in budget.categories"
@@ -185,6 +229,7 @@
</div>
<link rel="stylesheet" href="~/css/budget.css" />
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.8.2/angular.min.js"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css" />
<script src="~/js/budget.js"></script>

View File

@@ -143,8 +143,9 @@
</aside>
<datalist id="meals-list">
@foreach (var m in ViewBag.AvailableMeals as List<Aberwyn.Models.Meal>) {
<option value="@m.Name" />
@foreach (var meal in (List<Meal>)ViewBag.AvailableMeals)
{
<option value="@meal.Id">@meal.Name</option>
}
</datalist>
</form>

View File

@@ -56,16 +56,37 @@
ng-click="openMeal(getMealIdByDay(day))">
<div class="card-content">
<div class="day">{{day}}</div>
<div class="meal" ng-if="menu[day].breakfastMealName">Frukost: {{menu[day].breakfastMealName}}</div>
<div class="meal" ng-if="menu[day].lunchMealName">Lunch: {{menu[day].lunchMealName}}</div>
<div class="meal" ng-if="menu[day].dinnerMealName">Middag: {{menu[day].dinnerMealName}}</div>
<div class="meal" ng-if="!menu[day]">Inte bestämd</div>
<div class="meal" ng-if="menu[day].lunchMealName">Lunch: {{menu[day].lunchMealName}}</div>
<div class="meal" ng-if="menu[day].breakfastMealName">Frukost: {{menu[day].breakfastMealName}}</div>
<div class="meal" ng-if="menu[day] && !menu[day].breakfastMealName && !menu[day].lunchMealName && !menu[day].dinnerMealName">Inte bestämd</div>
</div>
</div>
</div>
</div>
</div>
<div class="school-meals card-view">
<h2>Skolmat</h2>
<div ng-repeat="school in schoolMealsBySchool">
<div class="school-meal-title" ng-click="toggleSchoolExpanded(school)">
<span class="chevron" ng-class="{ rotated: school.expanded }">&#x25BC;</span>
{{ school.name }}
</div>
<div class="card-container" ng-show="school.expanded">
<div class="meal-card" ng-repeat="day in school.days">
<div class="card-content school-meal-card-content">
<div class="day">{{ day.weekday }}</div>
<div class="meal" ng-repeat="meal in day.courses">{{ meal }}</div>
</div>
</div>
</div>
</div>
</div>
</div>
</body>
</html>

View File

@@ -0,0 +1,14 @@
<!DOCTYPE html>
<html lang="sv">
<head>
<meta charset="UTF-8">
<title>Offline</title>
<style>
body { font-family: sans-serif; text-align: center; padding: 2rem; background: #1F2C3C; color: white; }
</style>
</head>
<body>
<h1>🌐 Du är offline</h1>
<p>LEWEL är inte tillgänglig just nu. Kontrollera din internetanslutning och försök igen.</p>
</body>
</html>

View File

@@ -0,0 +1,12 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
namespace Aberwyn.Views.Home
{
public class OfflineModel : PageModel
{
public void OnGet()
{
}
}
}

View File

@@ -0,0 +1,48 @@
<!DOCTYPE html>
<html lang="sv" ng-app="mealGalleryApp">
<head>
<meta charset="utf-8">
<title>Måltider</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.8.2/angular.min.js"></script>
<link rel="stylesheet" href="/css/meal-gallery.css">
</head>
<body ng-controller="MealGalleryController">
<div class="meal-gallery-container" ng-controller="MealGalleryController">
<div class="meal-gallery-header">
<h1>Recept</h1>
<div class="search-container">
<input type="text" ng-model="search" placeholder="Sök recept...">
<i class="fa-solid fa-magnifying-glass"></i>
</div>
</div>
<div class="meal-gallery-grid">
<div class="meal-card" ng-repeat="meal in meals | filter:search">
<img ng-src="{{ meal.ThumbnailData ? 'data:image/webp;base64,' + meal.ThumbnailData : '/images/fallback.jpg' }}"
alt="{{ meal.Name }}">
<div class="meal-card-content">
<h3>{{ meal.Name }}</h3>
<p ng-if="meal.Description">{{ meal.Description }}</p>
<a class="btn-readmore" ng-href="/meal/view/{{ meal.Id }}">Läs mer</a>
</div>
</div>
</div>
</div>
</body>
<script>
angular.module("mealGalleryApp", []).controller("MealGalleryController", function ($scope, $http) {
$scope.meals = [];
$scope.search = "";
$http.get("/api/mealMenuApi/getMeals").then(res => {
$scope.meals = res.data;
});
});
</script>
</body>
</html>

View File

@@ -64,6 +64,10 @@
<label for="CarbType">Kolhydrat</label>
<input type="text" name="CarbType" value="@Model.CarbType" class="form-control" />
</div>
<div class="form-group">
<label for="Category">Kategori</label>
<input type="text" name="Category" value="@Model.Category" class="form-control" />
</div>
<div class="form-group">
<label for="RecipeUrl">Receptlänk</label>
@@ -79,7 +83,7 @@
<div class="form-group">
<label>Ingredienser</label>
<div id="ingredients-list">
@for (int i = 0; i < Model.Ingredients.Count; i++)
@for (int i = 0; i < (Model.Ingredients?.Count ?? 0); 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" />
@@ -215,7 +219,7 @@
closeImageModal();
}
});
$(document).ready(function () {
$(document).ready(function () {
if ($('#Instructions').length > 0) {
$('#Instructions').trumbowyg({
autogrow: true,
@@ -223,35 +227,6 @@
});
}
});
if ('serviceWorker' in navigator && 'PushManager' in window) {
navigator.serviceWorker.register('/service-worker.js')
.then(function (registration) {
console.log('Service Worker registered', registration);
return registration.pushManager.getSubscription()
.then(async function (subscription) {
if (subscription) {
console.log('Already subscribed to push notifications.');
return subscription;
}
const response = await fetch('/api/push/vapid-public-key');
const vapidPublicKey = await response.text();
const convertedVapidKey = urlBase64ToUint8Array(vapidPublicKey);
return registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: convertedVapidKey
});
});
}).then(function (subscription) {
return fetch('/api/push/subscribe', {
method: 'POST',
body: JSON.stringify(subscription),
headers: {
'Content-Type': 'application/json'
}
});
});
}
</script>

View File

@@ -0,0 +1,395 @@
@model Aberwyn.Models.SetupSettings
@{
ViewData["Title"] = "Installera Aberwyn";
}
<h1>Installera Aberwyn</h1>
@if (ViewBag.Error != null)
{
<div class="alert alert-danger">@ViewBag.Error</div>
}
<style>
.setup-step {
margin-bottom: 2rem;
padding: 1.5rem;
background-color: #f8f9fa;
border-radius: 8px;
box-shadow: 0 2px 6px rgba(0,0,0,0.05);
}
.form-group {
margin-bottom: 1rem;
display: flex;
flex-direction: column;
}
label {
font-weight: bold;
margin-bottom: 0.2rem;
}
input.form-control {
padding: 0.5rem;
font-size: 1rem;
border-radius: 6px;
}
.is-invalid {
border-color: #dc3545;
}
.invalid-feedback {
color: #dc3545;
font-size: 0.875rem;
display: none;
}
.is-invalid + .invalid-feedback {
display: block;
}
button {
padding: 10px 20px;
font-size: 1rem;
border-radius: 6px;
border: none;
cursor: pointer;
transition: background 0.2s ease;
margin-top: 0.5rem;
margin-right: 0.5rem;
}
.btn-primary { background-color: #6a0dad; color: white; }
.btn-primary:hover { background-color: #5a0cab; }
.btn-secondary { background-color: #adb5bd; color: black; }
.btn-secondary:hover { background-color: #9ca3af; }
.btn-success { background-color: #198754; color: white; }
.btn-success:hover { background-color: #157347; }
.btn-danger { background-color: #dc3545; color: white; }
.btn-danger:hover { background-color: #bb2d3b; }
.btn-outline-info {
border: 1px solid #0dcaf0;
color: #0dcaf0;
background: none;
}
.btn-outline-info:hover {
background-color: #0dcaf0;
color: white;
}
#setup-summary ul {
list-style: none;
padding-left: 0;
}
#setup-summary li {
margin-bottom: 0.4rem;
}
.spinner {
display: inline-block;
width: 1.2rem;
height: 1.2rem;
border: 3px solid rgba(0,0,0,0.2);
border-top-color: #6a0dad;
border-radius: 50%;
animation: spin 1s linear infinite;
margin-left: 0.5rem;
}
@@keyframes spin {
to { transform: rotate(360deg); }
}
</style>
<form method="post" id="setup-form">
<div id="step-1" class="setup-step">
<h2>Steg 1: Databasinställningar</h2>
<div class="form-group">
<label for="DbHost">Server</label>
<input asp-for="DbHost" name="DbHost" class="form-control" id="DbHost" required />
<div class="invalid-feedback">Fältet är obligatoriskt</div>
</div>
<div class="form-group">
<label for="DbPort">Port</label>
<input asp-for="DbPort" name="DbPort" class="form-control" id="DbPort" value="3306" required />
<div class="invalid-feedback">Fältet är obligatoriskt</div>
</div>
<div class="form-group">
<label for="DbName">Databasnamn</label>
<input asp-for="DbName" name="DbName" class="form-control" id="DbName" required />
<div class="invalid-feedback">Fältet är obligatoriskt</div>
</div>
<div class="form-group">
<label for="DbUser">Användarnamn</label>
<input asp-for="DbUser" name="DbUser" class="form-control" id="DbUser" required />
<div class="invalid-feedback">Fältet är obligatoriskt</div>
</div>
<div class="form-group">
<label for="DbPassword">Lösenord</label>
<input asp-for="DbPassword" name="DbPassword" type="password" class="form-control" id="DbPassword" required />
<div class="invalid-feedback">Fältet är obligatoriskt</div>
</div>
<button type="button" class="btn btn-outline-info" onclick="testDbConnection()">Testa anslutning</button>
<div id="db-test-result" class="mt-2"></div>
<button type="button" class="btn btn-primary" onclick="validateStep(1) && goToStep(2)">Nästa</button>
</div>
<div id="step-2" class="setup-step" style="display: none;">
<h2>Steg 2: Administratör</h2>
<div class="form-group">
<label for="AdminUsername">Admin Användarnamn</label>
<input asp-for="AdminUsername" name="AdminUsername" class="form-control" id="AdminUsername" required />
<div class="invalid-feedback">Fältet är obligatoriskt</div>
</div>
<div class="form-group">
<label for="AdminEmail">E-postadress</label>
<input asp-for="AdminEmail" name="AdminEmail" type="email" class="form-control" id="AdminEmail" required />
<div class="invalid-feedback">Fältet är obligatoriskt</div>
</div>
<div class="form-group">
<label for="AdminPassword">Lösenord</label>
<input asp-for="AdminPassword" name="AdminPassword" type="password" class="form-control"
id="AdminPassword"
pattern="(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[^a-zA-Z\d]).{6,}"
title="Minst 6 tecken, inklusive versal, gemen, siffra och specialtecken"
required />
<div class="invalid-feedback">Ange ett giltigt lösenord enligt kraven</div>
</div>
<div class="form-group">
<label for="ConfirmPassword">Bekräfta lösenord</label>
<input type="password" id="ConfirmPassword" class="form-control" required />
<span id="password-match-status" style="margin-top: 4px; font-size: 1.2rem;"></span>
<div class="invalid-feedback" id="confirmPasswordError">Lösenorden matchar inte</div>
</div>
<ul id="password-criteria" style="list-style: none; padding-left: 0; font-size: 0.9rem; margin-top: 0.5rem;">
<li id="length-criteria">❌ Minst 6 tecken</li>
<li id="uppercase-criteria">❌ Minst en stor bokstav (AZ)</li>
<li id="lowercase-criteria">❌ Minst en liten bokstav (az)</li>
<li id="digit-criteria">❌ Minst en siffra (09)</li>
<li id="special-criteria">❌ Minst ett specialtecken (!#&...)</li>
</ul>
<button type="button" class="btn btn-secondary" onclick="goToStep(1)">Tillbaka</button>
<button type="button" class="btn btn-primary" onclick="validateAdminStep() && goToStep(3)">Nästa</button>
</div>
<div id="step-3" class="setup-step" style="display: none;">
<h2>Steg 3: Konfiguration pågår...</h2>
<div id="setup-summary">
<ul>
<li><strong>Server:</strong> <span id="summary-DbHost"></span></li>
<li><strong>Port:</strong> <span id="summary-DbPort"></span></li>
<li><strong>Databas:</strong> <span id="summary-DbName"></span></li>
<li><strong>Användare:</strong> <span id="summary-DbUser"></span></li>
<li><strong>Adminnamn:</strong> <span id="summary-AdminUsername"></span></li>
<li><strong>Admin-e-post:</strong> <span id="summary-AdminEmail"></span></li>
</ul>
</div>
<div id="setup-progress" class="mt-3">
<p>
Skapar databastabeller och konfigurerar administratörskonto...
<span class="spinner"></span>
</p>
</div>
<button type="button" class="btn btn-secondary" onclick="goToStep(2)">Tillbaka</button>
<button type="button" class="btn btn-success" onclick="submitSetup()">Slutför installation</button>
</div>
</form>
@if (User.IsInRole("Admin"))
{
<form method="post" asp-action="Reset" class="mt-3" onsubmit="return confirm('Är du säker på att du vill återställa konfigurationen?')">
<button type="submit" class="btn btn-danger">Återställ inställningar</button>
</form>
}
@section Scripts {
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-validate/1.19.5/jquery.validate.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-validate-unobtrusive/3.2.12/jquery.validate.unobtrusive.min.js"></script>
<script>
function goToStep(step) {
document.querySelectorAll('.setup-step').forEach(s => s.style.display = 'none');
document.getElementById('step-' + step).style.display = 'block';
if (step === 3) {
document.getElementById('summary-DbHost').textContent = document.getElementById('DbHost').value;
document.getElementById('summary-DbPort').textContent = document.getElementById('DbPort').value;
document.getElementById('summary-DbName').textContent = document.getElementById('DbName').value;
document.getElementById('summary-DbUser').textContent = document.getElementById('DbUser').value;
document.getElementById('summary-AdminUsername').textContent = document.getElementById('AdminUsername').value;
document.getElementById('summary-AdminEmail').textContent = document.getElementById('AdminEmail').value;
document.getElementById('setup-progress').innerHTML = `<p>Redo att påbörja installationen.</p>`;
}
}
document.getElementById('AdminPassword').addEventListener('input', function () {
const val = this.value;
const hasLength = val.length >= 6;
const hasUpper = /[A-Z]/.test(val);
const hasLower = /[a-z]/.test(val);
const hasDigit = /\d/.test(val);
const hasSpecial = /[!@@#$%^&*(),.?":{}|<>_\-\\[\]\/+=~`]/.test(val); // lägg till specialtecken
updateCriteria('length-criteria', hasLength);
updateCriteria('uppercase-criteria', hasUpper);
updateCriteria('lowercase-criteria', hasLower);
updateCriteria('digit-criteria', hasDigit);
updateCriteria('special-criteria', hasSpecial);
});
function updateCriteria(id, isValid) {
const el = document.getElementById(id);
if (isValid) {
el.textContent = '✔ ' + el.textContent.slice(2);
el.style.color = 'green';
} else {
el.textContent = '❌ ' + el.textContent.slice(2);
el.style.color = 'red';
}
}
function validateStep(step) {
let isValid = true;
document.querySelectorAll('#step-' + step + ' [required]').forEach(field => {
if (!field.value.trim()) {
field.classList.add('is-invalid');
isValid = false;
} else {
field.classList.remove('is-invalid');
}
});
return isValid;
}
function testDbConnection() {
const host = document.getElementById('DbHost').value;
const port = document.getElementById('DbPort').value;
const db = document.getElementById('DbName').value;
const user = document.getElementById('DbUser').value;
const pass = document.getElementById('DbPassword').value;
const output = document.getElementById('db-test-result');
output.innerHTML = '<span class="spinner"></span> Testar...';
fetch('/api/setup/testdb', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ host, port, db, user, pass })
})
.then(async res => {
const text = await res.text();
console.log("Raw:", text);
try {
const result = JSON.parse(text);
if (result.success) {
output.innerHTML = '✅ <span class="text-success">Anslutning lyckades!</span>';
} else {
output.innerHTML = '❌ <span class="text-danger">' + result.message + '</span>';
}
} catch (e) {
output.innerHTML = '❌ <span class="text-danger">Felaktigt svar från servern: ' + text + '</span>';
}
})
.catch(err => {
output.innerHTML = '❌ <span class="text-danger">Nätverksfel: ' + err.message + '</span>';
});
}
function validateAdminStep() {
let valid = validateStep(2);
const pass = document.getElementById('AdminPassword');
const confirm = document.getElementById('ConfirmPassword');
const confirmError = document.getElementById('confirmPasswordError');
if (pass.value !== confirm.value) {
confirm.classList.add('is-invalid');
confirmError.style.display = 'block';
valid = false;
} else {
confirm.classList.remove('is-invalid');
confirmError.style.display = 'none';
}
return valid;
}
document.getElementById('ConfirmPassword').addEventListener('input', function () {
const pass = document.getElementById('AdminPassword').value;
const confirm = this.value;
const confirmField = document.getElementById('ConfirmPassword');
const matchStatus = document.getElementById('password-match-status');
if (confirm.length === 0) {
matchStatus.innerHTML = '';
confirmField.classList.remove('is-invalid');
return;
}
if (pass === confirm) {
matchStatus.innerHTML = '✔';
matchStatus.style.color = 'green';
confirmField.classList.remove('is-invalid');
} else {
matchStatus.innerHTML = '❌';
matchStatus.style.color = 'red';
confirmField.classList.add('is-invalid');
}
});
async function submitSetup() {
goToStep(3);
const form = document.getElementById('setup-form');
const formData = new FormData(form);
const data = {};
formData.forEach((value, key) => { data[key] = value; });
const resultEl = document.getElementById('setup-progress');
resultEl.innerHTML = `<p>
Skapar databastabeller och konfigurerar administratörskonto...
<span class="spinner"></span>
</p>`;
try {
const response = await fetch('/setup', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
const text = await response.text();
let result;
try {
result = JSON.parse(text);
} catch (err) {
throw new Error("Kunde inte tolka JSON-svar: " + text);
}
if (!response.ok) {
resultEl.innerHTML = `
<div class="alert alert-danger mt-3">
❌ Fel vid installation: ${result.error || 'Okänt fel'}
</div>`;
return;
}
resultEl.innerHTML = `
<div class="alert alert-success mt-3">
✅ ${result.message || 'Installation slutförd!'}
<br />
<button class="btn btn-success mt-3" onclick="window.location.href='/'">Gå vidare</button>
</div>`;
} catch (err) {
console.error("Något gick fel:", err);
resultEl.innerHTML = `<div class="alert alert-danger mt-3">
❌ Fel vid installation: ${err.message}
</div>`;
}
}
</script>
}

View File

@@ -0,0 +1,17 @@
@{
ViewData["Title"] = "Klart!";
}
<h1>✅ Aberwyn är nu installerad</h1>
<p>Du kan nu starta om applikationen för att börja använda den.</p>
<a href="/" class="btn btn-primary">Till startsidan</a>
<hr />
<h3>🔄 Återställ installation</h3>
<p>Om du vill nollställa installationen och börja om från början, klicka här:</p>
<form method="post" asp-action="Reset">
<button type="submit" class="btn btn-danger">Återställ konfiguration</button>
</form>

View File

@@ -21,35 +21,48 @@
<span class="initial L2">L</span><span class="name">udwig</span>
</h1>
</div>
<partial name="_LoginPartial" />
@if (ViewBag.IsSetupMode as bool? != true)
{
<partial name="_LoginPartial" />
}
</header>
<div class="page-content">
<aside class="sidebar">
<ul class="nav-list">
<li><a asp-controller="Home" asp-action="Index"><i class="fas fa-home"></i> Home</a></li>
@if (User.IsInRole("Admin") || User.IsInRole("Budget"))
{
<li><a asp-controller="Budget" asp-action="Index"><i class="fas fa-wallet"></i> Budget</a></li>
}
<li><a asp-controller="Home" asp-action="Menu"><i class="fas fa-utensils"></i> Veckomeny</a></li>
@if (ViewBag.RestaurantIsOpen as bool? == true)
{
<li><a asp-controller="FoodMenu" asp-action="PizzaOrder"><i class="fas fa-pizza-slice"></i> Beställ pizza</a></li>
}
@if (User.IsInRole("Admin") || User.IsInRole("Chef"))
{
<li><a asp-controller="FoodMenu" asp-action="Veckomeny"><i class="fas fa-calendar-week"></i> Administrera Veckomeny</a></li>
<li><a asp-controller="FoodMenu" asp-action="MealAdmin"><i class="fas fa-list"></i> Måltider</a></li>
<li><a asp-controller="FoodMenu" asp-action="PizzaAdmin"><i class="fas fa-list"></i> Pizza Admin</a></li>
@if (ViewBag.IsSetupMode as bool? != true)
{
<ul class="nav-list">
<li><a asp-controller="Home" asp-action="Index"><i class="fas fa-home"></i> Home</a></li>
@if (User.IsInRole("Budget"))
{
<li><a asp-controller="Budget" asp-action="Index"><i class="fas fa-wallet"></i> Budget</a></li>
}
<li><a asp-controller="Home" asp-action="Menu"><i class="fas fa-utensils"></i> Veckomeny</a></li>
@if (ViewBag.RestaurantIsOpen as bool? == true)
{
<li><a asp-controller="FoodMenu" asp-action="PizzaOrder"><i class="fas fa-pizza-slice"></i> Beställ pizza</a></li>
}
@if (User.IsInRole("Chef"))
{
<li><a asp-controller="FoodMenu" asp-action="Veckomeny"><i class="fas fa-calendar-week"></i> Administrera Veckomeny</a></li>
<li><a asp-controller="FoodMenu" asp-action="MealAdmin"><i class="fas fa-list"></i> Måltider</a></li>
<li><a asp-controller="FoodMenu" asp-action="PizzaAdmin"><i class="fas fa-list"></i> Pizza Admin</a></li>
}
}
@if (User.IsInRole("Admin"))
{
<li><a asp-controller="Admin" asp-action="Index"><i class="fas fa-cog"></i> Adminpanel</a></li>
}
</ul>
@if (User.IsInRole("Admin"))
{
<li><a asp-controller="Admin" asp-action="Index"><i class="fas fa-cog"></i> Adminpanel</a></li>
<li><a asp-controller="Admin" asp-action="Todo"><i class="fas fa-cog"></i> Todo</a></li>
}
</ul>
} else
{
<ul class="nav-list">
<li><a asp-controller="Setup" asp-action="Index"> Setup</a></li>
</ul>
}
</aside>
<main class="main-panel">

View File

@@ -13,7 +13,10 @@
<i class="fas fa-user-circle"></i>
</button>
<div class="auth-dropdown">
<span style="padding: 8px 12px; display: block;">@User.Identity.Name</span>
<a href="/User/Profile">
<i class="fas fa-user-cog"></i> Min profil
</a>
<form method="post" asp-area="Identity" asp-page="/Account/Logout" style="margin: 0;">
<button type="submit" class="dropdown-logout-button" style="padding: 8px 12px; width: 100%; text-align: left; background: none; border: none; cursor: pointer;">Logga ut</button>
</form>
@@ -31,7 +34,7 @@ else
<div class="auth-dropdown">
<form method="post" asp-area="Identity" asp-page="/Account/Login" style="padding: 12px; display: flex; flex-direction: column; gap: 8px;">
<input type="hidden" name="ReturnUrl" value="/" />
<input name="Input.Email" type="email" placeholder="E-post" required style="padding: 6px; font-size: 14px;" />
<input name="Input.Username" type="text" placeholder="Användarnamn" required style="padding: 6px; font-size: 14px;" />
<input name="Input.Password" type="password" placeholder="Lösenord" required style="padding: 6px; font-size: 14px;" />
<button type="submit" style="background-color: #3A4E62; color: white; border: none; padding: 6px 10px; border-radius: 4px; font-size: 14px;">
Logga in

View File

@@ -0,0 +1,32 @@
@model Aberwyn.Models.UserProfileViewModel
@{
ViewData["Title"] = "Userprofile";
}
<h2>Min profil</h2>
<form method="post" asp-action="SaveProfile">
<div>
<label asp-for="Name"></label>
<input asp-for="Name" />
</div>
<div>
<label asp-for="Email"></label>
<input asp-for="Email" readonly />
</div>
<h3>Pushnotiser</h3>
<div>
<input asp-for="NotifyPizza" type="checkbox" />
<label asp-for="NotifyPizza">Ny pizzabeställning</label>
</div>
<div>
<input asp-for="NotifyMenu" type="checkbox" />
<label asp-for="NotifyMenu">Meny ändrad</label>
</div>
<div>
<input asp-for="NotifyBudget" type="checkbox" />
<label asp-for="NotifyBudget">Budget uppdaterad</label>
</div>
<button type="submit">Spara</button>
</form>

View File

@@ -6,7 +6,7 @@
}
},
"ConnectionStrings": {
"DefaultConnection": "Server=mysql;Database=aberwyn_dev;User=aberwyn;Password=3edc4RFV;",
"DefaultConnection": "Server=192.168.1.108;Database=Nevyn;Uid=root;Pwd=3edc4RFV;",
"ProdConnection": "Server=192.168.1.108;Database=Nevyn;Uid=root;Pwd=3edc4RFV;"
}

View File

@@ -1,5 +1,5 @@
{
"ConnectionStrings": {
"DefaultConnection": "Server=mysql;Database=aberwyn_prod;User=aberwyn;Password=3edc4RFV;"
"DefaultConnection": "Server=192.168.1.108;Database=Nevyn;Uid=root;Pwd=3edc4RFV;"
}
}

View File

@@ -7,11 +7,12 @@
},
"AllowedHosts": "*",
"ConnectionStrings": {
"DefaultConnection": "Server=mysql;Database=aberwyn_dev;User=aberwyn;Password=3edc4RFV;"
"DefaultConnection": "Server=192.168.1.108;Database=Nevyn;Uid=root;Pwd=3edc4RFV;"
},
"VapidKeys": {
"Subject": "mailto:e@zcz.se",
"PublicKey": "BBLmMdU3X3e79SqzAy4vIAJI0jmzRME17F9UKbO8XT1dfnO-mWIPKIrFDbIZD4_3ic7uoijK61vaGdfFUk3HUfU",
"PrivateKey": "oranoCmCo8HXdc03juNgbeSlKE39N3DYus_eMunLsnc"
}
},
}

View File

@@ -0,0 +1,116 @@
/* ================================
ADMIN JUSTERAD LEWEL STIL
================================ */
h2, h3, h4,h5 {
margin-top: 2px;
margin-bottom: 12px;
color: #394B5A;
font-weight: 600;
}
form {
background-color: #ffffff;
padding: 2px 8px;
border-radius: 6px;
margin-bottom: 2px;
box-shadow: 0 1px 4px rgba(0,0,0,0.1);
}
.form-group {
margin-bottom: 2px;
}
.form-control {
width: 100%;
max-width: 360px;
padding: 6px 10px;
border-radius: 4px;
border: 1px solid #ccc;
font-size: 14px;
}
button, .btn {
font-size: 14px;
font-weight: 500;
border-radius: 4px;
padding: 6px 12px;
margin-top: 6px;
}
.btn-sm {
padding: 4px 8px;
font-size: 13px;
}
.alert-success {
background-color: #e6ffed;
color: #256029;
padding: 8px 12px;
border-radius: 4px;
border: 1px solid #b4e2c5;
margin-bottom: 16px;
max-width: 360px;
}
table.table {
width: 100%;
border-collapse: collapse;
background-color: #ffffff;
margin-top: 8px;
border-radius: 6px;
overflow: hidden;
font-size: 14px;
box-shadow: 0 1px 4px rgba(0,0,0,0.1);
}
table.table th,
table.table td {
padding: 8px 10px;
text-align: left;
border-bottom: 1px solid #eee;
}
table.table th {
background-color: #f0f4f8;
font-weight: 600;
}
.card {
background-color: #ffffff;
border-radius: 6px;
box-shadow: 0 1px 4px rgba(0,0,0,0.1);
overflow: hidden;
margin-bottom: 16px;
}
.card-header {
background-color: #394B5A;
color: #ffffff;
font-weight: 600;
padding: 10px 14px;
font-size: 15px;
}
.card-body {
padding: 5px;
}
#testResult {
font-weight: bold;
font-size: 14px;
margin-top: 6px;
}
ul {
margin-top: 0;
margin-bottom: 16px;
padding-left: 20px;
color: #394B5A;
}
@media (max-width: 768px) {
.form-control, .btn {
width: 100%;
}
}

View File

@@ -88,40 +88,7 @@ body {
background-color: #cbd5e1;
}
.summary-cards {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 8px;
margin: 16px 0 12px;
}
.summary-card {
background-color: var(--bg-card);
padding: 8px 10px;
border-radius: 10px;
text-align: center;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
font-weight: 600;
font-size: 13px;
min-width: 130px;
}
.summary-card.income {
border-top: 3px solid var(--card-income);
}
.summary-card.expense {
border-top: 3px solid var(--card-expense);
color: var(--card-expense);
}
.summary-card.savings {
border-top: 3px solid var(--card-savings);
}
.summary-card.leftover {
border-top: 3px solid var(--card-leftover);
}
.budget-grid {
display: grid;
@@ -535,3 +502,170 @@ color: var(--btn-check);
.suggestion-list li:hover {
background: #334155;
}
/* Summary */
.budget-overview-row {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin: 8px 0 16px;
}
.budget-summary-box,
.budget-chart-box {
flex: 1 1 250px;
background-color: var(--bg-card);
padding: 8px 12px;
border-radius: 8px;
box-shadow: 0 1px 3px rgba(0,0,0,0.04);
min-width: 220px;
}
.budget-summary-box h3,
.budget-chart-box h3 {
margin: 0 0 6px;
font-size: 13px;
font-weight: 600;
color: var(--text-main);
}
.summary-list,
.category-summary-list {
list-style: none;
padding: 0;
margin: 0;
}
.summary-list li,
.category-summary-list li {
display: flex;
justify-content: space-between;
padding: 1px 0;
font-size: 12px;
font-weight: 500;
line-height: 1.4;
color: var(--text-main);
}
.summary-list li span:first-child {
font-weight: 600;
color: var(--text-sub);
}
.category-summary-list li {
border-top: 1px dashed var(--border-color);
padding: 3px 0;
font-size: 11.5px;
}
.highlight {
font-weight: 700;
}
.highlight.leftover.plus {
color: #15803d;
}
.highlight.leftover.minus {
color: #dc2626;
}
/* chart */
.chart-area {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
}
.chart-legend h4 {
margin: 0 0 6px;
font-size: 13px;
font-weight: 600;
}
.chart-area {
flex: 1;
min-width: 200px;
max-width: 340px;
}
canvas {
max-width: 100%;
height: auto;
}
@media (min-width: 769px) {
.budget-overview-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
align-items: start;
}
.budget-chart-box {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
min-height: 300px;
text-align: center;
}
.budget-chart-box .chart-row {
display: flex;
justify-content: center;
align-items: center;
}
.budget-chart-box .chart-area {
flex: 1;
max-width: 420px;
width: 100%;
}
.budget-chart-box canvas {
max-width: 100%;
height: auto;
}
}
@media (max-width: 768px) {
.budget-overview-row {
display: flex;
flex-direction: column;
gap: 12px;
}
.budget-summary-box,
.budget-chart-box {
width: 100%;
}
.chart-row {
flex-direction: column;
align-items: center;
}
.chart-area {
max-width: 100%;
}
.chart-legend {
width: 100%;
text-align: center;
padding-bottom: 10px;
}
.chart-area canvas {
max-width: 240px;
height: auto;
}
}

View File

@@ -0,0 +1,107 @@
body {
background-color: #f7f7f7;
font-family: 'Segoe UI', sans-serif;
color: #222;
margin: 0;
}
.meal-gallery-container {
max-width: 1200px;
margin: 0 auto;
padding: 2rem 1rem;
}
.meal-gallery-header {
text-align: center;
margin-bottom: 2rem;
}
.meal-gallery-header h1 {
font-size: 2.4rem;
font-weight: 600;
}
.search-container {
margin-top: 1rem;
position: relative;
max-width: 400px;
margin-inline: auto;
}
.search-container input {
width: 100%;
padding: 10px 38px 10px 12px;
border-radius: 25px;
border: 1px solid #ccc;
font-size: 1rem;
}
.search-container i {
position: absolute;
right: 12px;
top: 50%;
transform: translateY(-50%);
color: #888;
}
.meal-gallery-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
gap: 24px;
}
.meal-card {
background: white;
border-radius: 12px;
overflow: hidden;
box-shadow: 0 2px 8px rgba(0,0,0,0.05);
transition: transform 0.2s ease, box-shadow 0.2s ease;
display: flex;
flex-direction: column;
}
.meal-card:hover {
transform: scale(1.015);
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
}
.meal-card img {
width: 100%;
height: 200px;
object-fit: cover;
}
.meal-card-content {
padding: 16px;
display: flex;
flex-direction: column;
flex-grow: 1;
}
.meal-card-content h3 {
margin: 0 0 8px;
font-size: 1.2rem;
color: #111;
}
.meal-card-content p {
flex-grow: 1;
font-size: 0.95rem;
color: #555;
}
.btn-readmore {
align-self: flex-start;
background: #007d36;
color: white;
padding: 8px 12px;
border-radius: 6px;
text-decoration: none;
font-size: 0.95rem;
margin-top: 12px;
}
.btn-readmore:hover {
background: #005c27;
}

View File

@@ -165,11 +165,21 @@ h1 {
.card-view .card-container {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
grid-template-columns: repeat(7, 1fr); /* exakt 7 kolumner */
gap: var(--spacing);
width: 100%;
max-width: 100%;
}
@media (max-width: 1024px) {
.card-view .card-container {
grid-template-columns: repeat(3, 1fr);
}
}
@media (max-width: 640px) {
.card-view .card-container {
grid-template-columns: repeat(2, 1fr);
}
}
.meal-card {
position: relative;
height: 160px;
@@ -196,7 +206,6 @@ h1 {
content: '';
position: absolute;
inset: 0;
background: linear-gradient(to top, rgba(49, 130, 206, 0.3), rgba(0, 0, 0, 0.7), transparent);
pointer-events: none;
transition: opacity 0.3s ease;
opacity: 1;
@@ -215,14 +224,15 @@ h1 {
flex-direction: column;
justify-content: flex-start;
align-items: center;
background: rgba(0, 0, 0, 0.4);
background: rgba(0, 0, 0, 0.3); /* ev. öka från 0.4 till 0.5 */
text-align: center;
box-sizing: border-box;
}
.card-content .day {
font-size: 1rem;
font-size: 1.3rem;
font-weight: bold;
text-shadow: 0 1px 7px rgba(0, 0, 0, 1);
margin-bottom: 6px;
white-space: nowrap;
overflow: hidden;
@@ -230,9 +240,14 @@ h1 {
}
.card-content .meal {
font-size: 0.8rem;
font-size: 1rem;
line-height: 1.3;
margin-bottom: 2px;
text-shadow:
0 0 4px rgba(0, 0, 0, 0.9),
0 0 8px rgba(0, 0, 0, 0.7),
0 1px 10px rgba(0, 0, 0, 1);
word-break: break-word;
}
.menu-header {
@@ -270,3 +285,38 @@ h1 {
background-color: rgba(0, 0, 0, 0.1);
}
.school-meal-card-content {
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: center;
height: 100%;
overflow: hidden;
}
.school-meal-card-content .day {
font-size: 1rem;
font-weight: bold;
margin-bottom: 4px;
flex-shrink: 0; /* Förhindrar att tryckas bort */
}
.school-meal-card-content .meal {
font-size: clamp(0.55rem, 1.0vw, 0.8rem);
line-height: 1.2;
text-align: center;
word-break: break-word;
overflow-y: auto;
max-height: 100%; /* Begränsa höjden */
}
.school-meal-title .chevron {
display: inline-block;
transition: transform 0.3s ease;
transform: rotate(-90deg);
}
.school-meal-title .chevron.rotated {
transform: rotate(0deg);
}

View File

@@ -1,5 +1,5 @@
/* ==========================================================================
HEADER LEWEL DESIGN (utan meny)
HEADER <EFBFBD> LEWEL DESIGN (utan meny)
========================================================================== */
.top-bar {
display: flex;
@@ -106,7 +106,9 @@
flex-direction: column;
z-index: 1001;
}
.auth-dropdown a i {
margin-right: 6px;
}
.auth-dropdown a {
padding: 8px 12px;
color: #1F2C3C;
@@ -128,7 +130,7 @@
}
/* ==========================================================================
LAYOUT OMBYGGD STRUKTUR
LAYOUT <EFBFBD> OMBYGGD STRUKTUR
========================================================================== */
body {
background-color: #1F2C3C;
@@ -174,7 +176,7 @@ body {
}
/* ==========================================================================
NAVIGATIONSLISTA KOMPAKT STIL
NAVIGATIONSLISTA <EFBFBD> KOMPAKT STIL
========================================================================== */
.nav-list {
list-style: none;

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

View File

@@ -5,6 +5,7 @@ app.controller('BudgetController', function ($scope, $http) {
$scope.loading = false;
$scope.error = null;
$scope.menuOpen = false;
$scope.chartMode = "pie";
const today = new Date();
$scope.selectedYear = today.getFullYear();
@@ -140,6 +141,7 @@ app.controller('BudgetController', function ($scope, $http) {
})
.finally(function () {
$scope.loading = false;
setTimeout($scope.drawCategoryChart, 0);
});
};
@@ -760,8 +762,70 @@ $scope.addItemFromDefinition = function (cat) {
});
};
$scope.isExpenseCategory = function (cat) {
return cat.items.some(i => i.isExpense);
};
$scope.drawCategoryChart = function () {
const ctx = document.getElementById("expenseChart");
if (!ctx || !$scope.budget?.categories) return;
const labels = [];
const data = [];
const colors = [];
$scope.budget.categories.forEach(cat => {
const sum = cat.items
.filter(i => i.isExpense)
.reduce((acc, i) => acc + i.amount, 0);
if (sum > 0) {
labels.push(cat.name);
data.push(sum);
colors.push(cat.color || "#94a3b8");
}
});
const total = data.reduce((a, b) => a + b, 0);
const topIndex = data.indexOf(Math.max(...data));
$scope.topExpenseCategory = topIndex !== -1 ? {
name: labels[topIndex],
value: data[topIndex],
percent: (data[topIndex] / total) * 100
} : null;
if (window.expenseChart && typeof window.expenseChart.destroy === 'function') {
window.expenseChart.destroy();
}
window.expenseChart = new Chart(ctx, {
type: 'pie',
data: {
labels: labels,
datasets: [{
data: data,
backgroundColor: colors
}]
},
options: {
responsive: true,
plugins: {
legend: { display: false },
tooltip: {
callbacks: {
label: ctx => `${ctx.label}: ${ctx.formattedValue} kr`
}
}
}
}
});
};
$scope.loadItemDefinitions().then(() => {
$scope.loadBudget();
});
});

View File

@@ -4,80 +4,94 @@
}
angular.module('mealMenuApp', ['ngSanitize'])
.controller('MealMenuController', function ($scope, $http, $sce) {
.controller('MealMenuController', function ($scope, $http, $sce, $timeout) {
console.log("Controller initierad");
const savedView = localStorage.getItem('mealViewMode');
$scope.viewMode = savedView === 'card' || savedView === 'list' ? savedView : 'card';
$scope.tooltip = {};
$scope.meals = [];
$scope.menu = {};
const today = new Date();
$scope.selectedWeek = getWeek(today);
$scope.selectedYear = today.getFullYear();
$scope.daysOfWeek = ["Måndag", "Tisdag", "Onsdag", "Torsdag", "Fredag", "Lördag", "Söndag"];
$scope.loadMeals = function () {
console.log("Hämtar måltider...");
return $http.get('/api/mealMenuApi/getMeals')
.then(res => {
console.log("Måltider hämtade:", res.data);
$scope.meals = res.data;
console.log("Alla måltider med bilder:", $scope.meals);
return res;
})
.catch(err => console.error("Fel vid hämtning av måltider:", err));
};
$scope.loadMenu = function () {
console.log("Hämtar meny för vecka:", $scope.selectedWeek, $scope.selectedYear);
$http.get('/api/mealMenuApi/menu', {
$http.get('/api/mealMenuApi/getWeeklyMenu', {
params: { weekNumber: $scope.selectedWeek, year: $scope.selectedYear }
}).then(res => {
console.log("Menyposter hämtade:", res.data);
const rawMenu = res.data;
$scope.menu = {};
res.data.forEach(item => {
rawMenu.forEach(item => {
const dayIndex = item.DayOfWeek - 1;
if (dayIndex < 0 || dayIndex >= $scope.daysOfWeek.length) {
console.warn("Ogiltig dag:", item.DayOfWeek);
return;
}
const day = $scope.daysOfWeek[dayIndex];
$scope.menu[day] = {};
['breakfast', 'lunch', 'dinner'].forEach(type => {
// Konvertera till PascalCase
const capitalType = type.charAt(0).toUpperCase() + type.slice(1);
const idKey = capitalType + 'MealId';
const nameKey = capitalType + 'MealName';
if (item[idKey]) {
const m = $scope.meals.find(x => x.Id == item[idKey]);
console.log(`Match för ${type} (${day}):`, m);
$scope.menu[day][type + 'MealId'] = item[idKey];
$scope.menu[day][type + 'MealName'] = m?.Name || item[nameKey] || 'Okänd rätt';
if (m && !$scope.menu[day].imageUrl) {
if (m.ImageData) {
const mime = m.ImageMimeType || "image/jpeg";
$scope.menu[day].imageUrl = `data:${mime};base64,${m.ImageData}`;
} else if (m.ImageUrl) {
$scope.menu[day].imageUrl = m.ImageUrl.startsWith('/')
? m.ImageUrl
: '/' + m.ImageUrl;
}
}
}
});
console.warn("Item:", item);
console.log("day: " + day + "(" + dayIndex + " ) item: " + item.DinnerMealName);
const thumb = item.DinnerMealThumbnail;
$scope.menu[day] = {
breakfastMealId: item.BreakfastMealId,
breakfastMealName: item.BreakfastMealName,
lunchMealId: item.LunchMealId,
lunchMealName: item.LunchMealName,
dinnerMealId: item.DinnerMealId,
dinnerMealName: item.DinnerMealName,
imageUrl: thumb ? `data:image/webp;base64,${thumb}` : '/img/default-thumbnail.webp'
};
});
}).catch(err => console.error("Fel vid hämtning av meny:", err));
$scope.loadSchoolMeals(); // Lägg till här
console.log("Byggd meny:", $scope.menu);
}).catch(err => console.error("Fel vid hämtning av veckomeny:", err));
};
$scope.schoolMeals = [];
$scope.schoolMealsBySchool = [];
$scope.schoolSensors = [
{ name: "William Engelbrektsskolan", entity: "sensor.engelbrektsskolan" },
{ name: "Louise - Nyeds skolan", entity: "sensor.nyedsskola" },
{ name: "Ludwig - Skogsgläntan", entity: "sensor.skogsglantan" }
];
$scope.schoolMealsBySchool = [];
$scope.loadSchoolMeals = function () {
const savedExpanded = JSON.parse(localStorage.getItem("expandedSchools") || "{}");
$scope.schoolMealsBySchool = $scope.schoolSensors.map(sensor => ({
name: sensor.name,
entity: sensor.entity,
days: [],
expanded: savedExpanded[sensor.entity] || false
}));
$scope.schoolMealsBySchool.forEach(school => {
$http.get(`/api/mealmenuapi/skolmat?week=${$scope.selectedWeek}&sensor=${school.entity}`)
.then(res => {
school.days = res.data;
})
.catch(() => {
school.days = [];
});
});
};
$scope.toggleSchoolExpanded = function (school) {
school.expanded = !school.expanded;
const expandedState = {};
$scope.schoolMealsBySchool.forEach(s => {
expandedState[s.entity] = s.expanded;
});
localStorage.setItem("expandedSchools", JSON.stringify(expandedState));
};
$scope.openMeal = function (mealId) {
if (!mealId) return;
@@ -85,7 +99,7 @@ angular.module('mealMenuApp', ['ngSanitize'])
};
$scope.getDayImage = function (day) {
return $scope.menu[day]?.imageUrl || '/images/default-meal.jpg';
return $scope.menu[day]?.imageUrl || '/img/default-thumbnail.webp';
};
$scope.getMealIdByDay = function (day) {
@@ -120,34 +134,24 @@ angular.module('mealMenuApp', ['ngSanitize'])
const yearStart = new Date(d.getFullYear(), 0, 1);
return Math.ceil((((d - yearStart) / 86400000) + 1) / 7);
}
$scope.getViewIcon = function () {
return $scope.viewMode === 'list' ? '🗒️' : '▣';
};
$scope.toggleView = function () {
$scope.viewMode = $scope.viewMode === 'list' ? 'card' : 'list';
localStorage.setItem('mealViewMode', $scope.viewMode); // ← spara läget
localStorage.setItem('mealViewMode', $scope.viewMode);
$timeout(() => {
const viewBtn = document.getElementById('toggle-view');
if (viewBtn) viewBtn.textContent = $scope.getViewIcon();
}, 0);
};
console.log("Initierar måltidsladdning...");
$scope.loadMeals().then(() => {
console.log("Laddar meny efter måltider...");
$scope.loadMenu();
setTimeout(() => {
const viewBtn = document.getElementById('toggle-view');
if (viewBtn) viewBtn.textContent = $scope.getViewIcon();
}, 0);
});
console.log("Initierar meny och måltidsladdning...");
$scope.loadMenu();
});
document.addEventListener("DOMContentLoaded", function () {
const themeBtn = document.getElementById('toggle-theme');
const viewBtn = document.getElementById('toggle-view');
@@ -168,7 +172,4 @@ document.addEventListener("DOMContentLoaded", function () {
const systemPrefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
setTheme(systemPrefersDark ? 'dark' : 'light');
}
// Initiera ikon för vy
});

View File

@@ -5,7 +5,7 @@ if ('serviceWorker' in navigator) {
window.addEventListener('load', function () {
navigator.serviceWorker.register('/service-worker.js')
.then(function (registration) {
console.log('✅ Service Worker registrerad med scope:', registration.scope);
//console.log('✅ Service Worker registrerad med scope:', registration.scope);
subscribeToPush().catch(console.error);
})
.catch(function (error) {
@@ -16,7 +16,11 @@ if ('serviceWorker' in navigator) {
async function subscribeToPush() {
const registration = await navigator.serviceWorker.ready;
const publicVapidKey = await fetch('/api/push/vapid-public-key').then(r => r.text());
const publicVapidKey = await fetch('/api/push/vapid-public-key')
.then(r => r.text())
.then(key => {
return key;
});
const subscription = await registration.pushManager.subscribe({
userVisibleOnly: true,
@@ -29,7 +33,7 @@ async function subscribeToPush() {
headers: { 'Content-Type': 'application/json' }
});
console.log('✅ Push-prenumeration skickad');
//console.log('✅ Push-prenumeration skickad');
}
async function enablePush() {

View File

@@ -22,27 +22,36 @@ app.controller("TodoController", function ($scope, $http) {
$scope.AddTask = function () {
const title = $scope.NewTask.Title;
const priority = $scope.NewTask.Priority;
if (typeof title !== 'string' || !title.trim()) {
return;
}
const task = {
Title: title.trim(),
Status: "ideas",
Priority: priority
Priority: priority,
Description: $scope.NewTask.Description || "",
Tags: $scope.NewTask.Tags || "",
AssignedTo: $scope.NewTask.AssignedTo || null
};
$http.post("/Admin/AddTodoTask", task, {
headers: { 'Content-Type': 'application/json' }
}).then(res => {
$scope.Tasks.push(res.data);
$scope.NewTask.Title = '';
$scope.NewTask.Priority = 2;
$scope.NewTask = {
Title: '',
Description: '',
Tags: '',
AssignedTo: '',
Priority: 2
};
}, err => {
console.error("Fel vid POST:", err);
});
};
$scope.MoveTask = function (droppedTask, newStatus) {
const task = $scope.Tasks.find(t => t.Id === droppedTask.Id);

View File

@@ -4,7 +4,7 @@
"start_url": "/",
"display": "standalone",
"background_color": "#1F2C3C",
"theme_color": "#6a0dad",
"theme_color": "#1F2C3C",
"icons": [
{
"src": "/images/lewel-icon.png",