Compare commits

..
12 changed files with 460 additions and 324 deletions
-3
View File
@@ -160,6 +160,3 @@ cython_debug/
# and can be added to the global gitignore or merged into this file. For a more nuclear # and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder. # option (not recommended) you can uncomment the following to ignore the entire idea folder.
.idea/ .idea/
postgres_data/
.prod.env
-1
View File
@@ -126,7 +126,6 @@ To create the first user, open the container's console using Unraid's UI, by cli
| variable | type | default | explanation | | variable | type | default | explanation |
|-------------------------------|-------------|-----------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| |-------------------------------|-------------|-----------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| INTERNAL_PORT | int | 8000 | The port on which the app listens on. Defaults to 8000 if not set. |
| DJANGO_ALLOWED_HOSTS | string | localhost 127.0.0.1 | A list of space separated domains and IPs representing the host/domain names that WYGIWYH site can serve. [Click here](https://docs.djangoproject.com/en/5.1/ref/settings/#allowed-hosts) for more details | | DJANGO_ALLOWED_HOSTS | string | localhost 127.0.0.1 | A list of space separated domains and IPs representing the host/domain names that WYGIWYH site can serve. [Click here](https://docs.djangoproject.com/en/5.1/ref/settings/#allowed-hosts) for more details |
| HTTPS_ENABLED | true\|false | false | Whether to use secure cookies. If this is set to true, the cookie will be marked as “secure”, which means browsers may ensure that the cookie is only sent under an HTTPS connection | | HTTPS_ENABLED | true\|false | false | Whether to use secure cookies. If this is set to true, the cookie will be marked as “secure”, which means browsers may ensure that the cookie is only sent under an HTTPS connection |
| URL | string | http://localhost http://127.0.0.1 | A list of space separated domains and IPs (with the protocol) representing the trusted origins for unsafe requests (e.g. POST). [Click here](https://docs.djangoproject.com/en/5.1/ref/settings/#csrf-trusted-origins ) for more details | | URL | string | http://localhost http://127.0.0.1 | A list of space separated domains and IPs (with the protocol) representing the trusted origins for unsafe requests (e.g. POST). [Click here](https://docs.djangoproject.com/en/5.1/ref/settings/#csrf-trusted-origins ) for more details |
@@ -182,3 +182,29 @@ def calculate_historical_account_balance(queryset):
historical_account_balance[date_filter(end_date, "b Y")] = month_data historical_account_balance[date_filter(end_date, "b Y")] = month_data
return historical_account_balance return historical_account_balance
def calculate_monthly_net_worth_difference(historical_net_worth):
diff_dict = OrderedDict()
if not historical_net_worth:
return diff_dict
# Get all currencies
currencies = set()
for data in historical_net_worth.values():
currencies.update(data.keys())
# Initialize prev_values for all currencies
prev_values = {currency: Decimal("0.00") for currency in currencies}
for month, values in historical_net_worth.items():
diff_values = {}
for currency in sorted(list(currencies)):
current_val = values.get(currency, Decimal("0.00"))
prev_val = prev_values.get(currency, Decimal("0.00"))
diff_values[currency] = current_val - prev_val
diff_dict[month] = diff_values
prev_values = values.copy()
return diff_dict
+34
View File
@@ -8,6 +8,7 @@ from django.views.decorators.http import require_http_methods
from apps.net_worth.utils.calculate_net_worth import ( from apps.net_worth.utils.calculate_net_worth import (
calculate_historical_currency_net_worth, calculate_historical_currency_net_worth,
calculate_historical_account_balance, calculate_historical_account_balance,
calculate_monthly_net_worth_difference,
) )
from apps.transactions.models import Transaction from apps.transactions.models import Transaction
from apps.transactions.utils.calculations import ( from apps.transactions.utils.calculations import (
@@ -96,6 +97,38 @@ def net_worth(request):
chart_data_currency_json = json.dumps(chart_data_currency, cls=DjangoJSONEncoder) chart_data_currency_json = json.dumps(chart_data_currency, cls=DjangoJSONEncoder)
monthly_difference_data = calculate_monthly_net_worth_difference(
historical_net_worth=historical_currency_net_worth
)
diff_labels = (
list(monthly_difference_data.keys()) if monthly_difference_data else []
)
diff_currencies = (
list(monthly_difference_data[diff_labels[0]].keys())
if monthly_difference_data and diff_labels
else []
)
diff_datasets = []
for i, currency in enumerate(diff_currencies):
data = [
float(month_data.get(currency, 0))
for month_data in monthly_difference_data.values()
]
diff_datasets.append(
{
"label": currency,
"data": data,
"borderWidth": 3,
}
)
chart_data_monthly_difference = {"labels": diff_labels, "datasets": diff_datasets}
chart_data_monthly_difference_json = json.dumps(
chart_data_monthly_difference, cls=DjangoJSONEncoder
)
historical_account_balance = calculate_historical_account_balance( historical_account_balance = calculate_historical_account_balance(
queryset=transactions_account_queryset queryset=transactions_account_queryset
) )
@@ -140,6 +173,7 @@ def net_worth(request):
"chart_data_accounts_json": chart_data_accounts_json, "chart_data_accounts_json": chart_data_accounts_json,
"accounts": accounts, "accounts": accounts,
"type": view_type, "type": view_type,
"chart_data_monthly_difference_json": chart_data_monthly_difference_json,
}, },
) )
+1 -1
View File
@@ -3,7 +3,7 @@
{% if icon %}<i class="{{ icon }}"></i>{% else %}<span class="fw-bold">{{ title.0 }}</span>{% endif %} {% if icon %}<i class="{{ icon }}"></i>{% else %}<span class="fw-bold">{{ title.0 }}</span>{% endif %}
</div> </div>
<div class="card-body"> <div class="card-body">
<h5 class="tw:text-{{ color }}-400 fw-bold tw:mr-[50px]" {{ attrs }}>{{ title }}{% if help_text %}<c-ui.help-icon :content="help_text" icon=""></c-ui.help-icon>{% endif %}</h5> <h5 class="tw:text-{{ color }}-400 fw-bold tw:mr-[50px] {{ title_css_classes }}" {{ attrs }}>{{ title }}{% if help_text %}<c-ui.help-icon :content="help_text" icon=""></c-ui.help-icon>{% endif %}</h5>
{{ slot }} {{ slot }}
</div> </div>
</div> </div>
@@ -354,6 +354,7 @@
display: false, display: false,
}, },
ticks: { ticks: {
display: false,
format: { maximumFractionDigits: 40, minimumFractionDigits: 1 } format: { maximumFractionDigits: 40, minimumFractionDigits: 1 }
} }
}, },
@@ -369,6 +370,7 @@
display: false, display: false,
}, },
ticks: { ticks: {
display: false,
format: { maximumFractionDigits: 40, minimumFractionDigits: 1 } format: { maximumFractionDigits: 40, minimumFractionDigits: 1 }
} }
} }
+90 -6
View File
@@ -9,7 +9,8 @@
{% block title %}{% if type == "current" %}{% translate 'Current Net Worth' %}{% else %}{% translate 'Projected Net Worth' %}{% endif %}{% endblock %} {% block title %}{% if type == "current" %}{% translate 'Current Net Worth' %}{% else %}{% translate 'Projected Net Worth' %}{% endif %}{% endblock %}
{% block content %} {% block content %}
<div hx-trigger="every 60m, updated from:window" hx-include="#view-type" class="show-loading" hx-get="" hx-target="body"> <div hx-trigger="every 60m, updated from:window" hx-include="#view-type" class="show-loading" hx-get=""
hx-target="body">
<div class="h-100 text-center mb-4 pt-2"> <div class="h-100 text-center mb-4 pt-2">
<div class="btn-group gap-3" role="group" id="view-type" _="on change trigger updated"> <div class="btn-group gap-3" role="group" id="view-type" _="on change trigger updated">
<input type="radio" class="btn-check" <input type="radio" class="btn-check"
@@ -32,17 +33,19 @@
class="fa-solid fa-rocket fa-fw me-2"></i>{% trans 'Projected' %}</label> class="fa-solid fa-rocket fa-fw me-2"></i>{% trans 'Projected' %}</label>
</div> </div>
</div> </div>
<div class="container px-md-3 py-3" _="init call initializeAccountChart() then initializeCurrencyChart() end"> <div class="container px-md-3 py-3"
_="init call initializeAccountChart() then initializeCurrencyChart() then initializeMonthlyDifferenceChart() end">
<div class="row gx-xl-4 gy-3 mb-4"> <div class="row gx-xl-4 gy-3 mb-4">
<div class="col-12 col-xl-5"> <div class="col-12 col-xl-5">
<div class="row row-cols-1 g-4"> <div class="row row-cols-1 g-4">
<div class="col"> <div class="col">
<c-ui.info-card color="yellow" icon="fa-solid fa-coins" title="{% trans 'By currency' %}" <c-ui.info-card color="yellow" icon="fa-solid fa-coins" title="{% trans 'By currency' %}"
title_css_classes="tw:cursor-pointer"
_="on click showAllDatasetsCurrency()"> _="on click showAllDatasetsCurrency()">
{% for currency in currency_net_worth.values %} {% for currency in currency_net_worth.values %}
<div class="d-flex justify-content-between mt-2"> <div class="d-flex justify-content-between mt-2">
<div class="d-flex align-items-baseline w-100"> <div class="d-flex align-items-baseline w-100">
<div class="currency-name text-start font-monospace tw:text-gray-300" <div class="currency-name text-start font-monospace tw:text-gray-300 tw:cursor-pointer"
_="on click showOnlyCurrencyDataset('{{ currency.currency.name }}')"> _="on click showOnlyCurrencyDataset('{{ currency.currency.name }}')">
{{ currency.currency.name }} {{ currency.currency.name }}
</div> </div>
@@ -91,10 +94,31 @@
</div> </div>
</div> </div>
<div class="col-12 col-xl-7"> <div class="col-12 col-xl-7">
<div class="chart-container position-relative tw:min-h-[40vh] tw:h-full"> <ul class="nav nav-tabs" id="myTab" role="tablist">
<li class="nav-item" role="presentation">
<button class="nav-link active" id="evolution-tab" data-bs-toggle="tab"
data-bs-target="#evolution-tab-pane" type="button" role="tab" aria-controls="evolution-tab-pane"
aria-selected="true">{% trans 'Evolution' %}</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="diff-tab" data-bs-toggle="tab" data-bs-target="#diff-tab-pane" type="button"
role="tab" aria-controls="diff-tab-pane" aria-selected="false">{% trans 'Difference' %}</button>
</li>
</ul>
<div class="tab-content" id="myTabContent">
<div class="tab-pane fade show active" id="evolution-tab-pane" role="tabpanel"
aria-labelledby="evolution-tab" tabindex="0">
<div class="chart-container position-relative tw:min-h-[40vh] tw:h-full tw:w-full">
<canvas id="currencyBalanceChart"></canvas> <canvas id="currencyBalanceChart"></canvas>
</div> </div>
</div> </div>
<div class="tab-pane fade" id="diff-tab-pane" role="tabpanel" aria-labelledby="diff-tab" tabindex="0">
<div class="chart-container position-relative tw:min-h-[40vh] tw:h-full tw:w-full">
<canvas id="monthlyDifferenceChart"></canvas>
</div>
</div>
</div>
</div>
</div> </div>
<hr> <hr>
<div class="row gx-xl-4 gy-3 mt-4"> <div class="row gx-xl-4 gy-3 mt-4">
@@ -102,6 +126,7 @@
<div class="row row-cols-1 g-4"> <div class="row row-cols-1 g-4">
<div class="col"> <div class="col">
<c-ui.info-card color="blue" icon="fa-solid fa-wallet" title="{% trans 'By account' %}" <c-ui.info-card color="blue" icon="fa-solid fa-wallet" title="{% trans 'By account' %}"
title_css_classes="tw:cursor-pointer"
_="on click showAllDatasetsAccount()"> _="on click showAllDatasetsAccount()">
{% regroup account_net_worth.values by account.group as account_data %} {% regroup account_net_worth.values by account.group as account_data %}
{% for data in account_data %} {% for data in account_data %}
@@ -115,7 +140,7 @@
{% for account in data.list %} {% for account in data.list %}
<div class="d-flex justify-content-between mt-2"> <div class="d-flex justify-content-between mt-2">
<div class="d-flex align-items-baseline w-100"> <div class="d-flex align-items-baseline w-100">
<div class="account-name text-start font-monospace tw:text-gray-300" <div class="account-name text-start font-monospace tw:text-gray-300 tw:cursor-pointer"
_="on click showOnlyAccountDataset('{{ account.account.name }}')"> _="on click showOnlyAccountDataset('{{ account.account.name }}')">
<span class="hierarchy-line-icon"></span>{{ account.account.name }}</div> <span class="hierarchy-line-icon"></span>{{ account.account.name }}</div>
<div class="dotted-line flex-grow-1"></div> <div class="dotted-line flex-grow-1"></div>
@@ -143,7 +168,7 @@
{% for account in data.list %} {% for account in data.list %}
<div class="d-flex justify-content-between mt-2"> <div class="d-flex justify-content-between mt-2">
<div class="d-flex align-items-baseline w-100"> <div class="d-flex align-items-baseline w-100">
<div class="account-name text-start font-monospace tw:text-gray-300" <div class="account-name text-start font-monospace tw:text-gray-300 tw:cursor-pointer"
_="on click showOnlyAccountDataset('{{ account.account.name }}')"> _="on click showOnlyAccountDataset('{{ account.account.name }}')">
{{ account.account.name }} {{ account.account.name }}
</div> </div>
@@ -310,6 +335,54 @@
} }
</script> </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,
ticks: {
display: false,
format: {maximumFractionDigits: 40, minimumFractionDigits: 0}
},
}
}
}
});
}
</script>
<script type="text/hyperscript"> <script type="text/hyperscript">
def showOnlyAccountDataset(datasetName) def showOnlyAccountDataset(datasetName)
for dataset in accountChart.data.datasets for dataset in accountChart.data.datasets
@@ -325,6 +398,12 @@
call currencyChart.setDatasetVisibility(currencyChart.data.datasets.indexOf(dataset), isMatch) call currencyChart.setDatasetVisibility(currencyChart.data.datasets.indexOf(dataset), isMatch)
end end
call currencyChart.update() call currencyChart.update()
for dataset in monthlyDifferenceChart.data.datasets
set isMatch to dataset.label is datasetName
call monthlyDifferenceChart.setDatasetVisibility(monthlyDifferenceChart.data.datasets.indexOf(dataset), isMatch)
end
call monthlyDifferenceChart.update()
end end
def showAllDatasetsAccount() def showAllDatasetsAccount()
@@ -339,6 +418,11 @@
call currencyChart.setDatasetVisibility(currencyChart.data.datasets.indexOf(dataset), true) call currencyChart.setDatasetVisibility(currencyChart.data.datasets.indexOf(dataset), true)
end end
call currencyChart.update() call currencyChart.update()
for dataset in monthlyDifferenceChart.data.datasets
call monthlyDifferenceChart.setDatasetVisibility(monthlyDifferenceChart.data.datasets.indexOf(dataset), true)
end
call monthlyDifferenceChart.update()
end end
</script> </script>
</div> </div>
+1 -1
View File
@@ -9,7 +9,7 @@
{% block content %} {% block content %}
<div> <div>
<div class="container"> <div class="container">
<div class="row vh-100 d-flex justify-content-center align-items-center"> <div class="row tw:h-dvh d-flex justify-content-center align-items-center">
<div class="col-md-6 col-xl-4 col-12"> <div class="col-md-6 col-xl-4 col-12">
{% settings "DEMO" as demo_mode %} {% settings "DEMO" as demo_mode %}
{% if demo_mode %} {% if demo_mode %}
+1 -1
View File
@@ -15,7 +15,7 @@ services:
- ./frontend/:/usr/src/frontend:z - ./frontend/:/usr/src/frontend:z
- wygiwyh_temp:/usr/src/app/temp/ - wygiwyh_temp:/usr/src/app/temp/
ports: ports:
- "${OUTBOUND_PORT:-8000}:${INTERNAL_PORT:-8000}" - "${OUTBOUND_PORT}:8000"
env_file: env_file:
- .env - .env
depends_on: depends_on:
+1 -1
View File
@@ -4,7 +4,7 @@ services:
container_name: ${SERVER_NAME} container_name: ${SERVER_NAME}
command: /start-single command: /start-single
ports: ports:
- "${OUTBOUND_PORT:-8000}:${INTERNAL_PORT:-8000}" - "${OUTBOUND_PORT}:8000"
env_file: env_file:
- .env - .env
depends_on: depends_on:
+1 -4
View File
@@ -4,9 +4,6 @@ set -o errexit
set -o pipefail set -o pipefail
set -o nounset set -o nounset
# Set INTERNAL_PORT with default value of 8000
INTERNAL_PORT=${INTERNAL_PORT:-8000}
rm -f /tmp/migrations_complete rm -f /tmp/migrations_complete
python manage.py migrate python manage.py migrate
@@ -16,4 +13,4 @@ touch /tmp/migrations_complete
python manage.py setup_users python manage.py setup_users
exec python manage.py runserver 0.0.0.0:$INTERNAL_PORT exec python manage.py runserver 0.0.0.0:8000
+1 -4
View File
@@ -4,9 +4,6 @@ set -o errexit
set -o pipefail set -o pipefail
set -o nounset set -o nounset
# Set INTERNAL_PORT with default value of 8000
INTERNAL_PORT=${INTERNAL_PORT:-8000}
# Remove flag file if it exists from previous run # Remove flag file if it exists from previous run
rm -f /tmp/migrations_complete rm -f /tmp/migrations_complete
@@ -18,4 +15,4 @@ touch /tmp/migrations_complete
python manage.py setup_users python manage.py setup_users
exec gunicorn WYGIWYH.wsgi:application --bind 0.0.0.0:$INTERNAL_PORT --timeout 600 exec gunicorn WYGIWYH.wsgi:application --bind 0.0.0.0:8000 --timeout 600