Budget improvements and list!
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
Elias Jansson
2025-09-11 22:42:56 +02:00
parent f63ccc2a38
commit 64aa9cf716
18 changed files with 2206 additions and 75 deletions

View File

@@ -24,6 +24,7 @@ namespace Aberwyn.Controllers
{ {
try try
{ {
var period = await _context.BudgetPeriods var period = await _context.BudgetPeriods
.Include(p => p.Categories) .Include(p => p.Categories)
.ThenInclude(c => c.Items) .ThenInclude(c => c.Items)
@@ -78,10 +79,13 @@ namespace Aberwyn.Controllers
[HttpGet("byname/{name}")] [HttpGet("byname/{name}")]
public async Task<IActionResult> GetBudgetByName(string name) public async Task<IActionResult> GetBudgetByName(string name)
{ {
var period = await _context.BudgetPeriods var period = _context.BudgetPeriods
.Include(p => p.Categories) .Include(p => p.Categories)
.ThenInclude(c => c.Items) .ThenInclude(c => c.Items)
.FirstOrDefaultAsync(p => p.Name != null && p.Name.ToLower() == name.ToLower()); .AsEnumerable() // hämta från db och gör resten i minnet
.FirstOrDefault(p => p.Name != null &&
p.Name.Equals(name, StringComparison.OrdinalIgnoreCase));
if (period == null) if (period == null)
{ {
@@ -363,6 +367,119 @@ namespace Aberwyn.Controllers
return Ok(new { id = newItem.Id }); return Ok(new { id = newItem.Id });
} }
[HttpGet("list")]
public async Task<IActionResult> GetAllBudgets()
{
var periods = await _context.BudgetPeriods
.Include(p => p.Categories)
.ThenInclude(c => c.Items)
.OrderByDescending(p => p.Year)
.ThenByDescending(p => p.Month)
.ToListAsync();
var result = periods.Select(p => new
{
id = p.Id,
name = p.Name,
year = p.Year,
month = p.Month,
categories = p.Categories
.OrderBy(c => c.Order)
.Select(c => new
{
id = c.Id,
name = c.Name,
color = c.Color,
total = c.Items.Sum(i => i.Amount),
items = c.Items
.OrderBy(i => i.Order)
.Select(i => new
{
id = i.Id,
name = i.Name,
amount = i.Amount,
isExpense = i.IsExpense,
includeInSummary = i.IncludeInSummary
}).ToList()
}).ToList(),
total = p.Categories.Sum(c => c.Items.Sum(i => i.Amount))
});
return Ok(result);
}
// DELETE: api/budget/byname/{name}
[HttpDelete("byname/{name}")]
public async Task<IActionResult> DeleteByName(string name)
{
var period = await _context.BudgetPeriods
.Include(p => p.Categories)
.ThenInclude(c => c.Items)
.FirstOrDefaultAsync(p => p.Name != null && p.Name.ToLower() == name.ToLower());
if (period == null)
return NotFound();
foreach (var category in period.Categories)
{
_context.BudgetItems.RemoveRange(category.Items);
}
_context.BudgetCategories.RemoveRange(period.Categories);
_context.BudgetPeriods.Remove(period);
await _context.SaveChangesAsync();
return NoContent();
}
// POST: api/budget/copy/byname/{targetName}?from={sourceName}
[HttpPost("copy/byname/{targetName}")]
public async Task<IActionResult> CopyFromNamedBudget(string targetName, [FromQuery] string from)
{
var targetPeriod = await _context.BudgetPeriods
.Include(p => p.Categories)
.ThenInclude(c => c.Items)
.FirstOrDefaultAsync(p => p.Name != null && p.Name.ToLower() == targetName.ToLower());
if (targetPeriod != null && targetPeriod.Categories.Any())
return BadRequest("Det finns redan data för denna budget.");
var sourcePeriod = await _context.BudgetPeriods
.Include(p => p.Categories)
.ThenInclude(c => c.Items)
.FirstOrDefaultAsync(p => p.Name != null && p.Name.ToLower() == from.ToLower());
if (sourcePeriod == null)
return NotFound("Ingen budget hittades att kopiera från.");
var newPeriod = new BudgetPeriod
{
Name = targetName,
Categories = sourcePeriod.Categories.Select(cat => new BudgetCategory
{
Name = cat.Name,
Color = cat.Color,
Order = cat.Order,
BudgetCategoryDefinitionId = cat.BudgetCategoryDefinitionId,
Items = cat.Items.Select(item => new BudgetItem
{
Name = item.Name,
Amount = item.Amount,
IsExpense = item.IsExpense,
IncludeInSummary = item.IncludeInSummary,
Order = item.Order,
BudgetItemDefinitionId = item.BudgetItemDefinitionId
}).ToList()
}).ToList()
};
_context.BudgetPeriods.Add(newPeriod);
await _context.SaveChangesAsync();
return Ok(new { id = newPeriod.Id });
}
[HttpDelete("item/{id}")] [HttpDelete("item/{id}")]

View File

@@ -13,6 +13,11 @@ namespace Aberwyn.Controllers
ViewBag.Month = month; ViewBag.Month = month;
return View(); return View();
} }
[Route("budget/list")]
public IActionResult List()
{
return View();
}
[Route("budget/{name}")] [Route("budget/{name}")]
public IActionResult Index(string name) public IActionResult Index(string name)

View File

@@ -488,6 +488,34 @@ namespace Aberwyn.Controllers
return RedirectToAction("PizzaAdmin"); return RedirectToAction("PizzaAdmin");
} }
[Authorize(Roles = "Chef")]
[HttpGet]
public IActionResult Calculator()
{
var plans = _context.DoughPlans
.OrderByDescending(p => p.Datum)
.ThenByDescending(p => p.Id)
.ToList();
ViewBag.Plans = plans;
return View(new DoughPlan { AntalPizzor = 8, ViktPerPizza = 220 });
}
[Authorize(Roles = "Chef")]
[HttpPost]
public IActionResult SaveDoughPlan([FromBody] DoughPlan model)
{
if (model == null) return BadRequest();
_context.DoughPlans.Add(model);
_context.SaveChanges();
return Json(new { success = true, id = model.Id });
}
} }
} }

View File

@@ -59,5 +59,8 @@ namespace Aberwyn.Controllers
TempData["Success"] = "Beställningen har lagts!"; TempData["Success"] = "Beställningen har lagts!";
return RedirectToAction("Order"); return RedirectToAction("Order");
} }
} }
} }

View File

@@ -38,9 +38,10 @@ namespace Aberwyn.Data
builder.Entity<TorrentItem>() builder.Entity<TorrentItem>()
.OwnsOne(t => t.Metadata); .OwnsOne(t => t.Metadata);
} }
public DbSet<DoughPlan> DoughPlans { get; set; }
public DbSet<BudgetPeriod> BudgetPeriods { get; set; } public DbSet<BudgetPeriod> BudgetPeriods { get; set; }
public DbSet<BudgetCategory> BudgetCategories { get; set; } public DbSet<BudgetCategory> BudgetCategories { get; set; }
public DbSet<BudgetItem> BudgetItems { get; set; } public DbSet<BudgetItem> BudgetItems { get; set; }

View File

@@ -32,7 +32,7 @@ namespace Aberwyn.Data
var oneHourAgo = DateTime.UtcNow.AddHours(-1); var oneHourAgo = DateTime.UtcNow.AddHours(-1);
if (debug) if (debug)
oneHourAgo = DateTime.UtcNow.AddHours(1); oneHourAgo = DateTime.UtcNow.AddHours(1);
var activeFeeds = await _context.RssFeeds var activeFeeds = await _context.RssFeeds
.Where(f => f.IsActive && f.LastChecked <= oneHourAgo) .Where(f => f.IsActive && f.LastChecked <= oneHourAgo)
.ToListAsync(); .ToListAsync();

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,44 @@
using System;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Aberwyn.Migrations
{
public partial class AddDoughPlans : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "DoughPlans",
columns: table => new
{
Id = table.Column<int>(type: "int", nullable: false)
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
AntalPizzor = table.Column<int>(type: "int", nullable: false),
ViktPerPizza = table.Column<double>(type: "double", nullable: false),
Mjol = table.Column<double>(type: "double", nullable: false),
Vatten = table.Column<double>(type: "double", nullable: false),
Olja = table.Column<double>(type: "double", nullable: false),
Salt = table.Column<double>(type: "double", nullable: false),
Jast = table.Column<double>(type: "double", nullable: false),
TotalDeg = table.Column<double>(type: "double", nullable: false),
Datum = table.Column<DateTime>(type: "datetime(6)", nullable: false),
Namn = table.Column<string>(type: "longtext", nullable: false)
.Annotation("MySql:CharSet", "utf8mb4")
},
constraints: table =>
{
table.PrimaryKey("PK_DoughPlans", x => x.Id);
})
.Annotation("MySql:CharSet", "utf8mb4");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "DoughPlans");
}
}
}

View File

@@ -243,6 +243,48 @@ namespace Aberwyn.Migrations
b.ToTable("BudgetPeriods"); b.ToTable("BudgetPeriods");
}); });
modelBuilder.Entity("Aberwyn.Models.DoughPlan", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<int>("AntalPizzor")
.HasColumnType("int");
b.Property<DateTime>("Datum")
.HasColumnType("datetime(6)");
b.Property<double>("Jast")
.HasColumnType("double");
b.Property<double>("Mjol")
.HasColumnType("double");
b.Property<string>("Namn")
.IsRequired()
.HasColumnType("longtext");
b.Property<double>("Olja")
.HasColumnType("double");
b.Property<double>("Salt")
.HasColumnType("double");
b.Property<double>("TotalDeg")
.HasColumnType("double");
b.Property<double>("Vatten")
.HasColumnType("double");
b.Property<double>("ViktPerPizza")
.HasColumnType("double");
b.HasKey("Id");
b.ToTable("DoughPlans");
});
modelBuilder.Entity("Aberwyn.Models.Ingredient", b => modelBuilder.Entity("Aberwyn.Models.Ingredient", b =>
{ {
b.Property<int>("Id") b.Property<int>("Id")

View File

@@ -26,5 +26,22 @@ namespace Aberwyn.Models
public bool RestaurantIsOpen { get; set; } public bool RestaurantIsOpen { get; set; }
} }
public class DoughPlan
{
public int Id { get; set; }
public int AntalPizzor { get; set; }
public double ViktPerPizza { get; set; }
public double Mjol { get; set; }
public double Vatten { get; set; }
public double Olja { get; set; }
public double Salt { get; set; }
public double Jast { get; set; }
public double TotalDeg { get; set; }
public DateTime Datum { get; set; }
public string Namn { get; set; }
}
} }

View File

@@ -5,84 +5,95 @@
ViewData["Title"] = "Budget"; ViewData["Title"] = "Budget";
} }
<div ng-app="budgetApp" ng-controller="BudgetController"> <div ng-app="budgetApp" ng-controller="BudgetController">
<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;">
<button class="nav-button" ng-click="previousMonth()">←</button>
<span class="month-label" ng-click="showMonthPicker = !showMonthPicker" style="cursor: pointer;">
{{ selectedMonthName }} {{ selectedYear }}
</span>
<button class="nav-button" ng-click="nextMonth()">→</button>
<div class="month-picker-dropdown" ng-show="showMonthPicker" style="position: absolute; top: 100%; left: 50%; transform: translateX(-50%); background: white; border: 1px solid #ccc; padding: 10px; border-radius: 8px; z-index: 1000; box-shadow: 0 4px 8px rgba(0,0,0,0.1);"> <div class="month-nav-bar" ng-if="!budget.name" style="display: flex; align-items: center; gap: 10px; position: relative;">
<select ng-model="tempMonth" ng-options="month for month in monthNames" style="margin-bottom: 8px;"></select><br> <a href="/budget/list" class="nav-button" style="margin-right: 10px;">
<input type="number" ng-model="tempYear" placeholder="År" style="width: 100%; margin-bottom: 8px;" /> Lista
<button class="nav-button" ng-click="applyMonthSelection()">Välj</button> </a>
</div> <button class="nav-button" ng-click="previousMonth()">←</button>
</div> <span class="month-label" ng-click="showMonthPicker = !showMonthPicker" style="cursor: pointer;">
{{ selectedMonthName }} {{ selectedYear }}
</span>
<button class="nav-button" ng-click="nextMonth()">→</button>
<div class="menu-container" ng-class="{ 'open': menuOpen }"> <div class="month-picker-dropdown" ng-show="showMonthPicker" style="position: absolute; top: 100%; left: 50%; transform: translateX(-50%); background: white; border: 1px solid #ccc; padding: 10px; border-radius: 8px; z-index: 1000; box-shadow: 0 4px 8px rgba(0,0,0,0.1);">
<button class="icon-button" ng-click="toggleMenu($event)"> <select ng-model="tempMonth" ng-options="month for month in monthNames" style="margin-bottom: 8px;"></select><br>
<i class="fa fa-ellipsis-v"></i> <input type="number" ng-model="tempYear" placeholder="År" style="width: 100%; margin-bottom: 8px;" />
</button> <button class="nav-button" ng-click="applyMonthSelection()">Välj</button>
<div class="dropdown-menu" ng-show="menuOpen">
<button ng-click="copyPreviousMonthSafe()">Kopiera föregående månad</button>
<button ng-click="deleteMonth(); menuOpen = false;" class="danger">Ta bort hela månaden</button>
<button ng-click="createNewCategory(); menuOpen = false;">Lägg till ny kategori</button>
<!--<button ng-click="openImportModule(); menuOpen = false;">📥 Importera rader</button> -->
</div>
</div> </div>
</div> </div>
<div class="month-nav-bar" ng-if="budget.name">
<a href="/budget/list" class="nav-button" style="margin-right: 10px;">
Lista
</a>
<span class="month-label">
{{ budget.name }}
</span>
</div>
<div class="menu-container" ng-class="{ 'open': menuOpen }">
<button class="icon-button" ng-click="toggleMenu($event)">
<i class="fa fa-ellipsis-v"></i>
</button>
<div class="dropdown-menu" ng-show="menuOpen">
<button class="nav-button" ng-click="createNamedBudget()">Ny budget</button>
<button ng-click="copyBudget(); menuOpen = false;">Kopiera</button>
<button ng-click="deleteBudget(); menuOpen = false;" class="danger">Ta bort</button>
<button ng-click="createNewCategory(); menuOpen = false;">Lägg till ny kategori</button>
</div>
</div>
</div>
<div class="budget-overview-row" ng-if="budget && budget.categories.length > 0"> <div class="budget-overview-row" ng-if="budget && budget.categories.length > 0">
<!-- Vänster: Summering --> <!-- Vänster: Summering -->
<div class="budget-summary-box compact"> <div class="budget-summary-box compact">
<h3>Sammanställning</h3> <h3>Sammanställning</h3>
<ul class="summary-list"> <ul class="summary-list">
<li><span>💰 Inkomst:</span> {{ getTotalIncome() | number:0 }} kr</li> <li><span>💰 Inkomst:</span> {{ getTotalIncome() | number:0 }} kr</li>
<li><span>💸 Utgift:</span> {{ getTotalExpense() | number:0 }} kr</li> <li><span>💸 Utgift:</span> {{ getTotalExpense() | number:0 }} kr</li>
<li><span>🏦 Sparande:</span> {{ getTotalSaving() | number:0 }} kr</li> <li><span>🏦 Sparande:</span> {{ getTotalSaving() | number:0 }} kr</li>
<p> <p>
📈 Kvar: 📈 Kvar:
<span class="highlight leftover" ng-class="{'plus': getLeftover() >= 0, 'minus': getLeftover() < 0}"> <span class="highlight leftover" ng-class="{'plus': getLeftover() >= 0, 'minus': getLeftover() < 0}">
{{ getLeftover() | number:0 }} kr {{ getLeftover() | number:0 }} kr
</span> </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>
</ul> <p ng-if="topExpenseCategory" style="font-size: 12px; margin-bottom: 12px;">
<hr> Största kategori: <br>
<ul class="category-summary-list"> <strong>{{ topExpenseCategory.name }}</strong> ({{ topExpenseCategory.percent | number:1 }}%)
<li ng-repeat="cat in budget.categories"> </p>
<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="chart-area">
<canvas id="expenseChart"></canvas>
</div>
</div>
</div> </div>
</div>
<div class="budget-grid" ng-if="budget && budget.categories"> <div class="budget-grid" ng-if="budget && budget.categories">

View File

@@ -0,0 +1,120 @@
@attribute [Authorize(Roles = "Budget")]
@using Microsoft.AspNetCore.Authorization
@{
ViewData["Title"] = "Budgetlista";
}
<div ng-app="budgetApp" ng-controller="BudgetListController" class="budget-page">
<!-- Toggle -->
<div class="details-toggle">
<label>
<input type="checkbox" ng-model="showDetails"> Visa mer detaljer
</label>
</div>
<div ng-repeat="(year, months) in monthsByYear">
<div class="year-header">{{ year }}</div>
<div class="budget-row">
<div class="budget-card" ng-repeat="month in months" ng-click="goToBudget(month)">
<!-- Flex-container med namn + bars -->
<div class="month-header">
<div class="month-name">{{ getMonthName(month) }}</div>
<div class="month-bars">
<div class="bar income" ng-style="{'height': month.barHeights.income + '%'}" title="Inkomst"></div>
<div class="bar expenses" ng-style="{'height': month.barHeights.expenses + '%'}" title="Utgifter"></div>
<div class="bar savings" ng-style="{'height': month.barHeights.savings + '%'}" title="Sparande"></div>
<!-- <div class="bar leftover" ng-style="{'height': month.barHeights.leftover + '%'}" title="Balans"></div>-->
</div>
</div>
<div class="item-list" ng-if="showDetails">
<div class="item-row" ng-repeat="cat in month.categories">
<span class="item-label" style="color: {{ cat.color }}">{{ cat.name }}</span>
<span class="amount">{{ getCategorySum(cat) | number:0 }} kr</span>
</div>
</div>
</div>
</div>
</div>
</div>
<link rel="stylesheet" href="~/css/budget-list.css" />
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.8.2/angular.min.js"></script>
<script>
angular.module('budgetApp', [])
.controller('BudgetListController', ['$scope', '$http', '$window', function($scope, $http, $window){
$scope.months = [];
$scope.monthsByYear = {};
$scope.showDetails = false;
const maxBarValue = 100000;
const monthNames = ["Januari","Februari","Mars","April","Maj","Juni",
"Juli","Augusti","September","Oktober","November","December"];
$scope.getMonthName = month => month.month ? monthNames[month.month-1] : month.name;
$scope.getCategorySum = cat => cat.items ? cat.items.reduce((sum,i)=>sum+i.amount,0) : 0;
$scope.goToBudget = month => $window.location.href = '/budget/' + (month.year || month.name) + '/' + (month.month || '');
$scope.getTotalIncome = period => {
return (period.categories || [])
.flatMap(c => c.items || [])
.filter(i => !i.isExpense && i.includeInSummary)
.reduce((sum,i)=>sum+i.amount,0);
};
$scope.getTotalSavings = period => {
return (period.categories || [])
.flatMap(c => c.items || [])
.filter(i => !i.isExpense && i.includeInSummary && i.name.toLowerCase().includes('spara'))
.reduce((sum,i)=>sum+i.amount,0);
};
$scope.getTotalExpenses = period => {
return (period.categories || [])
.flatMap(c => c.items || [])
.filter(i => i.isExpense && i.includeInSummary)
.reduce((sum,i)=>sum+i.amount,0);
};
$scope.getTotalLeftover = period => {
return $scope.getTotalIncome(period) - $scope.getTotalSavings(period) - $scope.getTotalExpenses(period);
};
$http.get('/api/budget/list').then(res => {
// Sortera från januari → december per år
const sorted = res.data.sort((a,b) => (a.year||0)-(b.year||0) || (a.month||0)-(b.month||0));
$scope.months = sorted.map(month => {
const income = $scope.getTotalIncome(month);
const savings = $scope.getTotalSavings(month);
const expenses = $scope.getTotalExpenses(month);
const leftover = $scope.getTotalLeftover(month);
month.barHeights = {
income: Math.max(income / maxBarValue * 100, 5),
savings: Math.max(savings / maxBarValue * 100, 5),
expenses: Math.max(expenses / maxBarValue * 100, 5),
leftover: Math.max(leftover / maxBarValue * 100, 5)
};
return month;
});
// Gruppera per år
$scope.monthsByYear = $scope.months.reduce((acc, m) => {
const year = m.year || 'Övrigt';
if (!acc[year]) acc[year] = [];
acc[year].push(m);
return acc;
}, {});
});
}]);
</script>

View File

@@ -0,0 +1,144 @@
@model Aberwyn.Models.DoughPlan
@{
var plans = ViewBag.Plans as List<Aberwyn.Models.DoughPlan>;
}
<div class="card border-info mb-4">
<div class="card-header">🍕 Pizzakalkylator</div>
<div class="card-body">
<form id="calcForm" class="grid grid-cols-1 md:grid-cols-4 gap-3">
<div>
<label>Antal pizzor</label>
<input id="antal" type="number" class="form-control" value="8" />
</div>
<div>
<label>Vikt per pizza (g)</label>
<input id="vikt" type="number" class="form-control" value="220" />
</div>
<div>
<label>Datum</label>
<input id="datum" type="date" class="form-control" value="@DateTime.Today.ToString("yyyy-MM-dd")" />
</div>
<div>
<label>Namn på tillfälle</label>
<input id="namn" class="form-control" value="Planering" />
</div>
</form>
<button type="button" class="btn btn-success mt-3" id="saveBtn">💾 Spara</button>
<hr />
<h5>Resultat</h5>
<div class="completed-orders-grid">
<div class="completed-order-box">
<strong>Totalt deg</strong>
<span id="totalDeg"></span> g
</div>
<div class="completed-order-box">
<strong>Mjöl</strong>
<span id="mjol"></span> g
</div>
<div class="completed-order-box">
<strong>Vatten</strong>
<span id="vatten"></span> g
</div>
<div class="completed-order-box">
<strong>Olja</strong>
<span id="olja"></span> g
</div>
<div class="completed-order-box">
<strong>Salt</strong>
<span id="salt"></span> g
</div>
<div class="completed-order-box">
<strong>Jäst</strong>
<span id="jast"></span> g
</div>
</div>
</div>
</div>
@if (plans?.Any() == true)
{
<div class="card border-success">
<div class="card-header">📋 Sparade planer</div>
<div class="card-body">
<div class="completed-orders-grid">
@foreach (var p in plans)
{
<div class="completed-order-box">
<strong>@p.Namn (@p.Datum.ToString("yyyy-MM-dd"))</strong>
<span>🍕 @p.AntalPizzor st × @p.ViktPerPizza:F1 g</span>
<hr style="margin:6px 0;" />
<span><b>Totalt:</b> @p.TotalDeg:F1 g</span>
<span>Mjöl: @p.Mjol:F1 g</span>
<span>Vatten: @p.Vatten:F1 g</span>
<span>Olja: @p.Olja:F1 g</span>
<span>Salt: @p.Salt:F1 g</span>
<span>Jäst: @p.Jast:F1 g</span>
</div>
}
</div>
</div>
</div>
}
<link rel="stylesheet" href="~/css/pizzacalculator.css" />
<script>
function round1(x) {
return Math.round(x * 10) / 10;
}
function calc() {
let antal = parseFloat(document.getElementById("antal").value) || 0;
let vikt = parseFloat(document.getElementById("vikt").value) || 0;
let total = antal * vikt;
let mjol = total * (100.0 / 162.0);
let vatten = mjol * 0.52;
let olja = mjol * 0.075;
let salt = mjol * 0.02;
let jast = mjol * 0.005;
document.getElementById("totalDeg").innerText = round1(total);
document.getElementById("mjol").innerText = round1(mjol);
document.getElementById("vatten").innerText = round1(vatten);
document.getElementById("olja").innerText = round1(olja);
document.getElementById("salt").innerText = round1(salt);
document.getElementById("jast").innerText = round1(jast);
return { antal, vikt, total, mjol, vatten, olja, salt, jast };
}
["antal","vikt"].forEach(id => {
document.getElementById(id).addEventListener("input", calc);
});
calc();
document.getElementById("saveBtn").addEventListener("click", function () {
let values = calc();
let data = {
AntalPizzor: values.antal,
ViktPerPizza: values.vikt,
TotalDeg: values.total,
Mjol: values.mjol,
Vatten: values.vatten,
Olja: values.olja,
Salt: values.salt,
Jast: values.jast,
Datum: document.getElementById("datum").value,
Namn: document.getElementById("namn").value
};
fetch('@Url.Action("SaveDoughPlan", "FoodMenu")', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
}).then(r => r.json()).then(res => {
if (res.success) {
location.reload(); // 🔄 Ladda om sidan så listan uppdateras
} else {
alert("❌ Kunde inte spara");
}
});
});
</script>

View File

@@ -0,0 +1,161 @@
:root {
--text-main: #1F2937;
--text-sub: #64748B;
--bg-main: #f8f9fa;
--bg-card: #ffffff;
--border-color: #e2e8f0;
--card-income: #4ade80;
--card-savings: #facc15;
--card-expenses: #f87171;
--card-leftover: #fb923c;
--card-bar-min-height: 4px; /* minsta höjd för synlighet */
}
/* Hela sidan */
.budget-page {
padding: 16px;
background-color: var(--bg-main);
}
/* År-headers */
.year-header {
margin-top: 20px;
margin-bottom: 12px;
font-size: 16px;
font-weight: 600;
color: var(--text-main);
}
/* Grid för månader (horisontellt med wrap) */
.budget-row {
display: flex;
flex-wrap: wrap;
gap: 12px;
}
/* Kort per månad */
.budget-card {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
background-color: var(--bg-card);
border: 1px solid var(--border-color);
border-radius: 10px;
cursor: pointer;
padding: 6px;
transition: transform 0.15s ease, box-shadow 0.15s ease;
min-width: 110px;
}
.budget-card:hover {
transform: translateY(-2px);
box-shadow: 0 6px 14px rgba(0,0,0,0.1);
}
/* Månadens namn */
.month-header {
display: flex;
align-items: center; /* staplar och text vertikalt centrerade */
gap: 6px; /* mellanrum mellan namn och staplar */
cursor: pointer;
}
.month-name {
font-weight: 600;
color: var(--text-main);
min-width: 50px;
}
.month-bars {
position: absolute;
top: 0;
right: 0;
display: flex;
gap: 2px;
opacity: 0.6;
height: 30px; /* maxhöjd på staplar */
}
.bar {
width: 6px; /* lite bredare */
border-radius: 3px 3px 0 0;
background-color: gray;
transition: height 0.3s ease;
min-height: 8px; /* alltid synlig höjd */
}
.bar.income {
background-color: var(--card-income);
}
.bar.savings {
background-color: var(--card-savings);
}
.bar.expenses {
background-color: var(--card-expenses);
}
.bar.leftover {
background-color: var(--card-leftover);
}
/* Detaljer */
.item-list {
margin-top: 6px;
display: flex;
flex-direction: column;
gap: 2px;
}
.item-row {
display: flex;
justify-content: space-between;
font-size: 12px;
min-width: 180px;
}
.item-label {
font-weight: 500;
color: var(--text-main);
}
.amount {
font-weight: 600;
}
/* Toggle */
.details-toggle {
display: flex;
align-items: center;
gap: 6px;
font-size: 13px;
padding: 8px 0;
}
.details-toggle input[type="checkbox"] {
width: 16px;
height: 16px;
}
/* Mobilanpassning */
@media (max-width: 768px) {
.budget-card {
min-width: 50px;
}
.month-name {
font-size: 12px;
}
.bar {
width: 6px;
}
.month-bars {
height: 50px;
}
}

View File

@@ -97,9 +97,10 @@ body {
.budget-grid { .budget-grid {
display: grid; display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); grid-template-columns: repeat(auto-fit, minmax(220px, 300px));
gap:10px; gap: 10px;
align-items: stretch; align-items: stretch;
} }

View File

@@ -0,0 +1,25 @@
.completed-orders-grid {
display: flex;
flex-wrap: wrap;
gap: 12px;
margin-top: 12px;
}
.completed-order-box {
background-color: #fff;
border: 1px solid #ddd;
border-radius: 12px;
padding: 12px;
font-size: 0.9rem;
min-width: 200px;
max-width: 220px;
box-shadow: 0 2px 6px rgba(0,0,0,0.08);
display: flex;
flex-direction: column;
transition: transform 0.15s ease, box-shadow 0.15s ease;
}
.completed-order-box:hover {
transform: translateY(-3px);
box-shadow: 0 4px 12px rgba(0,0,0,0.12);
}

View File

@@ -0,0 +1,28 @@
var app = angular.module("budgetListApp", []);
app.controller("BudgetListController", function ($scope, $http) {
$scope.loading = true;
$scope.error = null;
$scope.budgets = [];
$scope.monthNames = ["Januari", "Februari", "Mars", "April", "Maj", "Juni",
"Juli", "Augusti", "September", "Oktober", "November", "December"];
$scope.getBudgetUrl = function (b) {
if (b.name) {
return "/budget/" + encodeURIComponent(b.name);
}
return "/budget/" + b.year + "/" + b.month;
};
$http.get("/api/budget/list")
.then(res => {
$scope.budgets = res.data;
})
.catch(err => {
console.error("Kunde inte hämta budgetlista:", err);
$scope.error = "Fel vid laddning av budgetar.";
})
.finally(() => {
$scope.loading = false;
});
});

View File

@@ -148,7 +148,7 @@ app.controller('BudgetController', function ($scope, $http) {
let url = ""; let url = "";
if (useName) { if (useName) {
url = `/api/budget/byname/${initialName}`; url = `/api/budget/byname/${encodeURIComponent(initialName)}`;
} else { } else {
url = `/api/budget/${$scope.selectedYear}/${$scope.selectedMonth}`; url = `/api/budget/${$scope.selectedYear}/${$scope.selectedMonth}`;
} }
@@ -308,6 +308,84 @@ app.controller('BudgetController', function ($scope, $http) {
$scope.showToast("Fel vid borttagning av månad", true); $scope.showToast("Fel vid borttagning av månad", true);
}); });
}; };
$scope.deleteBudget = function () {
if (!confirm("Vill du verkligen ta bort hela budgeten?")) return;
if ($scope.budget.name) {
// Namnbudget
$http.delete(`/api/budget/byname/${encodeURIComponent($scope.budget.name)}`)
.then(() => {
$scope.showToast("Budget borttagen!");
window.location.href = "/budget"; // gå tillbaka till startsida
})
.catch(err => {
console.error("Kunde inte ta bort budget:", err);
$scope.showToast("Fel vid borttagning", true);
});
} else {
// Månad
const year = $scope.selectedYear;
const month = $scope.selectedMonth;
$http.delete(`/api/budget/${year}/${month}`)
.then(() => {
$scope.showToast("Månad borttagen!");
$scope.loadBudget();
})
.catch(err => {
console.error("Kunde inte ta bort månad:", err);
$scope.showToast("Fel vid borttagning", true);
});
}
};
$scope.copyBudget = function () {
if ($scope.budget.name) {
const from = prompt("Ange namnet på budgeten du vill kopiera ifrån:");
if (!from) return;
$http.post(`/api/budget/copy/byname/${encodeURIComponent($scope.budget.name)}?from=${encodeURIComponent(from)}`)
.then(() => {
$scope.showToast("Budget kopierad!");
$scope.loadBudget();
})
.catch(err => {
console.error("Kunde inte kopiera budget:", err);
$scope.showToast("Fel vid kopiering", true);
});
} else {
if (!confirm("Vill du kopiera föregående månad till den aktuella?")) return;
const year = $scope.selectedYear;
const month = $scope.selectedMonth;
$http.post(`/api/budget/copy/${year}/${month}`)
.then(() => {
$scope.showToast("Föregående månad kopierad!");
$scope.loadBudget();
})
.catch(err => {
console.error("Kunde inte kopiera månad:", err);
$scope.showToast("Fel vid kopiering av månad", true);
});
}
};
$scope.createNamedBudget = function () {
const name = prompt("Ange namn på din nya budget:");
if (!name) return;
$http.post('/api/budget', { name: name })
.then(res => {
$scope.showToast("Ny budget skapad!");
window.location.href = `/budget/${encodeURIComponent(name)}`;
})
.catch(err => {
console.error("Fel vid skapande:", err);
if (err.status === 409) {
$scope.showToast("En budget med detta namn finns redan.", true);
} else {
$scope.showToast("Kunde inte skapa budget.", true);
}
});
};
$scope.copyPreviousMonth = function () { $scope.copyPreviousMonth = function () {
if (!confirm("Vill du kopiera föregående månad till den aktuella?")) { if (!confirm("Vill du kopiera föregående månad till den aktuella?")) {
@@ -772,7 +850,9 @@ app.controller('BudgetController', function ($scope, $http) {
}); });
} }
}); });
$scope.goToBudgetList = function () {
$window.location.href = '/budget/list'; // den route som visar listan
};
$scope.addItemFromDefinition = function (cat) { $scope.addItemFromDefinition = function (cat) {
const definitionName = cat.newItemDefinition?.trim(); const definitionName = cat.newItemDefinition?.trim();
const label = cat.newItemLabel?.trim(); const label = cat.newItemLabel?.trim();