Files
kakebo/templates/pages/yearly/year.html
Andros Fenollosa a7dd758d44 Add planned expenses to Year with month breakdown
- PlannedExpense model: year, month, concept, amount
- CRUD via LiveView in Year page (add form + delete)
- Tables grouped by month with totals
- Variable expenses line in Year charts includes planned expenses
- Month page shows read-only planned expenses table for the month
- Month end calculations include planned expenses in totals
2026-03-22 10:28:16 +01:00

233 lines
9.1 KiB
HTML

{% extends "layouts/base.html" %}
{% load money %}
{% block title %}Kakebo - Year {{ year }}{% endblock %}
{% block content %}
<div class="px-4 py-6 max-w-2xl mx-auto">
<h1 class="text-2xl font-bold mb-4">Year</h1>
<div class="flex w-full items-center justify-between mb-8 py-3">
{% if has_prev_year %}
<a data-liveview-function="navigate"
data-action="click->page#run"
data-data-url="/year/?year={{ prev_year }}"
class="btn btn-ghost btn-sm">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"/>
</svg>
</a>
{% else %}
<span class="btn btn-ghost btn-sm btn-disabled opacity-0">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"/>
</svg>
</span>
{% endif %}
<span class="text-base-content/70 font-medium">{{ year }}</span>
{% if is_current_year %}
<span class="btn btn-ghost btn-sm btn-disabled opacity-0">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>
</svg>
</span>
{% else %}
<a data-liveview-function="navigate"
data-action="click->page#run"
data-data-url="/year/?year={{ next_year }}"
class="btn btn-ghost btn-sm">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>
</svg>
</a>
{% endif %}
</div>
{# Annual summary: Income vs Fixed expenses #}
<h2 class="text-xl font-semibold mb-4">Annual summary</h2>
<div class="card bg-base-100 shadow-sm mb-4">
<div class="card-body">
<div class="grid grid-cols-3 text-center gap-4 mb-4">
<div>
<p class="text-sm text-base-content/60">Total income</p>
<p class="text-lg font-bold">{{ total_income|money }} €</p>
</div>
<div>
<p class="text-sm text-base-content/60">Fixed expenses</p>
<p class="text-lg font-bold">{{ total_fe|money }} €</p>
</div>
<div>
<p class="text-sm text-base-content/60">Variable expenses</p>
<p class="text-lg font-bold">{{ total_expenses|money }} €</p>
</div>
</div>
<canvas id="chart-summary" height="200"></canvas>
</div>
</div>
{# Expenses by category #}
<h2 class="text-xl font-semibold mb-4">Expenses by category</h2>
<div class="card bg-base-100 shadow-sm mb-4">
<div class="card-body">
{% if category_totals %}
<div class="flex flex-col md:flex-row gap-6 items-center">
<div class="w-full md:w-1/2">
<canvas id="chart-pie" height="250"></canvas>
</div>
<div class="w-full md:w-1/2">
<div class="overflow-x-auto">
<table class="table table-sm">
<thead>
<tr>
<th>Category</th>
<th class="text-right">Total</th>
</tr>
</thead>
<tbody>
{% for cat in category_totals %}
<tr>
<td>{{ cat.name }}</td>
<td class="text-right">{{ cat.total|money }} €</td>
</tr>
{% endfor %}
</tbody>
<tfoot>
<tr class="font-bold">
<td>Total</td>
<td class="text-right">{{ total_expenses|money }} €</td>
</tr>
</tfoot>
</table>
</div>
</div>
</div>
{% else %}
<p class="text-base-content/50 text-center">No expenses this year</p>
{% endif %}
</div>
</div>
{# Monthly evolution per category #}
<h2 class="text-xl font-semibold mb-4">Monthly evolution</h2>
<div id="category-charts"></div>
{# Planned expenses #}
<h2 class="text-xl font-semibold mb-4">Planned expenses</h2>
<div id="planned-expenses">
{% include "pages/yearly/partials/planned_tables.html" %}
</div>
</div>
<script>
(function() {
var labels = {{ month_labels_json|safe }};
var incomeData = {{ income_by_month_json|safe }};
var feData = {{ fe_by_month_json|safe }};
var varExpData = {{ var_expenses_by_month_json|safe }};
var categoryTotals = {{ category_totals_json|safe }};
var categoryMonthly = {{ category_monthly_json|safe }};
var colors = [
'#f78ca4', '#6ec6ff', '#ffd54f', '#81c784',
'#ce93d8', '#ffab91', '#80deea', '#a5d6a7',
'#ef9a9a', '#90caf9'
];
var summaryCtx = document.getElementById('chart-summary');
if (summaryCtx) {
new Chart(summaryCtx, {
type: 'line',
data: {
labels: labels,
datasets: [
{
label: 'Income',
data: incomeData,
borderColor: '#81c784',
backgroundColor: '#81c78433',
fill: true,
tension: 0.3,
},
{
label: 'Fixed expenses',
data: feData,
borderColor: '#ef9a9a',
backgroundColor: '#ef9a9a33',
fill: true,
tension: 0.3,
},
{
label: 'Variable expenses',
data: varExpData,
borderColor: '#f78ca4',
backgroundColor: '#f78ca433',
fill: true,
tension: 0.3,
}
]
},
options: {
responsive: true,
plugins: { legend: { position: 'bottom' } },
scales: { y: { beginAtZero: true } }
}
});
}
var pieCtx = document.getElementById('chart-pie');
if (pieCtx && categoryTotals.length > 0) {
new Chart(pieCtx, {
type: 'doughnut',
data: {
labels: categoryTotals.map(function(c) { return c.name; }),
datasets: [{
data: categoryTotals.map(function(c) { return c.total; }),
backgroundColor: colors.slice(0, categoryTotals.length),
}]
},
options: {
responsive: true,
plugins: { legend: { position: 'bottom' } }
}
});
}
var chartsContainer = document.getElementById('category-charts');
if (chartsContainer && categoryMonthly.length > 0) {
categoryMonthly.forEach(function(cat, i) {
var card = document.createElement('div');
card.className = 'card bg-base-100 shadow-sm mb-4';
card.innerHTML = '<div class="card-body"><h3 class="card-title text-lg mb-2">' + cat.name + '</h3><canvas height="150"></canvas></div>';
chartsContainer.appendChild(card);
var canvas = card.querySelector('canvas');
new Chart(canvas, {
type: 'line',
data: {
labels: labels,
datasets: [{
label: cat.name,
data: cat.data,
borderColor: colors[i % colors.length],
backgroundColor: colors[i % colors.length] + '33',
fill: true,
tension: 0.3,
}]
},
options: {
responsive: true,
plugins: { legend: { display: false } },
scales: { y: { beginAtZero: true } }
}
});
});
} else if (chartsContainer) {
chartsContainer.innerHTML = '<div class="card bg-base-100 shadow-sm mb-4"><div class="card-body"><p class="text-base-content/50 text-center">No expenses this year</p></div></div>';
}
}());
</script>
{% endblock %}