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 namespace Aberwyn.Controllers
{ {
[Authorize(Roles = "Budget")]
public class BudgetController : Controller public class BudgetController : Controller
{ {
[Authorize(Roles = "Budget")] [Route("budget/{year:int}/{month:int}")]
public IActionResult Index(int year, int month)
{
ViewBag.Year = year;
ViewBag.Month = month;
return View();
}
// För fallback när ingen månad/år anges
[Route("budget")]
public IActionResult Index() public IActionResult Index()
{ {
ViewData["HideSidebar"] = true; var now = DateTime.Now;
return View(); 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 week = ISOWeek.GetWeekOfYear(date);
int year = date.Year; int year = date.Year;
// Gör om till ISO 8601: Måndag = 1, Söndag = 7
int dayOfWeek = (int)date.DayOfWeek; int dayOfWeek = (int)date.DayOfWeek;
if (dayOfWeek == 0) dayOfWeek = 7; if (dayOfWeek == 0) dayOfWeek = 7;
@@ -361,20 +360,30 @@ public List<WeeklyMenu> GetWeeklyMenu(int weekNumber, int year)
var allMeals = _context.Meals var allMeals = _context.Meals
.Where(m => mealIds.Contains(m.Id)) .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)) if (menu.BreakfastMealId is int bId && allMeals.TryGetValue(bId, out var breakfast))
menu.BreakfastMealName = breakfast; {
if (menu.LunchMealId.HasValue && allMeals.TryGetValue(menu.LunchMealId.Value, out var lunch)) menu.BreakfastMealName = breakfast.Name;
menu.LunchMealName = lunch; menu.BreakfastThumbnail = breakfast.ThumbnailData;
if (menu.DinnerMealId.HasValue && allMeals.TryGetValue(menu.DinnerMealId.Value, out var dinner)) }
menu.DinnerMealName = dinner;
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; return menu;
} }
public List<WeeklyMenu> GetMenuEntriesByDateRange(DateTime startDate, DateTime endDate) public List<WeeklyMenu> GetMenuEntriesByDateRange(DateTime startDate, DateTime endDate)
{ {
var results = new List<WeeklyMenu>(); var results = new List<WeeklyMenu>();

View File

@@ -43,6 +43,9 @@ public class WeeklyMenu
public int WeekNumber { get; set; } public int WeekNumber { get; set; }
public int Year { get; set; } public int Year { get; set; }
public DateTime CreatedAt { 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? BreakfastMealName { get; set; }
[NotMapped] public string? LunchMealName { get; set; } [NotMapped] public string? LunchMealName { get; set; }

View File

@@ -4,6 +4,7 @@
@{ @{
ViewData["Title"] = "Budget"; ViewData["Title"] = "Budget";
} }
<div ng-app="budgetApp" ng-controller="BudgetController" class="budget-page" ng-if="!loading"> <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="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;"> <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)"> on-category-drop="handleCategoryDrop(data, targetCategory)">
<div class="card-header" style="background-color: {{cat.color}};"> <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="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" /> <input class="color-edit" type="color" ng-model="cat.color" ng-if="cat.editing" />
<div class="header-actions"> <div class="header-actions">
@@ -129,11 +130,10 @@
ng-show="cat.editing" ng-show="cat.editing"
style="opacity: 0.5; padding-right: 6px; cursor: grab;"></i> style="opacity: 0.5; padding-right: 6px; cursor: grab;"></i>
<input type="text" ng-model="item.name" ng-if="cat.editing" /> <input type="text" class="item-label" ng-model="item.name" ng-if="cat.editing" />
<span ng-if="!cat.editing" title="{{ item.definitionName }}">{{ item.name }}</span> <span ng-if="!cat.editing" class="item-label" title="{{ item.name }}">{{ item.name }}</span>
<!-- <span ng-if="!cat.editing">#{{ item.definitionName }}</span>--> <input type="number" ng-model="item.amount" ng-if="cat.editing" />
<input type="number" ng-model="item.amount" ng-if="cat.editing" /> <span class="amount" ng-if="!cat.editing">{{ item.amount | number:0 }}</span>
<span class="amount" ng-if="!cat.editing">{{ item.amount | number:0 }}</span>
<!-- 3-pricksmeny --> <!-- 3-pricksmeny -->
<div class="item-menu-container" ng-if="cat.editing" style="position: relative;"> <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://cdn.jsdelivr.net/npm/chart.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.8.2/angular.min.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" /> <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.js"></script>
<script src="~/js/budget-dragdrop.js"></script> <script src="~/js/budget-dragdrop.js"></script>

View File

@@ -18,27 +18,87 @@
@if (Model != null) @if (Model != null)
{ {
<div class="meal-lines"> <div class="meal-lines">
@if (!string.IsNullOrWhiteSpace(Model.BreakfastMealName)) { @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)) { @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)) { @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) @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> </div>
} }
else else
{ {
<p class="no-menu">Ingen meny är inlagd för denna dag.</p> <p class="no-menu">Ingen meny är inlagd för denna dag.</p>
} }
<a class="nav-button" href="/Home/Menu">Visa hela veckomenyn</a> <a class="nav-button" href="/Home/Menu">Visa hela veckomenyn</a>
</div> </div>
</section> </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; background: #2563eb;
transform: translateY(-2px); 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 { :root {
--text-main: #1E293B; --text-main: #1F2937; /* Mörkblågrå tydlig men mjuk */
--text-sub: #64748B; --text-sub: #64748B; /* Sekundär gråblå */
--bg-main: #f9fafb; --bg-main: #1F2C3C; /* SID-bakgrund */
--bg-card: #f1f5f9; --bg-card: #dbe3ec;
--border-color: #e5e7eb; --bg-card-summary: #d6dde6;
--card-income: #f97316; --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-expense: #ef4444;
--card-savings: #facc15; --card-savings: #facc15;
--card-leftover: #86efac; --card-leftover: #4ade80;
--btn-edit: #3b82f6; --btn-edit: #3b82f6;
--btn-check: #10b981; --btn-check: #10b981;
--btn-delete: #ef4444; --btn-delete: #ef4444;
} }
body { body {
background-color: var(--bg-main); background-color: var(--bg-main);
color: var(--text-main); color: var(--text-main);
@@ -105,6 +110,7 @@ body {
height: 350px; height: 350px;
border: 1px solid var(--border-color); border: 1px solid var(--border-color);
border-radius: 12px; border-radius: 12px;
color: var(--text-main);
background-color: var(--bg-card); background-color: var(--bg-card);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05); box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
overflow: hidden; overflow: hidden;
@@ -134,6 +140,7 @@ body {
border-top: 1px solid var(--border-color); border-top: 1px solid var(--border-color);
border-radius: 0 0 12px 12px; border-radius: 0 0 12px 12px;
flex-shrink: 0; flex-shrink: 0;
background: var(--bg-card-summary);
} }
.card-header { .card-header {
@@ -521,7 +528,10 @@ color: var(--btn-check);
padding: 8px 12px; padding: 8px 12px;
border-radius: 8px; border-radius: 8px;
box-shadow: 0 1px 3px rgba(0,0,0,0.04); 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; min-width: 220px;
background-color: var(--bg-card);
color: var(--text-main);
} }
.budget-summary-box h3, .budget-summary-box h3,
@@ -601,7 +611,18 @@ canvas {
height: auto; 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) { @media (min-width: 769px) {
.budget-overview-row { .budget-overview-row {
display: grid; display: grid;
grid-template-columns: 1fr 1fr; grid-template-columns: 1fr 1fr;
@@ -634,10 +655,51 @@ canvas {
max-width: 100%; max-width: 100%;
height: auto; 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) { @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 { .budget-overview-row {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@@ -662,6 +724,8 @@ canvas {
width: 100%; width: 100%;
text-align: center; text-align: center;
padding-bottom: 10px; padding-bottom: 10px;
color: #1E293B; /* djupare textfärg */
font-weight: 600;
} }
.chart-area canvas { .chart-area canvas {

View File

@@ -7,13 +7,17 @@ app.controller('BudgetController', function ($scope, $http) {
$scope.menuOpen = false; $scope.menuOpen = false;
$scope.chartMode = "pie"; $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.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) { $scope.getMonthName = function (month) {
return $scope.monthNames[month - 1] || ""; return $scope.monthNames[month - 1] || "";
@@ -42,6 +46,17 @@ app.controller('BudgetController', function ($scope, $http) {
}; };
$scope.menuVisible = true; $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) { $scope.setItemType = function (item, type) {
if (type === 'expense') { if (type === 'expense') {
@@ -307,12 +322,12 @@ app.controller('BudgetController', function ($scope, $http) {
if (monthIndex >= 0 && $scope.tempYear) { if (monthIndex >= 0 && $scope.tempYear) {
$scope.selectedMonth = monthIndex + 1; $scope.selectedMonth = monthIndex + 1;
$scope.selectedYear = parseInt($scope.tempYear); $scope.selectedYear = parseInt($scope.tempYear);
$scope.selectedMonthName = $scope.getMonthName($scope.selectedMonth); $scope.updateMonthAndUrl();
$scope.showMonthPicker = false; $scope.showMonthPicker = false;
$scope.loadBudget();
} }
}; };
$scope.previousMonth = function () { $scope.previousMonth = function () {
if ($scope.selectedMonth === 1) { if ($scope.selectedMonth === 1) {
$scope.selectedMonth = 12; $scope.selectedMonth = 12;
@@ -320,12 +335,10 @@ app.controller('BudgetController', function ($scope, $http) {
} else { } else {
$scope.selectedMonth--; $scope.selectedMonth--;
} }
$scope.tempMonth = $scope.getMonthName($scope.selectedMonth); $scope.updateMonthAndUrl();
$scope.tempYear = $scope.selectedYear;
$scope.selectedMonthName = $scope.getMonthName($scope.selectedMonth);
$scope.loadBudget();
}; };
$scope.nextMonth = function () { $scope.nextMonth = function () {
if ($scope.selectedMonth === 12) { if ($scope.selectedMonth === 12) {
$scope.selectedMonth = 1; $scope.selectedMonth = 1;
@@ -333,12 +346,10 @@ app.controller('BudgetController', function ($scope, $http) {
} else { } else {
$scope.selectedMonth++; $scope.selectedMonth++;
} }
$scope.tempMonth = $scope.getMonthName($scope.selectedMonth); $scope.updateMonthAndUrl();
$scope.tempYear = $scope.selectedYear;
$scope.selectedMonthName = $scope.getMonthName($scope.selectedMonth);
$scope.loadBudget();
}; };
$scope.handleCategoryDrop = function (data, targetCategory) { $scope.handleCategoryDrop = function (data, targetCategory) {
if (data.type !== 'category') return; // ⛔ stoppa om det är ett item-drag if (data.type !== 'category') return; // ⛔ stoppa om det är ett item-drag