mirror of
https://github.com/eitchtee/WYGIWYH.git
synced 2026-11-13 12:23:44 +01:00
Consolidated rows are collapsible and remember their state per currency in localStorage. Share percentages render as badges on a shared subgrid column so they line up regardless of amount width. Currency and account menus drop their extra padding to align with the card title.
530 lines
25 KiB
HTML
530 lines
25 KiB
HTML
{% extends "layouts/base.html" %}
|
|
{% load currency_display %}
|
|
{% load crispy_forms_tags %}
|
|
{% load i18n %}
|
|
{% load month_name %}
|
|
{% load static %}
|
|
{% block title %}
|
|
{% if type == "current" %}
|
|
{% translate "Current Net Worth" %}
|
|
{% else %}
|
|
{% translate "Projected Net Worth" %}
|
|
{% endif %}
|
|
{% endblock title %}
|
|
{% block content %}
|
|
<div hx-trigger="every 60m, updated from:window"
|
|
hx-include="#view-type"
|
|
class="show-loading"
|
|
hx-get=""
|
|
hx-target="body">
|
|
<div class="h-full text-center mb-4 w-full">
|
|
<div class="tabs tabs-box mx-auto w-fit"
|
|
id="view-type"
|
|
_="on change trigger updated">
|
|
|
|
<label class="tab">
|
|
<input type="radio"
|
|
name="view_type"
|
|
class="tab"
|
|
aria-label="{% trans "Current" %}"
|
|
value="current"
|
|
{% if type == "current" %}checked{% endif %}/>
|
|
<i class="fa-solid fa-sack-dollar fa-fw me-2"></i>
|
|
{% trans "Current" %}
|
|
</label>
|
|
<label class="tab">
|
|
<input type="radio"
|
|
name="view_type"
|
|
class="tab"
|
|
aria-label="{% trans "Projected" %}"
|
|
value="projected"
|
|
{% if type == "projected" %}checked{% endif %}/>
|
|
<i class="fa-solid fa-rocket fa-fw me-2"></i>
|
|
{% trans "Projected" %}
|
|
</label>
|
|
</div>
|
|
</div>
|
|
<div class="container px-md-3 py-3"
|
|
_="init call initializeAccountChart() then initializeCurrencyChart() then initializeMonthlyDifferenceChart() end">
|
|
<div class="row gap-y-3">
|
|
<div class="col lg:col-5">
|
|
<div>
|
|
<c-ui.info-card color="yellow" icon="fa-solid fa-coins" title="{% trans "By currency" %}"
|
|
title_css_classes="cursor-pointer" _="on click showAllDatasetsCurrency()">
|
|
<ul class="menu bg-base-100 rounded-box p-0 -mx-3 w-[calc(100%+1.5rem)]">
|
|
{% for currency in currency_net_worth.values %}
|
|
<li>
|
|
{% if currency.consolidated and currency.consolidated.total_final != currency.total_final %}
|
|
<a class="cursor-pointer select-auto flex justify-between items-center w-full"
|
|
_="on click showOnlyCurrencyDataset('{{ currency.currency.name }}', '{{ currency.currency.name }}')">
|
|
<span
|
|
class="currency-name text-start font-mono shrink text-ellipsis">{{ currency.currency.name }}</span>
|
|
<span class="text-end shrink-0">
|
|
<div>
|
|
<c-amount.display :amount="currency.total_final" :prefix="currency.currency.prefix"
|
|
:suffix="currency.currency.suffix"
|
|
:decimal_places="currency.currency.decimal_places"
|
|
color="{% if currency.total_final > 0 %}green{% elif currency.total_final < 0 %}red{% endif %}"
|
|
text-end></c-amount.display>
|
|
</div>
|
|
{% if currency.exchanged and currency.exchanged.total_final %}
|
|
<div>
|
|
<c-amount.display :amount="currency.exchanged.total_final"
|
|
:prefix="currency.exchanged.currency.prefix"
|
|
:suffix="currency.exchanged.currency.suffix"
|
|
:decimal_places="currency.exchanged.currency.decimal_places" text-end
|
|
color="grey"></c-amount.display>
|
|
</div>
|
|
{% endif %}
|
|
</span>
|
|
</a>
|
|
<ul>
|
|
<li>
|
|
{# Collapsed by default; each currency's open state is remembered per browser #}
|
|
<details _="init if localStorage.getItem('net-worth-consolidated-{{ currency.currency.id }}') is 'open' set my.open to true end
|
|
on toggle if my.open call localStorage.setItem('net-worth-consolidated-{{ currency.currency.id }}', 'open')
|
|
else call localStorage.setItem('net-worth-consolidated-{{ currency.currency.id }}', 'closed') end
|
|
catch e log e end">
|
|
<summary class="cursor-pointer text-base-content/60 select-auto flex"
|
|
_="on click showOnlyCurrencyDataset('{{ currency.currency.name }} {% trans "Consolidated" %}', '{{ currency.currency.name }}')">
|
|
<span class="flex justify-between items-center w-full">
|
|
<span class="text-start shrink">{% trans "Consolidated" %}</span>
|
|
<span class="text-end shrink-0">
|
|
<c-amount.display :amount="currency.consolidated.total_final"
|
|
:prefix="currency.consolidated.currency.prefix"
|
|
:suffix="currency.consolidated.currency.suffix"
|
|
:decimal_places="currency.consolidated.currency.decimal_places"
|
|
color="{% if currency.consolidated.total_final > 0 %}green{% elif currency.consolidated.total_final < 0 %}red{% endif %}"
|
|
text-end></c-amount.display>
|
|
</span>
|
|
</span>
|
|
</summary>
|
|
{# Rows share the ul's columns via subgrid so badges line up regardless of amount width #}
|
|
<ul class="grid grid-cols-[minmax(0,1fr)_auto_auto]">
|
|
{% for component in currency.consolidated.components %}
|
|
<li class="col-span-3 grid grid-cols-subgrid">
|
|
<a class="col-span-3 grid grid-cols-subgrid items-center cursor-pointer text-base-content/60 select-auto text-sm"
|
|
_="on click showOnlyCurrencyDataset('{{ component.currency.name }}', '{{ component.currency.name }}')">
|
|
<span class="currency-name text-start font-mono truncate">{{ component.currency.name }}</span>
|
|
<span class="justify-self-start">
|
|
{% if component.share is not None %}
|
|
<span class="badge badge-sm badge-ghost tabular-nums">{{ component.share|floatformat:1 }}%</span>
|
|
{% endif %}
|
|
</span>
|
|
<span class="text-end whitespace-nowrap">
|
|
<c-amount.display :amount="component.total_final"
|
|
:prefix="currency.consolidated.currency.prefix"
|
|
:suffix="currency.consolidated.currency.suffix"
|
|
:decimal_places="currency.consolidated.currency.decimal_places"
|
|
color="grey"
|
|
divless></c-amount.display>
|
|
</span>
|
|
</a>
|
|
</li>
|
|
{% endfor %}
|
|
</ul>
|
|
</details>
|
|
</li>
|
|
</ul>
|
|
{% else %}
|
|
<a class="cursor-pointer select-auto flex justify-between items-center w-full"
|
|
_="on click showOnlyCurrencyDataset('{{ currency.currency.name }}', '{{ currency.currency.name }}')">
|
|
<span class="currency-name text-start font-mono shrink">{{ currency.currency.name }}</span>
|
|
<span class="text-end shrink-0">
|
|
<div>
|
|
<c-amount.display :amount="currency.total_final" :prefix="currency.currency.prefix"
|
|
:suffix="currency.currency.suffix"
|
|
:decimal_places="currency.currency.decimal_places"
|
|
color="{% if currency.total_final > 0 %}green{% elif currency.total_final < 0 %}red{% endif %}"
|
|
text-end></c-amount.display>
|
|
</div>
|
|
{% if currency.exchanged and currency.exchanged.total_final %}
|
|
<div>
|
|
<c-amount.display :amount="currency.exchanged.total_final"
|
|
:prefix="currency.exchanged.currency.prefix"
|
|
:suffix="currency.exchanged.currency.suffix"
|
|
:decimal_places="currency.exchanged.currency.decimal_places" text-end
|
|
color="grey"></c-amount.display>
|
|
</div>
|
|
{% endif %}
|
|
</span>
|
|
</a>
|
|
{% endif %}
|
|
</li>
|
|
{% endfor %}
|
|
</ul>
|
|
</c-ui.info-card>
|
|
</div>
|
|
</div>
|
|
<div class="col lg:col-7">
|
|
<div class="card bg-base-100">
|
|
<div class="card-body">
|
|
<div role="tablist" class="tabs tabs-border w-full" id="myTab">
|
|
<input type="radio"
|
|
name="networth_tabs"
|
|
role="tab"
|
|
class="tab"
|
|
aria-label="{% trans 'Evolution' %}"
|
|
id="tab-evolution"
|
|
checked="checked"/>
|
|
<div role="tabpanel" class="tab-content p-4" id="evolution-tab-pane">
|
|
<div class="chart-container relative min-h-[80vh] md:min-h-[40vh] h-full w-full">
|
|
<canvas id="currencyBalanceChart"></canvas>
|
|
</div>
|
|
</div>
|
|
<input type="radio"
|
|
name="networth_tabs"
|
|
role="tab"
|
|
class="tab"
|
|
aria-label="{% trans 'Difference' %}"
|
|
id="tab-diff"/>
|
|
<div role="tabpanel" class="tab-content p-4" id="diff-tab-pane">
|
|
<div class="chart-container relative min-h-[80vh] md:min-h-[40vh] h-full w-full">
|
|
<canvas id="monthlyDifferenceChart"></canvas>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<hr class="hr my-6"/>
|
|
<div class="row gap-y-3">
|
|
<div class="col lg:col-5">
|
|
<div>
|
|
<c-ui.info-card color="blue" icon="fa-solid fa-wallet" title="{% trans "By account" %}"
|
|
title_css_classes="cursor-pointer" _="on click showAllDatasetsAccount()">
|
|
<ul class="menu bg-base-100 rounded-box p-0 -mx-3 w-[calc(100%+1.5rem)]">
|
|
{% regroup account_net_worth.values by account.group as account_data %}
|
|
{% for data in account_data %}
|
|
{% if data.grouper %}
|
|
<li>
|
|
<details open>
|
|
<summary class="select-auto">
|
|
<span class="badge badge-primary">{{ data.grouper }}</span>
|
|
</summary>
|
|
<ul>
|
|
{% for account in data.list %}
|
|
<li>
|
|
<a class="cursor-pointer select-auto flex justify-between items-center w-full"
|
|
_="on click showOnlyAccountDataset('{{ account.account.name }}')">
|
|
<span
|
|
class="account-name text-start font-mono shrink text-ellipsis">{{ account.account.name }}</span>
|
|
<span class="text-end shrink-0">
|
|
<div>
|
|
<c-amount.display :amount="account.total_final" :prefix="account.currency.prefix"
|
|
:suffix="account.currency.suffix"
|
|
:decimal_places="account.currency.decimal_places"
|
|
color="{% if account.total_final > 0 %}green{% elif account.total_final < 0 %}red{% endif %}"></c-amount.display>
|
|
</div>
|
|
{% if account.exchanged and account.exchanged.total_final %}
|
|
<div>
|
|
<c-amount.display :amount="account.exchanged.total_final"
|
|
:prefix="account.exchanged.currency.prefix"
|
|
:suffix="account.exchanged.currency.suffix"
|
|
:decimal_places="account.exchanged.currency.decimal_places"
|
|
color="grey" text-end></c-amount.display>
|
|
</div>
|
|
{% endif %}
|
|
</span>
|
|
</a>
|
|
</li>
|
|
{% endfor %}
|
|
</ul>
|
|
</details>
|
|
</li>
|
|
{% else %}
|
|
{% for account in data.list %}
|
|
<li>
|
|
<a class="cursor-pointer flex select-auto justify-between items-center w-full"
|
|
_="on click showOnlyAccountDataset('{{ account.account.name }}')">
|
|
<span class="account-name text-start font-mono shrink">{{ account.account.name }}</span>
|
|
<span class="text-end shrink-0">
|
|
<div>
|
|
<c-amount.display :amount="account.total_final" :prefix="account.currency.prefix"
|
|
:suffix="account.currency.suffix"
|
|
:decimal_places="account.currency.decimal_places"
|
|
color="{% if account.total_final > 0 %}green{% elif account.total_final < 0 %}red{% endif %}"></c-amount.display>
|
|
</div>
|
|
{% if account.exchanged and account.exchanged.total_final %}
|
|
<div>
|
|
<c-amount.display :amount="account.exchanged.total_final"
|
|
:prefix="account.exchanged.currency.prefix"
|
|
:suffix="account.exchanged.currency.suffix"
|
|
:decimal_places="account.exchanged.currency.decimal_places" color="grey"
|
|
text-end></c-amount.display>
|
|
</div>
|
|
{% endif %}
|
|
</span>
|
|
</a>
|
|
</li>
|
|
{% endfor %}
|
|
{% endif %}
|
|
{% endfor %}
|
|
</ul>
|
|
</c-ui.info-card>
|
|
</div>
|
|
</div>
|
|
<div class="col lg:col-7">
|
|
<div class="chart-container relative min-h-[80vh] md:min-h-[40vh] h-full card bg-base-100">
|
|
<div class="card-body">
|
|
<canvas id="accountBalanceChart"></canvas>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<script>
|
|
var currencyChart;
|
|
|
|
function initializeCurrencyChart() {
|
|
// Destroy existing chart if it exists
|
|
if (currencyChart) {
|
|
currencyChart.destroy();
|
|
}
|
|
|
|
var chartData = JSON.parse('{{ chart_data_currency_json|safe }}');
|
|
var currencies = {{ currencies|safe }};
|
|
var ctx = document.getElementById('currencyBalanceChart').getContext('2d');
|
|
|
|
currencyChart = new Chart(ctx, {
|
|
type: 'line',
|
|
data: chartData,
|
|
options: {
|
|
maintainAspectRatio: false,
|
|
responsive: true,
|
|
interaction: {
|
|
mode: 'index',
|
|
intersect: false,
|
|
},
|
|
plugins: {
|
|
title: {
|
|
display: true,
|
|
text: '{% translate 'Evolution by currency' %}'
|
|
},
|
|
tooltip: {
|
|
mode: 'index',
|
|
intersect: false
|
|
}
|
|
},
|
|
scales: {
|
|
x: {
|
|
display: true,
|
|
title: {
|
|
display: false,
|
|
}
|
|
},
|
|
...Object.fromEntries(currencies.map((currency, i) => [
|
|
`y${i}`,
|
|
{
|
|
type: 'linear',
|
|
display: true,
|
|
grid: {
|
|
drawOnChartArea: i === 0,
|
|
drawTicks: false,
|
|
},
|
|
ticks: {
|
|
display: false,
|
|
format: {maximumFractionDigits: 40, minimumFractionDigits: 0}
|
|
},
|
|
border: {
|
|
display: false
|
|
}
|
|
}
|
|
]))
|
|
}
|
|
}
|
|
});
|
|
|
|
for (const dataset of currencyChart.data.datasets) {
|
|
if (!dataset.colorSource) continue;
|
|
const source = currencyChart.data.datasets.find(
|
|
candidate => candidate.label === dataset.colorSource
|
|
);
|
|
dataset.borderColor = source.borderColor;
|
|
dataset.backgroundColor = source.backgroundColor;
|
|
}
|
|
currencyChart.update('none');
|
|
}
|
|
</script>
|
|
<script id="accountBalanceChartScript">
|
|
var accountChart;
|
|
|
|
function initializeAccountChart() {
|
|
// Destroy existing chart if it exists
|
|
if (accountChart) {
|
|
accountChart.destroy();
|
|
}
|
|
|
|
var chartData = JSON.parse('{{ chart_data_accounts_json|safe }}');
|
|
var accounts = {{ accounts|safe }};
|
|
var ctx = document.getElementById('accountBalanceChart').getContext('2d');
|
|
|
|
accountChart = new Chart(ctx, {
|
|
type: 'line',
|
|
data: chartData,
|
|
options: {
|
|
maintainAspectRatio: false,
|
|
responsive: true,
|
|
interaction: {
|
|
mode: 'index',
|
|
intersect: false,
|
|
},
|
|
stacked: false,
|
|
plugins: {
|
|
title: {
|
|
display: true,
|
|
text: '{% translate "Evolution by account" %}'
|
|
},
|
|
tooltip: {
|
|
mode: 'index',
|
|
intersect: false
|
|
}
|
|
},
|
|
scales: {
|
|
x: {
|
|
display: true,
|
|
title: {
|
|
display: false,
|
|
}
|
|
},
|
|
...Object.fromEntries(accounts.map((account, i) => [
|
|
`y-axis-${i}`,
|
|
{
|
|
type: 'linear',
|
|
display: true,
|
|
position: i % 2 === 0 ? 'left' : 'right',
|
|
grid: {
|
|
drawOnChartArea: i === 0,
|
|
drawTicks: false,
|
|
},
|
|
ticks: {
|
|
display: false,
|
|
format: {maximumFractionDigits: 40, minimumFractionDigits: 0}
|
|
},
|
|
border: {
|
|
display: false
|
|
}
|
|
}
|
|
]))
|
|
}
|
|
}
|
|
});
|
|
}
|
|
</script>
|
|
<script id="monthlyDifferenceChartScript">
|
|
var monthlyDifferenceChart;
|
|
|
|
function initializeMonthlyDifferenceChart() {
|
|
if (monthlyDifferenceChart) {
|
|
monthlyDifferenceChart.destroy();
|
|
}
|
|
|
|
var chartData = JSON.parse('{{ chart_data_monthly_difference_json|safe }}');
|
|
var ctx = document.getElementById('monthlyDifferenceChart').getContext('2d');
|
|
|
|
monthlyDifferenceChart = new Chart(ctx, {
|
|
type: 'bar',
|
|
data: chartData,
|
|
options: {
|
|
maintainAspectRatio: false,
|
|
responsive: true,
|
|
interaction: {
|
|
mode: 'index',
|
|
intersect: false,
|
|
},
|
|
plugins: {
|
|
title: {
|
|
display: true,
|
|
text: '{% translate "Difference" %}'
|
|
},
|
|
tooltip: {
|
|
mode: 'index',
|
|
intersect: false
|
|
}
|
|
},
|
|
scales: {
|
|
x: {
|
|
stacked: true,
|
|
},
|
|
y: {
|
|
stacked: true,
|
|
border: {
|
|
display: true,
|
|
},
|
|
grid: {
|
|
drawTicks: false,
|
|
},
|
|
ticks: {
|
|
display: false,
|
|
format: {
|
|
maximumFractionDigits: 40,
|
|
minimumFractionDigits: 0
|
|
}
|
|
},
|
|
},
|
|
y1: {
|
|
position: 'right',
|
|
display: true,
|
|
grid: {
|
|
drawOnChartArea: false,
|
|
drawTicks: false,
|
|
},
|
|
border: {
|
|
display: true,
|
|
},
|
|
ticks: {
|
|
display: false,
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
</script>
|
|
<script type="text/hyperscript">
|
|
def showOnlyAccountDataset(datasetName)
|
|
for dataset in accountChart.data.datasets
|
|
set isMatch to dataset.label is datasetName
|
|
call accountChart.setDatasetVisibility(accountChart.data.datasets.indexOf(dataset), isMatch)
|
|
end
|
|
call accountChart.update()
|
|
end
|
|
|
|
def showOnlyCurrencyDataset(datasetName, differenceDatasetName)
|
|
for dataset in currencyChart.data.datasets
|
|
set isMatch to dataset.label is datasetName
|
|
call currencyChart.setDatasetVisibility(currencyChart.data.datasets.indexOf(dataset), isMatch)
|
|
end
|
|
call currencyChart.update()
|
|
|
|
for dataset in monthlyDifferenceChart.data.datasets
|
|
set isMatch to dataset.label is differenceDatasetName
|
|
call monthlyDifferenceChart.setDatasetVisibility(monthlyDifferenceChart.data.datasets.indexOf(dataset), isMatch)
|
|
end
|
|
call monthlyDifferenceChart.update()
|
|
end
|
|
|
|
def showAllDatasetsAccount()
|
|
for dataset in accountChart.data.datasets
|
|
call accountChart.setDatasetVisibility(accountChart.data.datasets.indexOf(dataset), true)
|
|
end
|
|
call accountChart.update()
|
|
end
|
|
|
|
def showAllDatasetsCurrency()
|
|
for dataset in currencyChart.data.datasets
|
|
call currencyChart.setDatasetVisibility(currencyChart.data.datasets.indexOf(dataset), true)
|
|
end
|
|
call currencyChart.update()
|
|
|
|
for dataset in monthlyDifferenceChart.data.datasets
|
|
call monthlyDifferenceChart.setDatasetVisibility(monthlyDifferenceChart.data.datasets.indexOf(dataset), true)
|
|
end
|
|
call monthlyDifferenceChart.update()
|
|
end
|
|
</script>
|
|
</div>
|
|
<c-ui.transactions_fab></c-ui.transactions_fab>
|
|
{% endblock content %}
|