New stuff

This commit is contained in:
Elias Jansson
2025-02-24 15:43:47 +01:00
parent 36429beada
commit 4635724569
22 changed files with 1493 additions and 294 deletions

View File

@@ -10,6 +10,7 @@
<ItemGroup>
<PackageReference Include="AngularJS.Core" Version="1.8.2" />
<PackageReference Include="HtmlAgilityPack" Version="1.11.67" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="7.0.9" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="6.0.12">
<PrivateAssets>all</PrivateAssets>
@@ -20,4 +21,9 @@
<PackageReference Include="MySql.EntityFrameworkCore" Version="8.0.5" />
</ItemGroup>
<ItemGroup>
<Folder Include="NewFolder1\" />
<Folder Include="NewFolder\" />
</ItemGroup>
</Project>

View File

@@ -19,16 +19,31 @@ namespace Aberwyn.Controllers
[HttpGet("items")]
public IActionResult GetBudgetItems([FromQuery] int month, [FromQuery] int year)
{
var items = _budgetService.GetBudgetItems(month, year);
return Ok(items);
try
{
var items = _budgetService.GetBudgetItems(month, year);
return Ok(items);
}
catch (Exception ex)
{
// Log the exception (consider using a logging framework)
return StatusCode(500, new { message = "An error occurred while fetching budget items.", details = ex.Message });
}
}
// New endpoint to get categories
[HttpGet("categories")]
public IActionResult GetCategories()
{
var categories = _budgetService.GetCategories();
return Ok(categories);
try
{
var categories = _budgetService.GetCategories();
return Ok(categories);
}
catch (Exception ex)
{
// Log the exception
return StatusCode(500, new { message = "An error occurred while fetching categories.", details = ex.Message });
}
}
[HttpPut("items")]
@@ -39,15 +54,20 @@ namespace Aberwyn.Controllers
return BadRequest("Invalid budget item data.");
}
// Assuming you have a method in your BudgetService to update an item
var result = _budgetService.UpdateBudgetItem(item);
if (result)
try
{
return Ok("Item updated successfully.");
var result = _budgetService.UpdateBudgetItem(item);
if (result)
{
return Ok("Item updated successfully.");
}
return StatusCode(500, "Error updating item.");
}
catch (Exception ex)
{
// Log the exception
return StatusCode(500, new { message = "An error occurred while updating the item.", details = ex.Message });
}
return StatusCode(500, "Error updating item.");
}
[HttpPost("items")]
@@ -58,17 +78,20 @@ namespace Aberwyn.Controllers
return BadRequest("Invalid budget item data.");
}
// Assuming you have a method in your BudgetService to add an item
var result = _budgetService.AddBudgetItem(item);
if (result)
try
{
return CreatedAtAction(nameof(GetBudgetItems), new { id = item.ID }, item);
var result = _budgetService.AddBudgetItem(item);
if (result)
{
return CreatedAtAction(nameof(GetBudgetItems), new { id = item.ID }, item);
}
return StatusCode(500, "Error adding item.");
}
catch (Exception ex)
{
// Log the exception
return StatusCode(500, new { message = "An error occurred while adding the item.", details = ex.Message });
}
return StatusCode(500, "Error adding item.");
}
}
}

View File

@@ -11,12 +11,15 @@ namespace Aberwyn.Controllers
{
private readonly ILogger<HomeController> _logger;
private readonly BudgetService _budgetService;
private readonly MenuService _menuService;
// Constructor to inject dependencies
public HomeController(ILogger<HomeController> logger, BudgetService budgetService)
public HomeController(ILogger<HomeController> logger, BudgetService budgetService, MenuService menuService)
{
_logger = logger;
_budgetService = budgetService;
_menuService = menuService;
}
public IActionResult Index()
@@ -34,6 +37,31 @@ namespace Aberwyn.Controllers
return View();
}
public IActionResult Menu()
{
// Get the current date and week
var currentDate = DateTime.Now;
var currentYear = currentDate.Year;
var calendar = new GregorianCalendar();
var currentWeek = calendar.GetWeekOfYear(currentDate, CalendarWeekRule.FirstFourDayWeek, DayOfWeek.Monday);
// Fetch weekly menu items for the current week and year
var weeklyMenus = _menuService.GetWeeklyMenu(currentWeek, currentYear);
var meals = _menuService.GetMeals(); // Fetch all meals
var model = new MenuViewModel
{
WeeklyMenus = weeklyMenus, // List of WeeklyMenu items
Meals = meals, // List of available meals
WeekNumber = currentWeek, // Dynamically set the current week number
Year = currentYear // Dynamically set the current year
};
return View(model);
}
// Optimized Budget Action to fetch filtered data directly from the database
public IActionResult Budget(string month, int? year)
{

View File

@@ -0,0 +1,81 @@
// MealMenuApiController.cs
using Aberwyn.Models;
using Aberwyn.Data;
using Microsoft.AspNetCore.Mvc;
namespace Aberwyn.Controllers
{
[ApiController]
[Route("api/[controller]")]
public class MealMenuApiController : ControllerBase
{
private readonly MenuService _menuService;
public MealMenuApiController(MenuService menuService)
{
_menuService = menuService;
}
// API endpoint to fetch the weekly menu
[HttpGet("menu")]
public IActionResult GetMenu(int weekNumber, int year)
{
var menu = _menuService.GetWeeklyMenu(weekNumber, year);
if (menu == null || !menu.Any())
{
// Return an empty object or array instead of a 404 error
return Ok(new List<WeeklyMenu>()); // Return empty list if no menu is found
}
return Ok(menu); // Return the menu as a JSON response
}
// API endpoint to fetch the weekly menu
[HttpGet("getMeals")]
public IActionResult GetMeals()
{
var meals = _menuService.GetMeals();
if (meals == null || !meals.Any())
{
// Return an empty object or array instead of a 404 error
return Ok(new List<Meal>()); // Return empty list if no menu is found
}
return Ok(meals); // Return the menu as a JSON response
}
// API endpoint to save the updated menu (if you need this)
[HttpPut("menu")]
public IActionResult SaveMenu([FromBody] MenuViewModel weeklyMenu)
{
// Process and save the menu (you may need to write this logic)
_menuService.UpdateWeeklyMenu(weeklyMenu);
return Ok("Menu saved successfully");
}
[HttpPost("addMeal")]
public IActionResult AddMeal([FromBody] Meal meal)
{
if (meal == null || string.IsNullOrWhiteSpace(meal.Name))
{
return BadRequest("Meal Name is required.");
}
var mealId = _menuService.AddMeal(meal);
if (mealId > 0)
{
return Ok(new { Message = "Meal added successfully", MealId = mealId });
}
return StatusCode(500, "Failed to add meal.");
}
}
}

View File

@@ -1,45 +1,157 @@
using Microsoft.AspNetCore.Mvc;
using HtmlAgilityPack;
using Microsoft.AspNetCore.Mvc;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;
[Route("api/[controller]")]
[ApiController]
public class RealEstateApiController : ControllerBase
public class RealEstateController : ControllerBase
{
private readonly HttpClient _httpClient;
public RealEstateApiController(HttpClient httpClient)
// Step 1: Find image links without downloading
[HttpPost("findImages")]
public async Task<IActionResult> FindImages([FromBody] UrlRequest request)
{
_httpClient = httpClient;
}
[HttpPost("download")]
public async Task<IActionResult> DownloadWebsite([FromBody] WebsiteRequest request)
{
if (string.IsNullOrWhiteSpace(request.Url))
if (string.IsNullOrEmpty(request.Url))
{
return BadRequest("URL cannot be empty.");
return BadRequest("URL is required.");
}
try
{
var response = await _httpClient.GetAsync(request.Url);
response.EnsureSuccessStatusCode();
var content = await response.Content.ReadAsStringAsync();
var imageLinks = await FetchImageLinksFromWebsiteAsync(request.Url);
return Ok(new { images = imageLinks });
}
catch (Exception ex)
{
return StatusCode(500, $"Error: {ex.Message}");
}
}
// Optional: Save content to a file
System.IO.File.WriteAllText("DownloadedWebsite.html", content);
// Step 2: Download images when user chooses to do so
[HttpPost("download")]
public async Task<IActionResult> DownloadWebsite([FromBody] UrlRequest request)
{
if (string.IsNullOrEmpty(request.Url))
{
return BadRequest("URL is required.");
}
return Ok("Website downloaded successfully.");
try
{
var images = await FetchImagesFromWebsiteAsync(request.Url);
// Log the fetched images
Console.WriteLine("Fetched images: " + string.Join(", ", images));
return Ok(new { images });
}
catch (Exception ex)
{
return StatusCode(500, $"Error: {ex.Message}");
}
}
// Fetch just the image links
private async Task<List<string>> FetchImageLinksFromWebsiteAsync(string url)
{
var imageLinks = new List<string>();
var httpClient = new HttpClient();
// Set User-Agent to imitate a browser request
httpClient.DefaultRequestHeaders.Add("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3");
try
{
// Send the GET request with headers
var response = await httpClient.GetStringAsync(url);
// Load the HTML into HtmlAgilityPack
var htmlDocument = new HtmlDocument();
htmlDocument.LoadHtml(response);
// Select all <li> elements containing the images you want
var liNodes = htmlDocument.DocumentNode.SelectNodes("//li[contains(@class, 'flex items-center justify-center')]");
if (liNodes != null)
{
foreach (var liNode in liNodes)
{
// Find the <img> tag within the <li> node
var imgNode = liNode.SelectSingleNode(".//img[contains(@class, 'absolute top-0 left-0 w-full h-full')]");
if (imgNode != null)
{
var imgSrc = imgNode.GetAttributeValue("src", "");
if (!string.IsNullOrEmpty(imgSrc))
{
// Ensure absolute URL if necessary
if (!Uri.IsWellFormedUriString(imgSrc, UriKind.Absolute))
{
var baseUri = new Uri(url);
imgSrc = new Uri(baseUri, imgSrc).ToString();
}
imageLinks.Add(imgSrc);
}
}
}
}
}
catch (HttpRequestException ex)
{
return StatusCode(500, $"Error downloading website: {ex.Message}");
// Catch any network-related exceptions (403, 404, etc.)
Console.WriteLine($"Error: {ex.Message}");
throw new Exception($"Error fetching images: {ex.Message}");
}
return imageLinks;
}
// Fetch images and download
private async Task<List<string>> FetchImagesFromWebsiteAsync(string url)
{
var images = new List<string>();
var httpClient = new HttpClient();
var response = await httpClient.GetStringAsync(url);
// Load the HTML into HtmlAgilityPack
var htmlDocument = new HtmlDocument();
htmlDocument.LoadHtml(response);
// Select all <img> elements with the specified class
var imgNodes = htmlDocument.DocumentNode.SelectNodes("//li//img");
if (imgNodes != null)
{
foreach (var imgNode in imgNodes)
{
var imgSrc = imgNode.GetAttributeValue("src", "");
if (!string.IsNullOrEmpty(imgSrc))
{
// Ensure absolute URL if necessary
if (!Uri.IsWellFormedUriString(imgSrc, UriKind.Absolute))
{
var baseUri = new Uri(url);
imgSrc = new Uri(baseUri, imgSrc).ToString();
}
images.Add(imgSrc);
}
}
}
return images;
}
}
public class WebsiteRequest
public class UrlRequest
{
public string Url { get; set; }
}

View File

@@ -1,23 +1,31 @@
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)
public BudgetService(IConfiguration configuration, IHostEnvironment env) // Update constructor
{
_configuration = configuration;
_env = env; // Initialize the environment field
}
public MySqlConnection GetConnection()
{
var connectionString = _configuration.GetConnectionString("DefaultConnection");
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())
@@ -25,9 +33,9 @@ namespace Aberwyn.Data
connection.Open();
string query = @"
UPDATE tblBudgetItems
SET Name = @name, Amount = @amount
WHERE idtblBudgetItems = @id";
UPDATE tblBudgetItems
SET Name = @name, Amount = @amount
WHERE idtblBudgetItems = @id";
using (var cmd = new MySqlCommand(query, connection))
{
@@ -48,17 +56,17 @@ namespace Aberwyn.Data
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";
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))
{
@@ -96,8 +104,8 @@ namespace Aberwyn.Data
connection.Open();
string query = @"
INSERT INTO tblBudgetItems (Name, Amount, Category, Month, Year)
VALUES (@name, @amount, @category, @month, @year)";
INSERT INTO tblBudgetItems (Name, Amount, Category, Month, Year)
VALUES (@name, @amount, @category, @month, @year)";
using (var cmd = new MySqlCommand(query, connection))
{
@@ -111,7 +119,6 @@ namespace Aberwyn.Data
}
}
// New method to fetch all categories
public List<string> GetCategories()
{
@@ -138,5 +145,4 @@ namespace Aberwyn.Data
return categories;
}
}
}

193
Aberwyn/Data/MenuService.cs Normal file
View File

@@ -0,0 +1,193 @@
using MySql.Data.MySqlClient;
using System.Collections.Generic;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;
using Aberwyn.Models;
namespace Aberwyn.Data
{
public class MenuService
{
private readonly IConfiguration _configuration;
private readonly IHostEnvironment _env;
public MenuService(IConfiguration configuration, IHostEnvironment env)
{
_configuration = configuration;
_env = env;
}
private MySqlConnection GetConnection()
{
var connectionString = _env.IsDevelopment()
? _configuration.GetConnectionString("DefaultConnection")
: _configuration.GetConnectionString("ProductionConnection");
return new MySqlConnection(connectionString);
}
public List<WeeklyMenu> GetWeeklyMenu(int weekNumber, int year)
{
var weeklyMenu = new List<WeeklyMenu>();
using (var connection = GetConnection())
{
connection.Open();
string query = @"
SELECT wm.Id, wm.DayOfWeek, wm.DinnerMealId, wm.LunchMealId, wm.WeekNumber, wm.Year,
dm.Name AS DinnerMealName, lm.Name AS LunchMealName
FROM WeeklyMenu wm
LEFT JOIN Meals dm ON wm.DinnerMealId = dm.Id
LEFT JOIN Meals lm ON wm.LunchMealId = lm.Id
WHERE wm.WeekNumber = @weekNumber AND wm.Year = @year";
using (var cmd = new MySqlCommand(query, connection))
{
cmd.Parameters.AddWithValue("@weekNumber", weekNumber);
cmd.Parameters.AddWithValue("@year", year);
using (var reader = cmd.ExecuteReader())
{
while (reader.Read())
{
weeklyMenu.Add(new WeeklyMenu
{
Id = reader.GetInt32("Id"),
DayOfWeek = reader.GetInt32("DayOfWeek"),
DinnerMealId = reader.IsDBNull(reader.GetOrdinal("DinnerMealId")) ? (int?)null : reader.GetInt32("DinnerMealId"),
LunchMealId = reader.IsDBNull(reader.GetOrdinal("LunchMealId")) ? (int?)null : reader.GetInt32("LunchMealId"),
WeekNumber = reader.GetInt32("WeekNumber"),
Year = reader.GetInt32("Year"),
DinnerMealName = reader.IsDBNull(reader.GetOrdinal("DinnerMealName")) ? null : reader.GetString("DinnerMealName"),
LunchMealName = reader.IsDBNull(reader.GetOrdinal("LunchMealName")) ? null : reader.GetString("LunchMealName"),
});
}
}
}
}
return weeklyMenu;
}
public List<Meal> GetMeals()
{
var meals = new List<Meal>();
using (var connection = GetConnection())
{
connection.Open();
string query = "SELECT Id, Name FROM Meals";
using (var cmd = new MySqlCommand(query, connection))
{
using (var reader = cmd.ExecuteReader())
{
while (reader.Read())
{
meals.Add(new Meal
{
Id = reader.GetInt32("Id"),
Name = reader.GetString("Name")
});
}
}
}
}
return meals;
}
public void UpdateWeeklyMenu(MenuViewModel menuData)
{
if (menuData == null || menuData.WeeklyMenus == null)
throw new ArgumentNullException(nameof(menuData), "Menu data or weekly menus cannot be null.");
using (var connection = GetConnection())
{
connection.Open();
using (var transaction = connection.BeginTransaction())
{
try
{
// Loop over each WeeklyMenu in the WeeklyMenus list
foreach (var weeklyMenu in menuData.WeeklyMenus)
{
int dayOfWeek = weeklyMenu.DayOfWeek;
var menu = weeklyMenu;
// SQL query to update the existing records
string query = @"
UPDATE WeeklyMenu
SET
DinnerMealId = @dinnerMealId,
LunchMealId = @lunchMealId,
Cook = @cook
WHERE DayOfWeek = @dayOfWeek
AND WeekNumber = @weekNumber
AND Year = @year;";
using (var cmd = new MySqlCommand(query, connection, transaction))
{
cmd.Parameters.AddWithValue("@dayOfWeek", dayOfWeek);
cmd.Parameters.AddWithValue("@dinnerMealId", menu.DinnerMealId ?? (object)DBNull.Value);
cmd.Parameters.AddWithValue("@lunchMealId", menu.LunchMealId ?? (object)DBNull.Value);
cmd.Parameters.AddWithValue("@weekNumber", menu.WeekNumber);
cmd.Parameters.AddWithValue("@year", menu.Year);
cmd.Parameters.AddWithValue("@cook", menu.Cook ?? (object)DBNull.Value);
cmd.ExecuteNonQuery();
}
}
transaction.Commit();
}
catch (Exception ex)
{
transaction.Rollback();
Console.WriteLine($"Error updating weekly menu: {ex.Message}");
throw;
}
}
}
}
public int AddMeal(Meal meal)
{
using (var connection = GetConnection())
{
connection.Open();
string query = @"
INSERT INTO Meals (Name, Description, ProteinType, CarbType, RecipeUrl, CreatedAt)
VALUES (@name, @description, @proteinType, @carbType, @recipeUrl, NOW());
SELECT LAST_INSERT_ID();";
using (var cmd = new MySqlCommand(query, connection))
{
cmd.Parameters.AddWithValue("@name", meal.Name);
cmd.Parameters.AddWithValue("@description", string.IsNullOrWhiteSpace(meal.Description) ? (object)DBNull.Value : meal.Description);
cmd.Parameters.AddWithValue("@proteinType", string.IsNullOrWhiteSpace(meal.ProteinType) ? (object)DBNull.Value : meal.ProteinType);
cmd.Parameters.AddWithValue("@carbType", string.IsNullOrWhiteSpace(meal.CarbType) ? (object)DBNull.Value : meal.CarbType);
cmd.Parameters.AddWithValue("@recipeUrl", string.IsNullOrWhiteSpace(meal.RecipeUrl) ? (object)DBNull.Value : meal.RecipeUrl);
var result = cmd.ExecuteScalar();
return result != null ? Convert.ToInt32(result) : 0;
}
}
}
private int GetDayOfWeek(string day)
{
return day switch
{
"Monday" => 1,
"Tuesday" => 2,
"Wednesday" => 3,
"Thursday" => 4,
"Friday" => 5,
"Saturday" => 6,
"Sunday" => 7,
_ => throw new System.ArgumentException("Invalid day name")
};
}
}
}
// Models for Meals and WeeklyMenu

View File

@@ -1,27 +1,33 @@
# Base image for runtime
FROM mcr.microsoft.com/dotnet/aspnet:6.0 AS base
WORKDIR /app
EXPOSE 80
EXPOSE 443
# Leave the ports exposed to allow Unraid to configure them
EXPOSE 80 443
# Image for building the project
FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build
WORKDIR /src
# Copy the .csproj and restore dependencies
COPY ["Aberwyn.csproj", "./"]
RUN dotnet restore "Aberwyn.csproj"
COPY Aberwyn.csproj ./
RUN dotnet restore
# Copy everything else and build the app
COPY . .
RUN dotnet build "Aberwyn.csproj" -c Release -o /app/build
RUN dotnet build Aberwyn.csproj -c Release -o /app/build
# Publish the app to the /app/publish folder
FROM build AS publish
RUN dotnet publish "Aberwyn.csproj" -c Release -o /app/publish
RUN dotnet publish Aberwyn.csproj -c Release -o /app/publish
# Use the base runtime image to run the app
FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
# Use environment variable for ports to allow flexibility
ENV ASPNETCORE_URLS="http://+:80"
# Set entry point to start the application
ENTRYPOINT ["dotnet", "Aberwyn.dll"]

View File

@@ -8,12 +8,15 @@
public class BudgetItem
{
public int ID { get; set; }
public string Name { get; set; }
// Option 1: Initialize with default values
public string Name { get; set; } = string.Empty; // Initialize to an empty string
public decimal Amount { get; set; }
public string Category { get; set; }
public string SubCategory { get; set; }
public string Category { get; set; } = string.Empty; // Initialize to an empty string
public string SubCategory { get; set; } = string.Empty; // Initialize to an empty string
public int Month { get; set; }
public int Year { get; set; }
public string Description { get; set; }
public string Description { get; set; } = string.Empty; // Initialize to an empty string
}
}

View File

@@ -0,0 +1,38 @@
using System.Collections.Generic;
using Aberwyn.Data;
using Aberwyn.Models;
namespace Aberwyn.Models
{
public class MenuViewModel
{
public List<Meal> Meals { get; set; } // List of all available meals
public List<WeeklyMenu> WeeklyMenus { get; set; } // List of weekly menu entries
public int WeekNumber { get; set; } // Week number for the menu
public int Year { get; set; } // Year for the menu
}
public class WeeklyMenu
{
public int Id { get; set; }
public int DayOfWeek { get; set; }
public int? DinnerMealId { get; set; }
public int? LunchMealId { get; set; }
public string Cook { get; set; }
public int WeekNumber { get; set; }
public int Year { get; set; }
public string DinnerMealName { get; set; }
public string LunchMealName { get; set; }
}
public class Meal
{
public int Id { get; set; }
public string Name { get; set; }
public string Description { get; set; }
public string ProteinType { get; set; }
public string CarbType { get; set; }
public string RecipeUrl { get; set; }
public DateTime CreatedAt { get; set; }
}
}

View File

@@ -14,8 +14,11 @@ builder.Services.AddHttpClient(); // Register HttpClient
builder.Services.AddDbContext<BudgetContext>(options =>
options.UseMySQL(builder.Configuration.GetConnectionString("BudgetDb")));
// Register your BudgetService with a scoped lifetime
builder.Services.AddScoped<BudgetService>();
builder.Services.AddScoped<MenuService>();
var app = builder.Build();
@@ -26,7 +29,7 @@ if (!app.Environment.IsDevelopment())
app.UseHsts();
}
app.UseHttpsRedirection();
//app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();

View File

@@ -0,0 +1,31 @@
using Aberwyn.Data;
using Aberwyn.Models;
using Microsoft.AspNetCore.Mvc;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace Aberwyn.ViewComponents
{
public class RightSidebarViewComponent : ViewComponent
{
private readonly BudgetService _budgetService;
public RightSidebarViewComponent(BudgetService budgetService)
{
_budgetService = budgetService;
}
public async Task<IViewComponentResult> InvokeAsync()
{
// Get the current month and year
int currentMonth = DateTime.Now.Month;
int currentYear = DateTime.Now.Year;
// Fetch the budget items using the service
var budgetItems = _budgetService.GetBudgetItems(currentMonth, currentYear);
return View(budgetItems);
}
}
}

View File

@@ -1,7 +1,7 @@
@model Aberwyn.Models.BudgetModel
@{
Layout = "_Layout"; // Assuming you have a layout file named _Layout.cshtml
Layout = "_Layout";
}
<!DOCTYPE html>
@@ -10,32 +10,29 @@
<meta charset="utf-8">
<title>Budget Overview</title>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.8.2/angular.min.js"></script>
<link rel="stylesheet" type="text/css" href="~/css/site.css">
<link rel="stylesheet" type="text/css" href="~/css/budget.css">
</head>
<body ng-controller="BudgetController">
<div class="budget-page">
<h1>Budget Overview</h1>
<form ng-submit="filterBudget()">
<label for="month">Month:</label>
<select id="month" ng-model="selectedMonth">
<option ng-repeat="month in months" value="{{ month }}">{{ month }}</option>
</select>
<label for="year">Year:</label>
<select id="year" ng-model="selectedYear">
<option ng-repeat="year in years" value="{{ year }}">{{ year }}</option>
</select>
<button type="submit">Filter</button>
</form>
<div class="date-picker" ng-click="toggleDatePicker($event)">
<button type="button" class="date-picker-toggle">
{{ selectedMonthName() }} {{ selectedYear }}
</button>
<div class="date-picker-dropdown" ng-show="isDatePickerVisible">
<select ng-model="selectedMonth" ng-options="month as monthName for (month, monthName) in monthOptions" ng-change="updateMonthYear()"></select>
<select ng-model="selectedYear" ng-options="year for year in years" ng-change="updateMonthYear()"></select>
</div>
</div>
<div class="budget-container">
<div ng-repeat="category in categories" class="category-block">
<div class="category-header">
{{ category }}
<span class="total">Total: {{ sumForCategory(category) }}</span> <!-- Total moved to the header -->
<span class="total">Total: {{ sumForCategory(category) }}</span>
</div>
<div class="items-wrapper">
@@ -49,7 +46,7 @@
</div>
</div>
<div class="add-item" ng-click="addNewItem(category)">+</div> <!-- Button for adding items -->
<div class="add-item" ng-click="addNewItem(category)">+</div>
</div>
</div>
</div>
@@ -65,7 +62,7 @@
$scope.selectedMonth = (today.getMonth() + 1).toString();
$scope.selectedYear = today.getFullYear();
$scope.categories = [];
$scope.budgetItems = @Html.Raw(Json.Serialize(Model.BudgetItems)); // Initialize budgetItems with server-side data
$scope.budgetItems = @Html.Raw(Json.Serialize(Model.BudgetItems)); // Initialize with server-side data
$scope.loadCategories = function () {
$http.get('/api/budgetapi/categories').then(response => {
@@ -73,6 +70,7 @@
}).catch(error => console.error("Error fetching categories:", error));
};
// Automatically filter when month or year changes
$scope.filterBudget = function () {
$http.get('/api/budgetapi/items', {
params: { month: $scope.selectedMonth, year: $scope.selectedYear }
@@ -108,8 +106,62 @@
$scope.budgetItems.push(newItem);
};
// Dropdown toggle functions
$scope.isMonthDropdownVisible = false;
$scope.isYearDropdownVisible = false;
$scope.toggleMonthDropdown = function () {
$scope.isMonthDropdownVisible = !$scope.isMonthDropdownVisible;
$scope.isYearDropdownVisible = false;
};
$scope.toggleYearDropdown = function () {
$scope.isYearDropdownVisible = !$scope.isYearDropdownVisible;
$scope.isMonthDropdownVisible = false;
};
$scope.selectMonth = function (month) {
$scope.selectedMonth = $scope.months.indexOf(month) + 1;
$scope.isMonthDropdownVisible = false;
$scope.filterBudget(); // Automatically filter after month selection
};
$scope.selectYear = function (year) {
$scope.selectedYear = year;
$scope.isYearDropdownVisible = false;
$scope.filterBudget(); // Automatically filter after year selection
};
$scope.selectedMonthName = function () {
return $scope.months[$scope.selectedMonth - 1];
};
// Initial load
$scope.loadCategories();
$scope.filterBudget();
// Month and year options as before
$scope.monthOptions = {
1: "January", 2: "February", 3: "March", 4: "April", 5: "May", 6: "June",
7: "July", 8: "August", 9: "September", 10: "October", 11: "November", 12: "December"
};
// Toggle display of date picker
$scope.toggleDatePicker = function ($event) {
$event.stopPropagation(); // Prevents the click from closing the dropdown
$scope.isDatePickerVisible = !$scope.isDatePickerVisible;
};
// Update selection and close dropdown
$scope.updateMonthYear = function () {
$scope.isDatePickerVisible = false; // Close after selection
$scope.filterBudget(); // Trigger filtering immediately
};
// Close the date picker when clicking outside
document.addEventListener('click', function () {
$scope.$apply(() => $scope.isDatePickerVisible = false);
});
});
</script>

View File

@@ -0,0 +1,98 @@
@model Aberwyn.Models.MenuViewModel
@{
Layout = "_Layout";
}
<!DOCTYPE html>
<html lang="en" ng-app="mealMenuApp">
<head>
<meta charset="utf-8">
<title>Meal Menu Overview</title>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.8.2/angular.min.js"></script>
<script src="~/js/menu.js"></script>
<link rel="stylesheet" type="text/css" href="~/css/meal-menu.css">
</head>
<body ng-controller="MealMenuController">
<div class="meal-menu-page">
<h1 class="page-title">Meal Menu Overview</h1>
<div class="date-picker">
<button type="button" class="date-btn" ng-click="goToPreviousWeek()">Previous Week</button>
<span class="week-info">Week {{ selectedWeek }} - {{ selectedYear }}</span>
<button type="button" class="date-btn" ng-click="goToNextWeek()">Next Week</button>
</div>
<div class="mode-toggle">
<button ng-click="toggleEditMode()" ng-show="!isEditing" class="mode-btn">Edit</button>
<button ng-click="toggleEditMode()" ng-show="isEditing" class="mode-btn">View</button>
</div>
<div class="meal-menu-container">
<div ng-repeat="day in daysOfWeek" class="day-item">
<div class="day-header">{{ day }}</div>
<div class="meal-info" ng-if="!isEditing">
<div ng-if="menu[day]">
<div ng-if="menu[day].breakfastMealName">
<span><strong>Breakfast:</strong> {{ menu[day].breakfastMealName }}</span>
</div>
<div ng-if="menu[day].lunchMealName">
<span><strong>Lunch:</strong> {{ menu[day].lunchMealName }}</span>
</div>
<div ng-if="menu[day].dinnerMealName">
<span><strong>Dinner:</strong> {{ menu[day].dinnerMealName }}</span>
</div>
</div>
<div ng-if="!menu[day]">
<span class="not-assigned">Not Assigned</span>
</div>
</div>
<div class="meal-edit" ng-if="isEditing">
<div>
<strong>Breakfast:</strong>
<select ng-model="menu[day].breakfastMealId" ng-change="handleMealSelection(day, 'breakfast')"
ng-options="meal.id as meal.name for meal in meals">
<option value="">Select Meal</option>
<option value="new">New Meal</option>
</select>
</div>
<div>
<strong>Lunch:</strong>
<select ng-model="menu[day].lunchMealId" ng-change="handleMealSelection(day, 'lunch')"
ng-options="meal.id as meal.name for meal in meals">
<option value="">Select Meal</option>
<option value="new">New Meal</option>
</select>
</div>
<div>
<strong>Dinner:</strong>
<select ng-model="menu[day].dinnerMealId" ng-change="handleMealSelection(day, 'dinner')"
ng-options="meal.id as meal.name for meal in meals">
<option value="">Select Meal</option>
<option value="new">New Meal</option>
</select>
</div>
</div>
</div>
<button ng-if="isEditing" ng-click="saveMenu()" class="save-btn">Save Menu</button>
</div>
<div class="new-meal-form" ng-if="isEditing">
<h3>Add a New Meal</h3>
<input type="text" placeholder="Meal Name" ng-model="newMeal.name">
<input type="text" placeholder="Description" ng-model="newMeal.description">
<input type="text" placeholder="Protein Type" ng-model="newMeal.proteinType">
<input type="text" placeholder="Carb Type" ng-model="newMeal.carbType">
<input type="text" placeholder="Recipe URL" ng-model="newMeal.recipeUrl">
<button ng-click="saveNewMeal()">Save Meal</button>
<button ng-click="cancelNewMeal()">Cancel</button>
</div>
</div>
</body>
</html>

View File

@@ -4,14 +4,28 @@
<h2>Real Estate Website Downloader</h2>
<form method="post" ng-submit="downloadWebsite()">
<label for="urlInput">Enter Website URL:</label>
<input type="text" id="urlInput" ng-model="websiteUrl" placeholder="https://example.com" required />
<button type="submit">Download</button>
</form>
<div ng-app="realEstateApp" ng-controller="RealEstateController">
<form ng-submit="fetchImages()">
<label for="urlInput">Enter Website URL:</label>
<input type="text" id="urlInput" ng-model="websiteUrl" placeholder="https://example.com" required />
<button type="submit">Fetch Images</button>
</form>
<div ng-if="downloadStatus">
<p>{{ downloadStatus }}</p>
<div ng-if="fetchStatus">
<p>{{ fetchStatus }}</p>
</div>
<!-- Display fetched images before download -->
<div ng-if="images.length > 0">
<h3>Fetched Images:</h3>
<div class="image-container">
<div ng-repeat="imgSrc in images" class="image-preview">
<img ng-src="{{ imgSrc }}" alt="Preview Image" />
<p>{{ imgSrc }}</p>
<button ng-click="downloadImage(imgSrc)">Download</button>
</div>
</div>
</div>
</div>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.8.2/angular.min.js"></script>
@@ -19,20 +33,63 @@
angular.module('realEstateApp', [])
.controller('RealEstateController', function ($scope, $http) {
$scope.websiteUrl = '';
$scope.downloadStatus = '';
$scope.fetchStatus = '';
$scope.images = [];
$scope.downloadWebsite = function () {
// Fetch images from the backend
$scope.fetchImages = function () {
if ($scope.websiteUrl) {
$http.post('/api/realestate/download', { url: $scope.websiteUrl })
.then(function (response) {
$scope.downloadStatus = 'Download successful!';
console.log(response.data); // Optional: Handle the downloaded data
$scope.fetchStatus = 'Images fetched successfully!';
$scope.images = response.data.images; // Display the fetched images
})
.catch(function (error) {
$scope.downloadStatus = 'Error downloading website.';
$scope.fetchStatus = 'Error fetching images.';
console.error(error);
});
}
};
// Function to download the image when clicked
$scope.downloadImage = function (imgSrc) {
var link = document.createElement('a');
link.href = imgSrc;
link.download = imgSrc.split('/').pop(); // Extract filename from URL
link.click();
};
});
</script>
<style>
.image-container {
display: flex;
flex-wrap: wrap;
margin-top: 20px;
}
.image-preview {
margin: 10px;
text-align: center;
}
.image-preview img {
width: 150px;
height: 150px;
object-fit: cover;
border: 1px solid #ccc;
margin-bottom: 10px;
}
.image-preview button {
background-color: #4CAF50;
color: white;
border: none;
padding: 5px 10px;
cursor: pointer;
}
.image-preview button:hover {
background-color: #45a049;
}
</style>

View File

@@ -0,0 +1,116 @@
@model IEnumerable<Aberwyn.Models.BudgetItem>
<aside class="right-sidebar">
<!-- Loading Spinner -->
<div id="loadingSpinner" style="display:none;">Loading...</div>
<!-- Error Message Placeholder -->
<div id="errorMessage" style="color:red; display:none;"></div>
<!-- Budget Section -->
<div class="sidebar-section" id="budgetItemsContainer">
<h3 class="sidebar-header" onclick="toggleBudgetSection()">
<span>Budget this month:</span>
<span class="total-budget-amount">0</span>
</h3>
<div class="sidebar-container">
<p>Loading budget items...</p>
</div>
</div>
</aside>
<script>
document.addEventListener("DOMContentLoaded", function () {
const loadingSpinner = document.getElementById("loadingSpinner");
const errorMessage = document.getElementById("errorMessage");
const budgetItemsContainer = document.getElementById("budgetItemsContainer");
const totalBudgetAmount = budgetItemsContainer.querySelector(".total-budget-amount");
const sidebarContent = budgetItemsContainer.querySelector(".sidebar-container");
// Show loading spinner
loadingSpinner.style.display = "block";
errorMessage.style.display = "none"; // Hide error message initially
// Fetch current month and year
const currentDate = new Date();
const month = currentDate.getMonth() + 1; // getMonth() returns 0-11, hence +1
const year = currentDate.getFullYear();
// Fetch data using AJAX
const fetchUrl = `/api/budgetapi/items?month=${month}&year=${year}`;
fetch(fetchUrl)
.then(response => {
if (!response.ok) {
return response.json().then(err => { throw new Error(err.message); });
}
return response.json();
})
.then(data => {
loadingSpinner.style.display = "none"; // Hide loading spinner
if (data.length > 0) {
let totalAmount = 0;
sidebarContent.innerHTML = ''; // Clear existing items
// Group and display items
const groupedItems = data.reduce((acc, item) => {
acc[item.category] = acc[item.category] || [];
acc[item.category].push(item);
return acc;
}, {});
// Create accordion for each category
for (const [category, items] of Object.entries(groupedItems)) {
const amount = items.reduce((sum, item) => sum + item.amount, 0);
totalAmount += amount; // Sum total amounts
// Create accordion element
const itemElement = `
<div class="accordion-item">
<div class="accordion-header" onclick="toggleAccordion(this)">
<span class="accordion-title">${category}</span>
<span class="item-amount">(${amount.toFixed(2)})</span>
<i class="fas fa-chevron-down"></i>
</div>
<div class="accordion-content">
${items.map(item => `<div class="item-name">${item.name}: <span class="item-amount">${item.amount.toFixed(2)}</span></div>`).join('')}
</div>
</div>`;
sidebarContent.insertAdjacentHTML('beforeend', itemElement);
}
// Update total amount
totalBudgetAmount.textContent = totalAmount.toFixed(2); // Format total amount
} else {
sidebarContent.innerHTML = "<p>No categories available.</p>";
}
})
.catch(error => {
console.error("Error fetching data:", error);
errorMessage.textContent = error.message; // Display error message
errorMessage.style.display = "block"; // Show error message
});
});
// Function to toggle accordion visibility
function toggleAccordion(header) {
const content = header.nextElementSibling;
const allContents = document.querySelectorAll(".accordion-content");
// Close all other accordion items
allContents.forEach(item => {
if (item !== content) {
item.style.display = "none";
}
});
// Toggle current accordion item
content.style.display = content.style.display === "block" ? "none" : "block";
}
// Function to toggle the budget section visibility
function toggleBudgetSection() {
const sidebarContainer = document.querySelector(".sidebar-container");
sidebarContainer.classList.toggle('collapsed'); // Add 'collapsed' class to toggle visibility
}
</script>

View File

@@ -1,14 +1,11 @@
@using Aberwyn.Models
@model Aberwyn.Models.BudgetModel
<!DOCTYPE html>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css">
<link rel="stylesheet" href="~/css/site.css" asp-append-version="true" />
<title>Your Page Title</title>
<title>LEWEL - Dashboard</title>
</head>
<header class="header">
<div class="header-container">
@@ -22,9 +19,8 @@
</div>
</header>
<body>
<div class="grid-container">
<aside class="left-sidebar"> <!-- Left sidebar -->
<aside class="left-sidebar">
<ul class="navbar-nav">
<li class="nav-item">
<a class="nav-link" asp-area="" asp-controller="Home" asp-action="Index">
@@ -41,53 +37,25 @@
<i class="fas fa-building"></i> RealEstate
</a>
</li>
<li class="nav-item">
<a class="nav-link" asp-area="" asp-controller="Home" asp-action="Menu">
<i class="fas fa-utensils"></i> Menu
</a>
</li>
</ul>
</aside>
<main role="main" class="main-content"> <!-- Main content area -->
@RenderBody() <!-- This is where the content of each page will be rendered -->
<main role="main" class="main-content">
@RenderBody()
</main>
<aside class="right-sidebar"> <!-- Updated Right Sidebar -->
@if (Model != null && Model.BudgetItems != null)
{
<h3 class="sidebar-budget-header">Budget this month: <span class="total-budget-amount">@Model.BudgetItems.Sum(item => item.Amount)</span></h3>
<div class="sidebar-budget-container"> <!-- New Container -->
@if (Model.BudgetItems.Any())
{
foreach (var categoryGroup in Model.BudgetItems.GroupBy(item => item.Category))
{
<div class="sidebar-budget-item">
<span class="budget-item-name">@categoryGroup.Key</span>
<span class="budget-item-amount">@categoryGroup.Sum(item => item.Amount)</span>
</div>
}
}
else
{
<p>No categories available.</p>
}
</div>
}
else
{
<h3>No budget data available.</h3>
}
<!-- Right Sidebar with budget data from the RightSidebarViewComponent -->
<aside class="right-sidebar">
@await Component.InvokeAsync("RightSidebar")
</aside>
</div>
<script>
document.addEventListener("DOMContentLoaded", function () {
const budgetHeader = document.querySelector('.sidebar-budget-header');
const budgetContainer = document.querySelector('.sidebar-budget-container');
budgetHeader.addEventListener('click', function () {
budgetContainer.style.display = budgetContainer.style.display === 'none' ? 'block' : 'none';
});
// Initially hide the budget items container
budgetContainer.style.display = 'none';
});
</script>
<script src="~/js/site.js" asp-append-version="true"></script>
@await RenderSectionAsync("Scripts", required: false)
</body>
</html>

View File

@@ -7,6 +7,7 @@
},
"AllowedHosts": "*",
"ConnectionStrings": {
"DefaultConnection": "Server=192.168.1.108;Database=Nevyn;Uid=root;Pwd=3edc4RFV;"
"DefaultConnection": "Server=192.168.1.108;Database=Nevyn;Uid=root;Pwd=3edc4RFV;",
"ProductionConnection": "Server=172.18.0.2;Database=Nevyn;Uid=root;Pwd=3edc4RFV;"
}
}

View File

@@ -0,0 +1,81 @@
/* Budget Container */
.budget-container {
display: flex;
flex-wrap: wrap;
gap: 10px; /* Reduced gap for tighter layout */
padding: 0; /* Removed padding */
margin: 0; /* Removed margin */
}
/* Category Block */
.category-block {
flex: 1 1 300px;
max-width: 230px;
min-width: 180px;
margin: 0; /* Removed margin */
}
.category-header {
display: flex;
justify-content: space-between; /* Aligns the category name and total */
align-items: center; /* Center vertically */
padding: 5px; /* Retain padding */
background-color: #4A6572; /* Muted blue-gray */
color: #ffffff;
border-radius: 8px;
margin-bottom: 10px;
font-weight: bold;
max-width: 100%; /* Ensures the header doesn't exceed the width of its container */
box-sizing: border-box; /* Include padding in width calculation */
}
.total {
font-weight: bold;
color: #ffffff; /* White for visibility */
background-color: rgba(74, 101, 114, 0.7); /* Semi-transparent muted blue-gray */
padding: 5px 10px; /* Add some padding for spacing */
border-radius: 5px; /* Rounded corners */
}
/* Items Layout */
.items-wrapper {
display: flex;
min-width: 200px;
flex-direction: column;
padding: 0; /* Removed padding */
margin: 0; /* Removed margin */
}
/* Item Styles */
.item {
display: flex;
align-items: center;
margin-bottom: 5px; /* Space between items */
gap: 5px; /* Reduced gap between item components */
}
/* Input Styles */
.item-name input {
flex: 1; /* Compact size for amount field */
max-width: 20ch; /* Allow up to 7 characters */
padding: 6px; /* Slightly reduced padding */
border: none;
border-radius: 5px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
background-color: #f9f9f9;
transition: background-color 0.3s;
}
.item-amount input {
flex: 1; /* Compact size for amount field */
max-width: 7ch; /* Allow up to 7 characters */
padding: 6px; /* Slightly reduced padding */
border: none;
border-radius: 5px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
background-color: #f9f9f9;
transition: background-color 0.3s;
}
/* Focus Effect */
.item-name input:focus

View File

@@ -0,0 +1,210 @@
/* Meal Menu Page Styles */
.meal-menu-page {
background-color: #2A3A48; /* Dark background to match theme */
padding: 30px 60px; /* Add more padding to the sides */
border-radius: 12px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
max-width: 900px; /* Adjust width to provide more space */
margin: 0 auto;
color: #B0BEC5; /* Light gray text */
}
.meal-menu-page h1 {
font-size: 2rem;
text-align: center;
color: #FFFFFF;
margin-bottom: 20px;
}
/* Date Picker */
.date-picker {
display: flex;
justify-content: center;
align-items: center;
margin-bottom: 30px;
gap: 20px;
}
.date-picker span {
font-size: 1.2rem;
color: #B0BEC5;
}
.date-picker button {
background-color: #4A6572;
color: #ffffff;
padding: 12px 25px;
border: none;
border-radius: 8px;
cursor: pointer;
font-size: 1rem;
transition: background-color 0.3s;
}
.date-picker button:hover {
background-color: #3A4E62;
}
/* Mode Toggle */
.mode-toggle {
text-align: center;
margin-bottom: 30px;
}
.mode-toggle button {
background-color: #4CAF50;
color: #ffffff;
padding: 12px 20px;
border: none;
border-radius: 8px;
cursor: pointer;
font-size: 1rem;
transition: background-color 0.3s;
}
.mode-toggle button:hover {
background-color: #45a049;
}
/* Meal Menu Container - Vertical Layout */
.meal-menu-container {
display: flex;
flex-direction: column; /* Align the days vertically */
gap: 20px;
justify-content: flex-start;
}
/* Day Block Styling */
.day-block {
background-color: #1F2C3C; /* Darker background for day blocks */
padding: 20px;
border-radius: 10px;
width: 100%;
text-align: center;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
/* Day Header with Line */
.day-header {
font-size: 1.5rem;
font-weight: bold;
margin-bottom: 15px;
color: #FFEB3B; /* Brighter yellow for the text */
position: relative; /* Allows absolute positioning for the line */
}
.day-header::after {
content: "";
position: absolute;
bottom: 0; /* Place it at the bottom of the header */
left: 0;
width: 100%; /* Make the line span across the header */
height: 2px; /* Thickness of the line */
background-color: #FFEB3B; /* Yellow line for contrast */
border-radius: 2px; /* Rounded corners for the line */
}
/* Meal Selection */
.meal-selection {
font-size: 1rem;
color: #B0BEC5;
margin-bottom: 10px;
}
.meal-selection strong {
color: #FFFFFF;
}
.meal-selection span {
color: #FFEB3B; /* Highlight the meal names */
}
/* Edit Mode Styling */
.edit-mode select {
background-color: #444;
color: #fff;
border: none;
padding: 10px;
border-radius: 6px;
width: 100%;
font-size: 1rem;
margin-top: 5px;
cursor: pointer;
}
.edit-mode select:focus {
outline: none;
box-shadow: 0 0 8px rgba(66, 133, 244, 0.8);
}
/* Save Button */
.save-menu-button {
display: block;
background-color: #FF9800;
color: #fff;
padding: 12px 25px;
border: none;
border-radius: 8px;
cursor: pointer;
font-size: 1rem;
width: 200px;
margin: 30px auto;
text-align: center;
transition: background-color 0.3s;
}
.save-menu-button:hover {
background-color: #F57C00;
}
/* New Meal Form Styling */
.new-meal-form {
display: flex;
flex-direction: column; /* Stack input fields vertically */
gap: 15px; /* Space between fields */
padding: 20px;
background-color: #1F2C3C; /* Dark background to match theme */
border-radius: 10px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.2);
max-width: 400px; /* Control the width of the form */
margin: 0 auto;
}
/* Input Fields */
.new-meal-form input,
.new-meal-form select,
.new-meal-form textarea {
background-color: #444; /* Dark background for inputs */
color: #fff; /* White text */
border: none;
padding: 12px 15px;
border-radius: 6px;
font-size: 1rem;
width: 100%; /* Ensure inputs take full width */
}
/* Focused Input Fields */
.new-meal-form input:focus,
.new-meal-form select:focus,
.new-meal-form textarea:focus {
outline: none;
box-shadow: 0 0 8px rgba(66, 133, 244, 0.8); /* Blue focus highlight */
}
/* Submit Button */
.new-meal-form button {
background-color: #FF9800; /* Button color */
color: #fff;
padding: 12px 20px;
border: none;
border-radius: 8px;
cursor: pointer;
font-size: 1rem;
transition: background-color 0.3s;
width: 100%; /* Button takes full width */
}
/* Button Hover */
.new-meal-form button:hover {
background-color: #F57C00;
}

View File

@@ -1,7 +1,7 @@
/* Base Styles */
body {
background-color: #1F2C3C;
color: #4b004b;
background-color: #1F2C3C; /* Dark background */
color: #B0BEC5; /* Light Gray text for better readability */
font-family: 'Roboto', sans-serif;
margin: 0;
padding: 20px; /* Keep padding for general layout */
@@ -10,7 +10,7 @@ body {
/* Container for Centering and Max Width */
.budget-page {
width: 100%; /* Make the page full width */
max-width: 1900px; /* You can adjust or remove this */
max-width: 1900px; /* Adjust or remove this */
margin: 0 auto; /* Centering */
padding: 0; /* Removed extra padding for budget page */
}
@@ -25,7 +25,7 @@ body {
}
.left-sidebar, .sidebar-budget-right {
background-color: #f4f4f4;
background-color: #f9f9f9; /* Softer background */
border-radius: 12px;
padding: 10px; /* Adjusted padding for sidebars */
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
@@ -33,13 +33,13 @@ body {
.main-content {
padding: 10px;
background-color: #f4f4f4; /* Light gray instead of white */
background-color: #f4f4f4; /* Light gray */
border-radius: 12px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
}
.nav-link {
color: #4b004b;
color: #3A4E62; /* Muted blue-gray for consistency */
}
.nav-link:hover {
@@ -52,114 +52,6 @@ body {
}
}
/* Budget Container */
.budget-container {
display: flex;
flex-wrap: wrap;
gap: 10px; /* Reduced gap for tighter layout */
padding: 0; /* Removed padding */
margin: 0; /* Removed margin */
}
/* Category Block */
.category-block {
flex: 1 1 300px;
max-width: 230px;
min-width: 180px;
margin: 0; /* Removed margin */
}
.category-header {
display: flex;
justify-content: space-between; /* Aligns the category name and total */
align-items: center; /* Center vertically */
padding: 5px; /* Retain padding */
background-color: #6a0dad; /* Adjust color as needed */
color: #ffffff;
border-radius: 8px;
margin-bottom: 10px;
font-weight: bold;
max-width: 100%; /* Ensures the header doesn't exceed the width of its container */
box-sizing: border-box; /* Include padding in width calculation */
}
.total {
font-weight: bold;
color: #ffffff; /* Keep white for visibility */
background-color: rgba(106, 13, 173, 0.7); /* Add a semi-transparent background for contrast */
padding: 5px 10px; /* Add some padding for spacing */
border-radius: 5px; /* Rounded corners */
}
/* Items Layout */
.items-wrapper {
display: flex;
min-width: 200px;
flex-direction: column;
padding: 0; /* Removed padding */
margin: 0; /* Removed margin */
}
/* Item Styles */
.item {
display: flex;
align-items: center;
margin-bottom: 5px; /* Space between items */
gap: 5px; /* Reduced gap between item components */
}
/* Input Styles */
.item-name input {
flex: 1; /* Compact size for amount field */
max-width: 20ch; /* Allow up to 7 characters */
padding: 6px; /* Slightly reduced padding */
border: none;
border-radius: 5px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
background-color: #f9f9f9;
transition: background-color 0.3s;
}
.item-amount input {
flex: 1; /* Compact size for amount field */
max-width: 7ch; /* Allow up to 7 characters */
padding: 6px; /* Slightly reduced padding */
border: none;
border-radius: 5px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
background-color: #f9f9f9;
transition: background-color 0.3s;
}
/* Focus Effect */
.item-name input:focus,
.item-amount input:focus {
background-color: #e6e6e6;
}
/* Add Item Button */
.add-item {
display: flex; /* Use flex to center the "+" */
align-items: center; /* Center the "+" vertically */
justify-content: center; /* Center the "+" horizontally */
height: 25px; /* Adjusted height to match input fields */
cursor: pointer;
color: #ffffff; /* White text color for contrast */
background-color: #6a0dad; /* Use the same color as the header for consistency */
margin-top: 5px; /* Reduced space above the button */
border: none; /* No border */
border-radius: 5px; /* Rounded corners */
font-size: 15px; /* Adjusted font size for better fit */
transition: background-color 0.3s, transform 0.3s; /* Transition for hover effect */
}
/* Hover Effect for Add Item Button */
.add-item:hover {
background-color: #5a0c9a; /* Darker shade on hover */
transform: scale(1.05); /* Slightly enlarge on hover for emphasis */
}
.header-container {
display: flex;
justify-content: flex-start; /* Aligns the header to the left */
@@ -178,7 +70,7 @@ body {
}
.name {
color: #FFFFFF; /* White for names */
color: #ffffff; /* White for names */
font-size: 0.4em; /* Adjust size as needed */
}
@@ -214,7 +106,7 @@ body {
.nav-link {
text-decoration: none; /* Remove underline */
color: #000; /* Link color */
color: #3A4E62; /* Muted blue-gray for consistency */
display: flex; /* Use flexbox to align icon and text */
align-items: center; /* Center items vertically */
}
@@ -224,34 +116,111 @@ body {
font-size: 1.2em; /* Adjust icon size */
}
/* Sidebar Right Styles */
.right-sidebar {
background-color: #f8f9fa; /* Use your existing background color */
padding: 10px; /* Reduce padding for compactness */
margin: 0; /* Remove any margins */
border-radius: 8px; /* Optional: Add rounded corners */
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); /* Optional: Add shadow for depth */
.sidebar-section {
margin-bottom: 5px;
}
.sidebar-budget-header {
cursor: pointer; /* Indicate clickable header */
font-weight: bold; /* Make header bold */
margin: 0; /* Remove default margin */
padding-bottom: 5px; /* Space below header */
}
.sidebar-budget-container {
margin-top: 10px; /* Space above budget items */
}
.sidebar-budget-item {
.sidebar-header {
cursor: pointer;
background-color: #4CAF50; /* Green */
color: white;
padding: 8px; /* Slightly more padding */
border-radius: 5px;
display: flex;
justify-content: space-between; /* Space out name and amount */
padding: 5px 0; /* Reduced padding for compactness */
border-bottom: 1px solid #e0e0e0; /* Optional: Divider line */
justify-content: space-between;
align-items: center; /* Center items vertically */
font-size: 14px;
transition: background-color 0.3s; /* Smooth hover transition */
}
.sidebar-budget-item:last-child {
border-bottom: none; /* Remove border from last item */
}
.sidebar-header:hover {
background-color: #45a049; /* Darker green on hover */
}
.accordion-item {
border-top: 1px solid #ddd; /* Separator between items */
}
.accordion-header {
display: flex; /* Flexbox for header */
justify-content: space-between;
align-items: center; /* Center vertically */
padding: 8px; /* Padding for accordion header */
cursor: pointer;
background-color: #f1f1f1; /* Light background for header */
border-radius: 4px;
transition: background-color 0.3s; /* Smooth hover transition */
}
.accordion-header:hover {
background-color: #e0e0e0; /* Slightly darker background on hover */
}
.accordion-content {
display: none; /* Hidden by default */
background-color: #fff; /* White background for content */
padding: 5px; /* Padding for content */
border-radius: 4px; /* Rounded corners */
}
.item-name {
font-weight: normal; /* Normal weight for item names */
font-size: 12px; /* Smaller font size */
}
.item-amount {
color: #333;
font-size: 12px; /* Smaller font size for amounts */
}
.date-picker {
position: relative;
}
.date-picker-toggle {
background-color: #4A6572; /* Muted blue-gray */
color: #ffffff;
padding: 10px 15px;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 16px;
transition: background-color 0.3s;
}
.date-picker-toggle:hover {
background-color: #3A4E62; /* Darker blue-gray on hover */
}
.date-picker-dropdown {
position: absolute;
top: 100%;
left: 0;
background-color: #333;
color: #ffffff;
padding: 10px;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
margin-top: 8px;
z-index: 1000;
display: flex;
gap: 10px;
width: max-content;
}
.date-picker-dropdown select {
background-color: #444;
color: #ffffff;
border: none;
padding: 8px;
border-radius: 4px;
font-size: 14px;
appearance: none;
cursor: pointer;
transition: background-color 0.3s;
}
.date-picker-dropdown select:hover {
background-color: #555;
}

117
Aberwyn/wwwroot/js/menu.js Normal file
View File

@@ -0,0 +1,117 @@
angular.module('mealMenuApp', [])
.controller('MealMenuController', function ($scope, $http) {
$scope.isEditing = false;
$scope.toggleEditMode = function () {
$scope.isEditing = !$scope.isEditing;
};
$scope.meals = [];
$scope.menu = {};
$scope.showNewMealForm = false;
$scope.newMeal = {};
$scope.selectedMealDay = null;
$scope.selectedMealType = null;
const today = new Date();
$scope.selectedWeek = getWeek(today);
$scope.selectedYear = today.getFullYear();
$scope.daysOfWeek = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"];
$scope.loadMeals = function () {
$http.get('/api/mealMenuApi/getMeals')
.then(response => {
$scope.meals = response.data;
})
.catch(error => console.error("Error fetching meals:", error));
};
$scope.loadMenu = function () {
$http.get('/api/mealMenuApi/menu', {
params: { weekNumber: $scope.selectedWeek, year: $scope.selectedYear }
}).then(response => {
$scope.menu = {};
response.data.forEach(item => {
const dayOfWeek = $scope.daysOfWeek[item.dayOfWeek - 1];
if (!$scope.menu[dayOfWeek]) $scope.menu[dayOfWeek] = {};
if (item.breakfastMealId) {
$scope.menu[dayOfWeek].breakfastMealId = item.breakfastMealId;
$scope.menu[dayOfWeek].breakfastMealName = getMealNameById(item.breakfastMealId);
}
if (item.lunchMealId) {
$scope.menu[dayOfWeek].lunchMealId = item.lunchMealId;
$scope.menu[dayOfWeek].lunchMealName = getMealNameById(item.lunchMealId);
}
if (item.dinnerMealId) {
$scope.menu[dayOfWeek].dinnerMealId = item.dinnerMealId;
$scope.menu[dayOfWeek].dinnerMealName = getMealNameById(item.dinnerMealId);
}
});
}).catch(error => console.error("Error fetching weekly menu:", error));
};
function getMealNameById(mealId) {
const meal = $scope.meals.find(m => m.id === mealId);
return meal ? meal.name : "Unknown Meal";
}
$scope.handleMealSelection = function (day, mealType) {
if ($scope.menu[day][mealType + "MealId"] === "new") {
$scope.showNewMealForm = true;
$scope.newMeal = { name: "", description: "", proteinType: "", carbType: "", recipeUrl: "" };
$scope.selectedMealDay = day;
$scope.selectedMealType = mealType;
}
};
$scope.saveNewMeal = function () {
if (!$scope.newMeal.name) {
alert("Meal name is required");
return;
}
$http.post('/api/mealMenuApi/addMeal', $scope.newMeal)
.then(response => {
const addedMeal = response.data;
$scope.meals.push(addedMeal);
$scope.menu[$scope.selectedMealDay][$scope.selectedMealType + "MealId"] = addedMeal.id;
$scope.showNewMealForm = false;
$scope.newMeal = {}; // Reset new meal data after save
})
.catch(error => console.error("Error saving new meal:", error));
};
$scope.cancelNewMeal = function () {
$scope.showNewMealForm = false;
$scope.newMeal = {}; // Reset new meal data when canceled
};
function getWeek(date) {
const day = date.getDay() || 7;
date.setDate(date.getDate() + 4 - day);
const yearStart = new Date(date.getFullYear(), 0, 1);
return Math.ceil(((date - yearStart) / 86400000 + 1) / 7);
}
$scope.goToPreviousWeek = function () {
if ($scope.selectedWeek === 1) {
$scope.selectedYear--;
$scope.selectedWeek = 52;
} else {
$scope.selectedWeek--;
}
$scope.loadMenu();
};
$scope.goToNextWeek = function () {
if ($scope.selectedWeek === 52) {
$scope.selectedYear++;
$scope.selectedWeek = 1;
} else {
$scope.selectedWeek++;
}
$scope.loadMenu();
};
$scope.loadMeals();
$scope.loadMenu();
});