Budget improvements and list!
All checks were successful
continuous-integration/drone/push Build is passing
All checks were successful
continuous-integration/drone/push Build is passing
This commit is contained in:
@@ -24,6 +24,7 @@ namespace Aberwyn.Controllers
|
||||
{
|
||||
try
|
||||
{
|
||||
|
||||
var period = await _context.BudgetPeriods
|
||||
.Include(p => p.Categories)
|
||||
.ThenInclude(c => c.Items)
|
||||
@@ -78,10 +79,13 @@ namespace Aberwyn.Controllers
|
||||
[HttpGet("byname/{name}")]
|
||||
public async Task<IActionResult> GetBudgetByName(string name)
|
||||
{
|
||||
var period = await _context.BudgetPeriods
|
||||
var period = _context.BudgetPeriods
|
||||
.Include(p => p.Categories)
|
||||
.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)
|
||||
{
|
||||
@@ -363,6 +367,119 @@ namespace Aberwyn.Controllers
|
||||
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}")]
|
||||
|
||||
@@ -13,6 +13,11 @@ namespace Aberwyn.Controllers
|
||||
ViewBag.Month = month;
|
||||
return View();
|
||||
}
|
||||
[Route("budget/list")]
|
||||
public IActionResult List()
|
||||
{
|
||||
return View();
|
||||
}
|
||||
|
||||
[Route("budget/{name}")]
|
||||
public IActionResult Index(string name)
|
||||
|
||||
@@ -488,6 +488,34 @@ namespace Aberwyn.Controllers
|
||||
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 });
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -59,5 +59,8 @@ namespace Aberwyn.Controllers
|
||||
TempData["Success"] = "Beställningen har lagts!";
|
||||
return RedirectToAction("Order");
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,6 +40,7 @@ namespace Aberwyn.Data
|
||||
}
|
||||
|
||||
|
||||
public DbSet<DoughPlan> DoughPlans { get; set; }
|
||||
|
||||
public DbSet<BudgetPeriod> BudgetPeriods { get; set; }
|
||||
public DbSet<BudgetCategory> BudgetCategories { get; set; }
|
||||
|
||||
1304
Aberwyn/Migrations/20250903130637_AddDoughPlans.Designer.cs
generated
Normal file
1304
Aberwyn/Migrations/20250903130637_AddDoughPlans.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
44
Aberwyn/Migrations/20250903130637_AddDoughPlans.cs
Normal file
44
Aberwyn/Migrations/20250903130637_AddDoughPlans.cs
Normal 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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -243,6 +243,48 @@ namespace Aberwyn.Migrations
|
||||
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 =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
|
||||
@@ -26,5 +26,22 @@ namespace Aberwyn.Models
|
||||
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; }
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -5,84 +5,95 @@
|
||||
ViewData["Title"] = "Budget";
|
||||
}
|
||||
|
||||
<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="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 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="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);">
|
||||
<select ng-model="tempMonth" ng-options="month for month in monthNames" style="margin-bottom: 8px;"></select><br>
|
||||
<input type="number" ng-model="tempYear" placeholder="År" style="width: 100%; margin-bottom: 8px;" />
|
||||
<button class="nav-button" ng-click="applyMonthSelection()">Välj</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="month-nav-bar" ng-if="!budget.name" style="display: flex; align-items: center; gap: 10px; position: relative;">
|
||||
<a href="/budget/list" class="nav-button" style="margin-right: 10px;">
|
||||
Lista
|
||||
</a>
|
||||
<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="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 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 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);">
|
||||
<select ng-model="tempMonth" ng-options="month for month in monthNames" style="margin-bottom: 8px;"></select><br>
|
||||
<input type="number" ng-model="tempYear" placeholder="År" style="width: 100%; margin-bottom: 8px;" />
|
||||
<button class="nav-button" ng-click="applyMonthSelection()">Välj</button>
|
||||
</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">
|
||||
<!-- Vänster: Summering -->
|
||||
<div class="budget-summary-box compact">
|
||||
<h3>Sammanställning</h3>
|
||||
<ul class="summary-list">
|
||||
<li><span>💰 Inkomst:</span> {{ getTotalIncome() | number:0 }} kr</li>
|
||||
<li><span>💸 Utgift:</span> {{ getTotalExpense() | number:0 }} kr</li>
|
||||
<li><span>🏦 Sparande:</span> {{ getTotalSaving() | number:0 }} kr</li>
|
||||
<p>
|
||||
📈 Kvar:
|
||||
<span class="highlight leftover" ng-class="{'plus': getLeftover() >= 0, 'minus': getLeftover() < 0}">
|
||||
{{ getLeftover() | number:0 }} kr
|
||||
</span>
|
||||
<div class="budget-overview-row" ng-if="budget && budget.categories.length > 0">
|
||||
<!-- Vänster: Summering -->
|
||||
<div class="budget-summary-box compact">
|
||||
<h3>Sammanställning</h3>
|
||||
<ul class="summary-list">
|
||||
<li><span>💰 Inkomst:</span> {{ getTotalIncome() | number:0 }} kr</li>
|
||||
<li><span>💸 Utgift:</span> {{ getTotalExpense() | number:0 }} kr</li>
|
||||
<li><span>🏦 Sparande:</span> {{ getTotalSaving() | number:0 }} kr</li>
|
||||
<p>
|
||||
📈 Kvar:
|
||||
<span class="highlight leftover" ng-class="{'plus': getLeftover() >= 0, 'minus': getLeftover() < 0}">
|
||||
{{ getLeftover() | number:0 }} kr
|
||||
</span>
|
||||
</p>
|
||||
|
||||
</ul>
|
||||
<hr>
|
||||
<ul class="category-summary-list">
|
||||
<li ng-repeat="cat in budget.categories">
|
||||
<span style="color: {{ cat.color }}">{{ cat.name }}</span>
|
||||
<span>{{ getCategorySum(cat) | number:0 }} kr</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- Höger: Diagramväxling -->
|
||||
<div class="budget-chart-box compact">
|
||||
<div class="chart-row">
|
||||
<div class="chart-legend">
|
||||
<h4>Utgifter</h4>
|
||||
|
||||
<p style="font-size: 12px; margin: 4px 0 10px;">
|
||||
Totalt: <strong>{{ getTotalExpense() | number:0 }} kr</strong>
|
||||
</p>
|
||||
|
||||
</ul>
|
||||
<hr>
|
||||
<ul class="category-summary-list">
|
||||
<li ng-repeat="cat in budget.categories">
|
||||
<span style="color: {{ cat.color }}">{{ cat.name }}</span>
|
||||
<span>{{ getCategorySum(cat) | number:0 }} kr</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- Höger: Diagramväxling -->
|
||||
<div class="budget-chart-box compact">
|
||||
<div class="chart-row">
|
||||
<div class="chart-legend">
|
||||
<h4>Utgifter</h4>
|
||||
|
||||
<p style="font-size: 12px; margin: 4px 0 10px;">
|
||||
Totalt: <strong>{{ getTotalExpense() | number:0 }} kr</strong>
|
||||
</p>
|
||||
|
||||
<p ng-if="topExpenseCategory" style="font-size: 12px; margin-bottom: 12px;">
|
||||
Största kategori: <br>
|
||||
<strong>{{ topExpenseCategory.name }}</strong> ({{ topExpenseCategory.percent | number:1 }}%)
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="chart-area">
|
||||
<canvas id="expenseChart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
<p ng-if="topExpenseCategory" style="font-size: 12px; margin-bottom: 12px;">
|
||||
Största kategori: <br>
|
||||
<strong>{{ topExpenseCategory.name }}</strong> ({{ topExpenseCategory.percent | number:1 }}%)
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="chart-area">
|
||||
<canvas id="expenseChart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div class="budget-grid" ng-if="budget && budget.categories">
|
||||
|
||||
120
Aberwyn/Views/Budget/List.cshtml
Normal file
120
Aberwyn/Views/Budget/List.cshtml
Normal 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>
|
||||
144
Aberwyn/Views/FoodMenu/Calculator.cshtml
Normal file
144
Aberwyn/Views/FoodMenu/Calculator.cshtml
Normal 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>
|
||||
161
Aberwyn/wwwroot/css/budget-list.css
Normal file
161
Aberwyn/wwwroot/css/budget-list.css
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -97,9 +97,10 @@ body {
|
||||
|
||||
.budget-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
||||
gap:10px;
|
||||
grid-template-columns: repeat(auto-fit, minmax(220px, 300px));
|
||||
gap: 10px;
|
||||
align-items: stretch;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
25
Aberwyn/wwwroot/css/pizzacalculator.css
Normal file
25
Aberwyn/wwwroot/css/pizzacalculator.css
Normal 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);
|
||||
}
|
||||
28
Aberwyn/wwwroot/js/budget-list.js
Normal file
28
Aberwyn/wwwroot/js/budget-list.js
Normal 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;
|
||||
});
|
||||
});
|
||||
@@ -148,7 +148,7 @@ app.controller('BudgetController', function ($scope, $http) {
|
||||
|
||||
let url = "";
|
||||
if (useName) {
|
||||
url = `/api/budget/byname/${initialName}`;
|
||||
url = `/api/budget/byname/${encodeURIComponent(initialName)}`;
|
||||
} else {
|
||||
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.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 () {
|
||||
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) {
|
||||
const definitionName = cat.newItemDefinition?.trim();
|
||||
const label = cat.newItemLabel?.trim();
|
||||
|
||||
Reference in New Issue
Block a user