Mostly css changes and welcome page thumbnails
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
Elias Jansson
2025-06-13 15:46:57 +02:00
parent 6a43435950
commit fb62f076a0
8 changed files with 285 additions and 47 deletions

View File

@@ -3,13 +3,23 @@ using Microsoft.AspNetCore.Mvc;
namespace Aberwyn.Controllers
{
[Authorize(Roles = "Budget")]
public class BudgetController : Controller
{
[Authorize(Roles = "Budget")]
public IActionResult Index()
[Route("budget/{year:int}/{month:int}")]
public IActionResult Index(int year, int month)
{
ViewData["HideSidebar"] = true;
ViewBag.Year = year;
ViewBag.Month = month;
return View();
}
// För fallback när ingen månad/år anges
[Route("budget")]
public IActionResult Index()
{
var now = DateTime.Now;
return RedirectToAction("Index", new { year = now.Year, month = now.Month });
}
}
}
}

View File

@@ -344,7 +344,6 @@ public List<WeeklyMenu> GetWeeklyMenu(int weekNumber, int year)
int week = ISOWeek.GetWeekOfYear(date);
int year = date.Year;
// Gör om till ISO 8601: Måndag = 1, Söndag = 7
int dayOfWeek = (int)date.DayOfWeek;
if (dayOfWeek == 0) dayOfWeek = 7;
@@ -361,20 +360,30 @@ public List<WeeklyMenu> GetWeeklyMenu(int weekNumber, int year)
var allMeals = _context.Meals
.Where(m => mealIds.Contains(m.Id))
.ToDictionary(m => m.Id, m => m.Name);
.ToDictionary(m => m.Id);
if (menu.BreakfastMealId.HasValue && allMeals.TryGetValue(menu.BreakfastMealId.Value, out var breakfast))
menu.BreakfastMealName = breakfast;
if (menu.LunchMealId.HasValue && allMeals.TryGetValue(menu.LunchMealId.Value, out var lunch))
menu.LunchMealName = lunch;
if (menu.DinnerMealId.HasValue && allMeals.TryGetValue(menu.DinnerMealId.Value, out var dinner))
menu.DinnerMealName = dinner;
if (menu.BreakfastMealId is int bId && allMeals.TryGetValue(bId, out var breakfast))
{
menu.BreakfastMealName = breakfast.Name;
menu.BreakfastThumbnail = breakfast.ThumbnailData;
}
if (menu.LunchMealId is int lId && allMeals.TryGetValue(lId, out var lunch))
{
menu.LunchMealName = lunch.Name;
menu.LunchThumbnail = lunch.ThumbnailData;
}
if (menu.DinnerMealId is int dId && allMeals.TryGetValue(dId, out var dinner))
{
menu.DinnerMealName = dinner.Name;
menu.DinnerThumbnail = dinner.ThumbnailData;
}
}
return menu;
}
public List<WeeklyMenu> GetMenuEntriesByDateRange(DateTime startDate, DateTime endDate)
{
var results = new List<WeeklyMenu>();

View File

@@ -43,6 +43,9 @@ public class WeeklyMenu
public int WeekNumber { get; set; }
public int Year { get; set; }
public DateTime CreatedAt { get; set; }
[NotMapped] public byte[]? BreakfastThumbnail { get; set; }
[NotMapped] public byte[]? LunchThumbnail { get; set; }
[NotMapped] public byte[]? DinnerThumbnail { get; set; }
[NotMapped] public string? BreakfastMealName { get; set; }
[NotMapped] public string? LunchMealName { get; set; }

View File

@@ -4,6 +4,7 @@
@{
ViewData["Title"] = "Budget";
}
<div ng-app="budgetApp" ng-controller="BudgetController" class="budget-page" ng-if="!loading">
<div class="budget-header" style="display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 16px;">
<div class="month-nav-bar" style="display: flex; align-items: center; gap: 10px; position: relative;">
@@ -93,7 +94,7 @@
on-category-drop="handleCategoryDrop(data, targetCategory)">
<div class="card-header" style="background-color: {{cat.color}};">
<div class="header-left" ng-if="!cat.editing">{{ cat.name }}</div>
<div class="header-left" ng-if="!cat.editing" >{{ cat.name }}</div>
<input class="header-edit" type="text" ng-model="cat.name" ng-if="cat.editing" />
<input class="color-edit" type="color" ng-model="cat.color" ng-if="cat.editing" />
<div class="header-actions">
@@ -129,11 +130,10 @@
ng-show="cat.editing"
style="opacity: 0.5; padding-right: 6px; cursor: grab;"></i>
<input type="text" ng-model="item.name" ng-if="cat.editing" />
<span ng-if="!cat.editing" title="{{ item.definitionName }}">{{ item.name }}</span>
<!-- <span ng-if="!cat.editing">#{{ item.definitionName }}</span>-->
<input type="number" ng-model="item.amount" ng-if="cat.editing" />
<span class="amount" ng-if="!cat.editing">{{ item.amount | number:0 }}</span>
<input type="text" class="item-label" ng-model="item.name" ng-if="cat.editing" />
<span ng-if="!cat.editing" class="item-label" title="{{ item.name }}">{{ item.name }}</span>
<input type="number" ng-model="item.amount" ng-if="cat.editing" />
<span class="amount" ng-if="!cat.editing">{{ item.amount | number:0 }}</span>
<!-- 3-pricksmeny -->
<div class="item-menu-container" ng-if="cat.editing" style="position: relative;">
@@ -231,5 +231,9 @@
<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>
window.initialYear = @ViewBag.Year;
window.initialMonth = @ViewBag.Month;
</script>
<script src="~/js/budget.js"></script>
<script src="~/js/budget-dragdrop.js"></script>

View File

@@ -18,27 +18,87 @@
@if (Model != null)
{
<div class="meal-lines">
@if (!string.IsNullOrWhiteSpace(Model.BreakfastMealName)) {
<p><strong>Frukost:</strong> @Model.BreakfastMealName</p>
<div class="meal-line">
@if (Model.BreakfastThumbnail != null)
{
var b64 = Convert.ToBase64String(Model.BreakfastThumbnail);
<button type="button" class="thumb-button" onclick="showLargeImage('@b64', '@Model.BreakfastMealName')">
<img class="meal-thumb" src="data:image/jpeg;base64,@b64" alt="@Model.BreakfastMealName" />
</button>
}
<p><strong>Frukost:</strong> @Model.BreakfastMealName</p>
</div>
}
@if (!string.IsNullOrWhiteSpace(Model.LunchMealName)) {
<p><strong>Lunch:</strong> @Model.LunchMealName</p>
<div class="meal-line">
@if (Model.LunchThumbnail != null)
{
var b64 = Convert.ToBase64String(Model.LunchThumbnail);
<button type="button" class="thumb-button" onclick="showLargeImage('@b64', '@Model.LunchMealName')">
<img class="meal-thumb" src="data:image/jpeg;base64,@b64" alt="@Model.LunchMealName" />
</button>
}
<p><strong>Lunch:</strong> @Model.LunchMealName</p>
</div>
}
@if (!string.IsNullOrWhiteSpace(Model.DinnerMealName)) {
<p><strong>Middag:</strong> @Model.DinnerMealName</p>
<div class="meal-line">
@if (Model.DinnerThumbnail != null)
{
var b64 = Convert.ToBase64String(Model.DinnerThumbnail);
<button type="button" class="thumb-button" onclick="showLargeImage('@b64', '@Model.DinnerMealName')">
<img class="meal-thumb" src="data:image/jpeg;base64,@b64" alt="@Model.DinnerMealName" />
</button>
}
<p><strong>Middag:</strong> @Model.DinnerMealName</p>
</div>
}
@if (ViewBag.RestaurantIsOpen as bool? == true)
{
<p><strong>Pizzerian är öppen!</strong></p><a asp-controller="FoodMenu" asp-action="PizzaOrder">Klicka här för att Beställa pizza</a>
<p><strong>Pizzerian är öppen!</strong></p>
<a asp-controller="FoodMenu" asp-action="PizzaOrder">Klicka här för att Beställa pizza</a>
}
</div>
}
else
{
<p class="no-menu">Ingen meny är inlagd för denna dag.</p>
}
<a class="nav-button" href="/Home/Menu">Visa hela veckomenyn</a>
</div>
</section>
<div id="lightboxOverlay" class="lightbox-overlay" onclick="hideLargeImage()" style="display:none;">
<div class="lightbox-content" onclick="event.stopPropagation()">
<button class="close-lightbox" onclick="hideLargeImage()">×</button>
<img id="lightboxImage" class="lightbox-image" src="" alt="Meal image" />
<p id="lightboxCaption" class="lightbox-caption"></p>
</div>
</div>
<script>
function showLargeImage(base64Data, caption) {
const overlay = document.getElementById("lightboxOverlay");
const image = document.getElementById("lightboxImage");
const text = document.getElementById("lightboxCaption");
image.src = `data:image/jpeg;base64,${base64Data}`;
text.textContent = caption || '';
overlay.style.display = "flex";
document.body.classList.add("no-scroll");
}
function hideLargeImage() {
document.getElementById("lightboxOverlay").style.display = "none";
document.body.classList.remove("no-scroll");
}
document.addEventListener('keydown', function (e) {
if (e.key === "Escape") hideLargeImage();
});
</script>

View File

@@ -80,3 +80,80 @@
background: #2563eb;
transform: translateY(-2px);
}
.thumb-button {
border: none;
background: none;
padding: 0;
cursor: pointer;
}
.meal-line {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 10px;
}
.meal-thumb {
width: 48px;
height: 48px;
object-fit: cover;
border-radius: 6px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.lightbox-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0,0,0,0.8);
display: flex;
align-items: center;
justify-content: center;
z-index: 10000;
padding: 20px;
}
.lightbox-content {
position: relative;
max-width: 90%;
max-height: 90%;
display: flex;
flex-direction: column;
align-items: center;
}
.lightbox-image {
max-width: 100%;
max-height: 80vh;
border-radius: 8px;
box-shadow: 0 4px 16px rgba(0,0,0,0.5);
}
.lightbox-caption {
color: white;
margin-top: 10px;
font-size: 16px;
text-align: center;
}
.close-lightbox {
position: absolute;
top: -10px;
right: -10px;
background: #fff;
color: #333;
border: none;
border-radius: 50%;
font-size: 20px;
width: 32px;
height: 32px;
cursor: pointer;
box-shadow: 0 2px 6px rgba(0,0,0,0.3);
}
.no-scroll {
overflow: hidden;
}

View File

@@ -1,18 +1,23 @@
:root {
--text-main: #1E293B;
--text-sub: #64748B;
--bg-main: #f9fafb;
--bg-card: #f1f5f9;
--border-color: #e5e7eb;
--card-income: #f97316;
--text-main: #1F2937; /* Mörkblågrå tydlig men mjuk */
--text-sub: #64748B; /* Sekundär gråblå */
--bg-main: #1F2C3C; /* SID-bakgrund */
--bg-card: #dbe3ec;
--bg-card-summary: #d6dde6;
--border-color: #cbd5e1; /* Ljus kant men inte skarp */
--item-divider: rgba(0, 0, 0, 0.05); /* ljus standard */
--card-income: #fb923c;
--card-expense: #ef4444;
--card-savings: #facc15;
--card-leftover: #86efac;
--card-leftover: #4ade80;
--btn-edit: #3b82f6;
--btn-check: #10b981;
--btn-delete: #ef4444;
}
body {
background-color: var(--bg-main);
color: var(--text-main);
@@ -105,6 +110,7 @@ body {
height: 350px;
border: 1px solid var(--border-color);
border-radius: 12px;
color: var(--text-main);
background-color: var(--bg-card);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
overflow: hidden;
@@ -134,6 +140,7 @@ body {
border-top: 1px solid var(--border-color);
border-radius: 0 0 12px 12px;
flex-shrink: 0;
background: var(--bg-card-summary);
}
.card-header {
@@ -521,7 +528,10 @@ color: var(--btn-check);
padding: 8px 12px;
border-radius: 8px;
box-shadow: 0 1px 3px rgba(0,0,0,0.04);
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.15);
min-width: 220px;
background-color: var(--bg-card);
color: var(--text-main);
}
.budget-summary-box h3,
@@ -601,7 +611,18 @@ canvas {
height: auto;
}
body.dark-mode {
--item-divider: rgba(255, 255, 255, 0.05); /* för mörk bakgrund */
}
.item-row:not(.total-row) {
border-bottom: 1px solid var(--item-divider);
padding-bottom: 4px;
margin-bottom: 4px;
}
@media (min-width: 769px) {
.budget-overview-row {
display: grid;
grid-template-columns: 1fr 1fr;
@@ -634,10 +655,51 @@ canvas {
max-width: 100%;
height: auto;
}
.item-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
padding: 4px 8px;
border-bottom: 1px solid var(--border-color);
}
.item-label {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 100%;
display: inline-block;
}
.amount {
flex-shrink: 0;
white-space: nowrap;
font-weight: bold;
}
}
@media (max-width: 768px) {
.item-label {
white-space: normal;
overflow-wrap: break-word;
word-break: break-word;
flex: 1 1 auto;
max-width: 100%;
}
.amount {
white-space: nowrap;
text-align: right;
flex-shrink: 0;
}
.budget-grid {
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
}
.budget-overview-row {
display: flex;
flex-direction: column;
@@ -662,6 +724,8 @@ canvas {
width: 100%;
text-align: center;
padding-bottom: 10px;
color: #1E293B; /* djupare textfärg */
font-weight: 600;
}
.chart-area canvas {

View File

@@ -7,13 +7,17 @@ app.controller('BudgetController', function ($scope, $http) {
$scope.menuOpen = false;
$scope.chartMode = "pie";
const today = new Date();
$scope.selectedYear = today.getFullYear();
$scope.selectedMonth = today.getMonth() + 1;
$scope.tempMonth = $scope.monthNames?.[today.getMonth()] || "";
$scope.tempYear = $scope.selectedYear;
$scope.monthNames = ["Januari", "Februari", "Mars", "April", "Maj", "Juni", "Juli", "Augusti", "September", "Oktober", "November", "December"];
$scope.getMonthName = function (month) {
return $scope.monthNames[month - 1] || "";
};
$scope.selectedYear = window.initialYear || new Date().getFullYear();
$scope.selectedMonth = window.initialMonth || new Date().getMonth() + 1;
$scope.tempMonth = $scope.monthNames[$scope.selectedMonth - 1];
$scope.tempYear = $scope.selectedYear;
$scope.selectedMonthName = $scope.getMonthName($scope.selectedMonth);
$scope.getMonthName = function (month) {
return $scope.monthNames[month - 1] || "";
@@ -42,6 +46,17 @@ app.controller('BudgetController', function ($scope, $http) {
};
$scope.menuVisible = true;
};
$scope.updateMonthAndUrl = function () {
const year = $scope.selectedYear;
const month = $scope.selectedMonth.toString();
const newUrl = `/budget/${year}/${month}`;
window.history.replaceState(null, '', newUrl); // Uppdaterar URL utan reload
$scope.selectedMonthName = $scope.getMonthName($scope.selectedMonth);
$scope.tempMonth = $scope.selectedMonthName;
$scope.tempYear = $scope.selectedYear;
$scope.loadBudget();
};
$scope.setItemType = function (item, type) {
if (type === 'expense') {
@@ -307,12 +322,12 @@ app.controller('BudgetController', function ($scope, $http) {
if (monthIndex >= 0 && $scope.tempYear) {
$scope.selectedMonth = monthIndex + 1;
$scope.selectedYear = parseInt($scope.tempYear);
$scope.selectedMonthName = $scope.getMonthName($scope.selectedMonth);
$scope.updateMonthAndUrl();
$scope.showMonthPicker = false;
$scope.loadBudget();
}
};
$scope.previousMonth = function () {
if ($scope.selectedMonth === 1) {
$scope.selectedMonth = 12;
@@ -320,12 +335,10 @@ app.controller('BudgetController', function ($scope, $http) {
} else {
$scope.selectedMonth--;
}
$scope.tempMonth = $scope.getMonthName($scope.selectedMonth);
$scope.tempYear = $scope.selectedYear;
$scope.selectedMonthName = $scope.getMonthName($scope.selectedMonth);
$scope.loadBudget();
$scope.updateMonthAndUrl();
};
$scope.nextMonth = function () {
if ($scope.selectedMonth === 12) {
$scope.selectedMonth = 1;
@@ -333,12 +346,10 @@ app.controller('BudgetController', function ($scope, $http) {
} else {
$scope.selectedMonth++;
}
$scope.tempMonth = $scope.getMonthName($scope.selectedMonth);
$scope.tempYear = $scope.selectedYear;
$scope.selectedMonthName = $scope.getMonthName($scope.selectedMonth);
$scope.loadBudget();
$scope.updateMonthAndUrl();
};
$scope.handleCategoryDrop = function (data, targetCategory) {
if (data.type !== 'category') return; // ⛔ stoppa om det är ett item-drag