mirror of
https://github.com/eitchtee/WYGIWYH.git
synced 2026-02-25 00:44:52 +01:00
Compare commits
50 Commits
feat/bette
...
feat/netwo
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7645153f77 | ||
|
|
9dce5e9efe | ||
|
|
bb3cc5da6c | ||
|
|
f2abeff31a | ||
|
|
666eaff167 | ||
|
|
d72454f854 | ||
|
|
333aa81923 | ||
|
|
41b8cfd1e7 | ||
|
|
1fa7985b01 | ||
|
|
38392a6322 | ||
|
|
637c62319b | ||
|
|
f91fe67629 | ||
|
|
9eb1818a20 | ||
|
|
50ac679e33 | ||
|
|
2a463c63b8 | ||
|
|
dce65f2faf | ||
|
|
a053cb3947 | ||
|
|
2d43072120 | ||
|
|
70bdee065e | ||
|
|
95db27a32f | ||
|
|
d6d4e6a102 | ||
|
|
bc0f30fead | ||
|
|
a9a86fc491 | ||
|
|
c3b5f2bf39 | ||
|
|
19128e5aed | ||
|
|
9b5c6d3413 | ||
|
|
73c873a2ad | ||
|
|
9d2be22a77 | ||
|
|
6a3d31f37d | ||
|
|
3be3a3c14b | ||
|
|
a5b0f4efb7 | ||
|
|
6da50db417 | ||
|
|
a6c1daf902 | ||
|
|
6a271fb3d7 | ||
|
|
2cf9a9dd0f | ||
|
|
0deaabe719 | ||
|
|
b14342af2e | ||
|
|
2c14ce6366 | ||
|
|
2dd887b0d9 | ||
|
|
8be7758dc0 | ||
|
|
05dd782df5 | ||
|
|
187fe43283 | ||
|
|
cae73376db | ||
|
|
7225454a6e | ||
|
|
70c8c1e07c | ||
|
|
c392a2c988 | ||
|
|
17ea859fd2 | ||
|
|
8aae6f928f | ||
|
|
7c43b06b9f | ||
|
|
e16e279911 |
@@ -36,7 +36,7 @@ class ArbitraryDecimalDisplayNumberInput(forms.TextInput):
|
||||
{
|
||||
"x-data": "",
|
||||
"x-mask:dynamic": f"$money($input, '{get_format('DECIMAL_SEPARATOR')}', '{get_format('THOUSAND_SEPARATOR')}', '30')",
|
||||
"x-on:keyup": "$el.dispatchEvent(new Event('input'))",
|
||||
"x-on:keyup": "if (!['Control', 'Shift', 'Alt', 'Meta'].includes($event.key) && !(($event.ctrlKey || $event.metaKey) && $event.key.toLowerCase() === 'a')) $el.dispatchEvent(new Event('input'))",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -182,3 +182,29 @@ def calculate_historical_account_balance(queryset):
|
||||
historical_account_balance[date_filter(end_date, "b Y")] = month_data
|
||||
|
||||
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
|
||||
|
||||
@@ -8,6 +8,7 @@ from django.views.decorators.http import require_http_methods
|
||||
from apps.net_worth.utils.calculate_net_worth import (
|
||||
calculate_historical_currency_net_worth,
|
||||
calculate_historical_account_balance,
|
||||
calculate_monthly_net_worth_difference,
|
||||
)
|
||||
from apps.transactions.models import Transaction
|
||||
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)
|
||||
|
||||
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(
|
||||
queryset=transactions_account_queryset
|
||||
)
|
||||
@@ -140,6 +173,7 @@ def net_worth(request):
|
||||
"chart_data_accounts_json": chart_data_accounts_json,
|
||||
"accounts": accounts,
|
||||
"type": view_type,
|
||||
"chart_data_monthly_difference_json": chart_data_monthly_difference_json,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@@ -95,9 +95,9 @@ class DryRunResults:
|
||||
return
|
||||
|
||||
if isinstance(end_instance, Transaction):
|
||||
start_instance = end_instance.deepcopy()
|
||||
end_instance = end_instance.deepcopy()
|
||||
elif isinstance(end_instance, dict):
|
||||
start_instance = deepcopy(end_instance)
|
||||
end_instance = deepcopy(end_instance)
|
||||
|
||||
result = {
|
||||
"type": "update_or_create_transaction",
|
||||
@@ -213,6 +213,16 @@ def check_for_transaction_rules(
|
||||
f"{prefix}internal_id": transaction.internal_id,
|
||||
f"{prefix}is_deleted": transaction.deleted,
|
||||
f"{prefix}is_muted": transaction.mute,
|
||||
f"{prefix}is_recurring": transaction.recurring_transaction is not None,
|
||||
f"{prefix}is_installment": transaction.installment_plan is not None,
|
||||
f"{prefix}installment_number": (
|
||||
transaction.installment_id if transaction.installment_plan else None
|
||||
),
|
||||
f"{prefix}installment_total": (
|
||||
transaction.installment_plan.number_of_installments
|
||||
if transaction.installment_plan
|
||||
else None
|
||||
),
|
||||
}
|
||||
else:
|
||||
return {
|
||||
@@ -256,6 +266,12 @@ def check_for_transaction_rules(
|
||||
f"{prefix}internal_id": transaction.get("internal_id", ""),
|
||||
f"{prefix}is_deleted": transaction.get("deleted", True),
|
||||
f"{prefix}is_muted": transaction.get("mute", False),
|
||||
f"{prefix}is_recurring": transaction.get(
|
||||
"recurring_transaction", False
|
||||
),
|
||||
f"{prefix}is_installment": transaction.get("installment", False),
|
||||
f"{prefix}installment_number": transaction.get("installment_id"),
|
||||
f"{prefix}installment_total": transaction.get("installment_total"),
|
||||
}
|
||||
|
||||
def _process_update_or_create_transaction_action(processed_action):
|
||||
@@ -520,7 +536,8 @@ def check_for_transaction_rules(
|
||||
return transaction
|
||||
|
||||
user = get_user_model().objects.get(id=user_id)
|
||||
write_current_user(user)
|
||||
if not dry_run:
|
||||
write_current_user(user)
|
||||
logs = [] if dry_run else None
|
||||
dry_run_results = DryRunResults(dry_run=dry_run)
|
||||
|
||||
@@ -745,11 +762,12 @@ def check_for_transaction_rules(
|
||||
"** Error while executing 'check_for_transaction_rules' task",
|
||||
level="error",
|
||||
)
|
||||
delete_current_user()
|
||||
if not dry_run:
|
||||
delete_current_user()
|
||||
raise e
|
||||
|
||||
delete_current_user()
|
||||
if not dry_run:
|
||||
delete_current_user()
|
||||
|
||||
return logs, dry_run_results.results
|
||||
|
||||
|
||||
@@ -90,4 +90,12 @@ def serialize_transaction(sender: Transaction, deleted: bool):
|
||||
"internal_note": sender.internal_note,
|
||||
"internal_id": sender.internal_id,
|
||||
"mute": sender.mute,
|
||||
"installment_id": sender.installment_id if sender.installment_plan else None,
|
||||
"installment_total": (
|
||||
sender.installment_plan.number_of_installments
|
||||
if sender.installment_plan is not None
|
||||
else None
|
||||
),
|
||||
"installment": sender.installment_plan is not None,
|
||||
"recurring_transaction": sender.recurring_transaction is not None,
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import decimal
|
||||
import logging
|
||||
from copy import deepcopy
|
||||
|
||||
@@ -381,6 +382,9 @@ class Transaction(OwnedObject):
|
||||
default_manager_name = "objects"
|
||||
|
||||
def clean_fields(self, *args, **kwargs):
|
||||
if isinstance(self.amount, (str, int, float)):
|
||||
self.amount = decimal.Decimal(str(self.amount))
|
||||
|
||||
self.amount = truncate_decimal(
|
||||
value=self.amount, decimal_places=self.account.currency.decimal_places
|
||||
)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
3510
app/locale/it/LC_MESSAGES/django.po
Normal file
3510
app/locale/it/LC_MESSAGES/django.po
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -3,7 +3,7 @@
|
||||
{% if icon %}<i class="{{ icon }}"></i>{% else %}<span class="fw-bold">{{ title.0 }}</span>{% endif %}
|
||||
</div>
|
||||
<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 }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -354,6 +354,7 @@
|
||||
display: false,
|
||||
},
|
||||
ticks: {
|
||||
display: false,
|
||||
format: { maximumFractionDigits: 40, minimumFractionDigits: 1 }
|
||||
}
|
||||
},
|
||||
@@ -369,6 +370,7 @@
|
||||
display: false,
|
||||
},
|
||||
ticks: {
|
||||
display: false,
|
||||
format: { maximumFractionDigits: 40, minimumFractionDigits: 1 }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,4 @@
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="{% static 'img/favicon/favicon-32x32.png' %}">
|
||||
<link rel="icon" type="image/png" sizes="96x96" href="{% static 'img/favicon/favicon-96x96.png' %}">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="{% static 'img/favicon/favicon-16x16.png' %}">
|
||||
<meta name="msapplication-TileColor" content="#ffffff">
|
||||
<meta name="msapplication-TileImage" content="{% static 'img/favicon/ms-icon-144x144.png' %}">
|
||||
<meta name="theme-color" content="#ffffff">
|
||||
|
||||
@@ -10,18 +10,10 @@
|
||||
<nav
|
||||
id="sidebar"
|
||||
hx-boost="true"
|
||||
hx-swap="transition:true"
|
||||
data-bs-scroll="true"
|
||||
class="offcanvas-lg offcanvas-start d-lg-flex flex-column position-fixed top-0 start-0 h-100 bg-body-tertiary shadow-sm tw:z-1020">
|
||||
|
||||
{# <div>#}
|
||||
{# <a href="{% url 'index' %}" class="d-none d-lg-flex tw:justify-start p-3 text-decoration-none sidebar-title">#}
|
||||
{# <img src="{% static 'img/logo-icon.svg' %}" alt="WYGIWYH Logo" height="30" width="30" title="WYGIWYH"/>#}
|
||||
{# <span class="fs-4 fw-bold ms-3">WYGIWYH</span>#}
|
||||
{# </a>#}
|
||||
{##}
|
||||
{##}
|
||||
{# </div>#}
|
||||
|
||||
<div class="d-none d-lg-flex tw:justify-between tw:items-center tw:border-b tw:border-gray-600 tw:lg:flex">
|
||||
<a href="{% url 'index' %}" class="m-0 d-none d-lg-flex tw:justify-start p-3 text-decoration-none sidebar-title">
|
||||
<img src="{% static 'img/logo-icon.svg' %}" alt="WYGIWYH Logo" height="30" width="30" title="WYGIWYH"/>
|
||||
@@ -154,16 +146,16 @@
|
||||
aria-controls="collapsible-panel"
|
||||
class="sidebar-menu-item tw:text-wrap tw:lg:text-nowrap tw:lg:text-sm d-flex align-items-center text-decoration-none p-2 rounded-3 sidebar-item {% active_link views='tags_index||entities_index||categories_index||accounts_index||account_groups_index||currencies_index||exchange_rates_index||rules_index||import_profiles_index||automatic_exchange_rates_index||export_index||users_index' css_class="sidebar-active" %}">
|
||||
<i class="fa-solid fa-toolbox fa-fw"></i>
|
||||
<span
|
||||
class="ms-3 fw-medium tw:lg:group-hover:truncate tw:lg:group-focus:truncate tw:lg:group-hover:text-ellipsis tw:lg:group-focus:text-ellipsis">
|
||||
{% translate 'Management' %}
|
||||
</span>
|
||||
<span class="ms-3 fw-medium tw:lg:group-hover:truncate tw:lg:group-focus:truncate tw:lg:group-hover:text-ellipsis tw:lg:group-focus:text-ellipsis">
|
||||
{% translate 'Management' %}
|
||||
</span>
|
||||
<i class="fa-solid fa-chevron-right fa-fw ms-auto pe-2"></i>
|
||||
</div>
|
||||
</ul>
|
||||
|
||||
<div class="mt-auto p-2 w-100">
|
||||
<div id="collapsible-panel"
|
||||
class="p-0 collapse tw:absolute tw:bottom-0 tw:left-0 tw:w-full tw:z-30 tw:max-h-dvh">
|
||||
class="p-0 collapse tw:absolute tw:bottom-0 tw:left-0 tw:w-full tw:z-30 tw:max-h-dvh {% active_link views='tags_index||entities_index||categories_index||accounts_index||account_groups_index||currencies_index||exchange_rates_index||rules_index||import_profiles_index||automatic_exchange_rates_index||export_index||users_index' css_class="show" %}">
|
||||
<div class="tw:h-dvh tw:backdrop-blur-3xl tw:flex tw:flex-col">
|
||||
<div
|
||||
class="tw:justify-between tw:items-center tw:p-4 tw:border-b tw:border-gray-600 sidebar-submenu-header">
|
||||
|
||||
@@ -9,338 +9,422 @@
|
||||
{% block title %}{% if type == "current" %}{% translate 'Current Net Worth' %}{% else %}{% translate 'Projected Net Worth' %}{% endif %}{% endblock %}
|
||||
|
||||
{% 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-100 text-center mb-4 pt-2">
|
||||
<div class="btn-group gap-3" role="group" id="view-type" _="on change trigger updated">
|
||||
<input type="radio" class="btn-check"
|
||||
name="view_type"
|
||||
id="current-view"
|
||||
autocomplete="off"
|
||||
value="current"
|
||||
{% if type == "current" %}checked{% endif %}>
|
||||
<label class="btn btn-outline-primary rounded-5" for="current-view"><i
|
||||
class="fa-solid fa-sack-dollar fa-fw me-2"></i>{% trans 'Current' %}</label>
|
||||
<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="btn-group gap-3" role="group" id="view-type" _="on change trigger updated">
|
||||
<input type="radio" class="btn-check"
|
||||
name="view_type"
|
||||
id="current-view"
|
||||
autocomplete="off"
|
||||
value="current"
|
||||
{% if type == "current" %}checked{% endif %}>
|
||||
<label class="btn btn-outline-primary rounded-5" for="current-view"><i
|
||||
class="fa-solid fa-sack-dollar fa-fw me-2"></i>{% trans 'Current' %}</label>
|
||||
|
||||
<input type="radio"
|
||||
class="btn-check"
|
||||
name="view_type"
|
||||
id="projected-view"
|
||||
autocomplete="off"
|
||||
value="projected"
|
||||
{% if type == "projected" %}checked{% endif %}>
|
||||
<label class="btn btn-outline-primary rounded-5" for="projected-view"><i
|
||||
class="fa-solid fa-rocket fa-fw me-2"></i>{% trans 'Projected' %}</label>
|
||||
<input type="radio"
|
||||
class="btn-check"
|
||||
name="view_type"
|
||||
id="projected-view"
|
||||
autocomplete="off"
|
||||
value="projected"
|
||||
{% if type == "projected" %}checked{% endif %}>
|
||||
<label class="btn btn-outline-primary rounded-5" for="projected-view"><i
|
||||
class="fa-solid fa-rocket fa-fw me-2"></i>{% trans 'Projected' %}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container px-md-3 py-3" _="init call initializeAccountChart() then initializeCurrencyChart() end">
|
||||
<div class="row gx-xl-4 gy-3 mb-4">
|
||||
<div class="col-12 col-xl-5">
|
||||
<div class="row row-cols-1 g-4">
|
||||
<div class="col">
|
||||
<c-ui.info-card color="yellow" icon="fa-solid fa-coins" title="{% trans 'By currency' %}"
|
||||
_="on click showAllDatasetsCurrency()">
|
||||
{% for currency in currency_net_worth.values %}
|
||||
<div class="d-flex justify-content-between mt-2">
|
||||
<div class="d-flex align-items-baseline w-100">
|
||||
<div class="currency-name text-start font-monospace tw:text-gray-300"
|
||||
_="on click showOnlyCurrencyDataset('{{ currency.currency.name }}')">
|
||||
{{ currency.currency.name }}
|
||||
<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="col-12 col-xl-5">
|
||||
<div class="row row-cols-1 g-4">
|
||||
<div class="col">
|
||||
<c-ui.info-card color="yellow" icon="fa-solid fa-coins" title="{% trans 'By currency' %}"
|
||||
title_css_classes="tw:cursor-pointer"
|
||||
_="on click showAllDatasetsCurrency()">
|
||||
{% for currency in currency_net_worth.values %}
|
||||
<div class="d-flex justify-content-between mt-2">
|
||||
<div class="d-flex align-items-baseline w-100">
|
||||
<div class="currency-name text-start font-monospace tw:text-gray-300 tw:cursor-pointer"
|
||||
_="on click showOnlyCurrencyDataset('{{ currency.currency.name }}')">
|
||||
{{ currency.currency.name }}
|
||||
</div>
|
||||
<div class="dotted-line flex-grow-1"></div>
|
||||
<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>
|
||||
</div>
|
||||
<div class="dotted-line flex-grow-1"></div>
|
||||
</div>
|
||||
{% if currency.exchanged and currency.exchanged.total_final %}
|
||||
<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>
|
||||
: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>
|
||||
</div>
|
||||
</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 %}
|
||||
{% if currency.consolidated and currency.consolidated.total_final != currency.total_final %}
|
||||
<div class="d-flex align-items-baseline w-100">
|
||||
<div class="account-name text-start font-monospace tw:text-gray-300">
|
||||
<span class="hierarchy-line-icon"></span>{% trans 'Consolidated' %}</div>
|
||||
<div class="dotted-line flex-grow-1"></div>
|
||||
<div class="">
|
||||
<c-amount.display
|
||||
{% endif %}
|
||||
{% if currency.consolidated and currency.consolidated.total_final != currency.total_final %}
|
||||
<div class="d-flex align-items-baseline w-100">
|
||||
<div class="account-name text-start font-monospace tw:text-gray-300">
|
||||
<span class="hierarchy-line-icon"></span>{% trans 'Consolidated' %}</div>
|
||||
<div class="dotted-line flex-grow-1"></div>
|
||||
<div class="">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</c-ui.info-card>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</c-ui.info-card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-xl-7">
|
||||
<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>
|
||||
</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 class="col-12 col-xl-7">
|
||||
<div class="chart-container position-relative tw:min-h-[40vh] tw:h-full">
|
||||
<canvas id="currencyBalanceChart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<hr>
|
||||
<div class="row gx-xl-4 gy-3 mt-4">
|
||||
<div class="col-12 col-xl-5">
|
||||
<div class="row row-cols-1 g-4">
|
||||
<div class="col">
|
||||
<c-ui.info-card color="blue" icon="fa-solid fa-wallet" title="{% trans 'By account' %}"
|
||||
_="on click showAllDatasetsAccount()">
|
||||
{% regroup account_net_worth.values by account.group as account_data %}
|
||||
{% for data in account_data %}
|
||||
{% if data.grouper %}
|
||||
<div class="d-flex justify-content-between mt-2">
|
||||
<div class="d-flex align-items-baseline w-100">
|
||||
<div class="text-start font-monospace tw:text-gray-300"><span class="badge text-bg-primary">
|
||||
<hr>
|
||||
<div class="row gx-xl-4 gy-3 mt-4">
|
||||
<div class="col-12 col-xl-5">
|
||||
<div class="row row-cols-1 g-4">
|
||||
<div class="col">
|
||||
<c-ui.info-card color="blue" icon="fa-solid fa-wallet" title="{% trans 'By account' %}"
|
||||
title_css_classes="tw:cursor-pointer"
|
||||
_="on click showAllDatasetsAccount()">
|
||||
{% regroup account_net_worth.values by account.group as account_data %}
|
||||
{% for data in account_data %}
|
||||
{% if data.grouper %}
|
||||
<div class="d-flex justify-content-between mt-2">
|
||||
<div class="d-flex align-items-baseline w-100">
|
||||
<div class="text-start font-monospace tw:text-gray-300"><span class="badge text-bg-primary">
|
||||
{{ data.grouper }}</span></div>
|
||||
</div>
|
||||
</div>
|
||||
{% for account in data.list %}
|
||||
<div class="d-flex justify-content-between mt-2">
|
||||
<div class="d-flex align-items-baseline w-100">
|
||||
<div class="account-name text-start font-monospace tw:text-gray-300"
|
||||
_="on click showOnlyAccountDataset('{{ account.account.name }}')">
|
||||
<span class="hierarchy-line-icon"></span>{{ account.account.name }}</div>
|
||||
<div class="dotted-line flex-grow-1"></div>
|
||||
<div class="">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
{% if account.exchanged and account.exchanged.total_final %}
|
||||
<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>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
{% for account in data.list %}
|
||||
<div class="d-flex justify-content-between mt-2">
|
||||
<div class="d-flex align-items-baseline w-100">
|
||||
<div class="account-name text-start font-monospace tw:text-gray-300"
|
||||
_="on click showOnlyAccountDataset('{{ account.account.name }}')">
|
||||
{{ account.account.name }}
|
||||
</div>
|
||||
<div class="dotted-line flex-grow-1"></div>
|
||||
<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>
|
||||
{% for account in data.list %}
|
||||
<div class="d-flex justify-content-between mt-2">
|
||||
<div class="d-flex align-items-baseline w-100">
|
||||
<div class="account-name text-start font-monospace tw:text-gray-300 tw:cursor-pointer"
|
||||
_="on click showOnlyAccountDataset('{{ account.account.name }}')">
|
||||
<span class="hierarchy-line-icon"></span>{{ account.account.name }}</div>
|
||||
<div class="dotted-line flex-grow-1"></div>
|
||||
<div class="">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% if account.exchanged and account.exchanged.total_final %}
|
||||
<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>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</c-ui.info-card>
|
||||
{% if account.exchanged and account.exchanged.total_final %}
|
||||
<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>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
{% for account in data.list %}
|
||||
<div class="d-flex justify-content-between mt-2">
|
||||
<div class="d-flex align-items-baseline w-100">
|
||||
<div class="account-name text-start font-monospace tw:text-gray-300 tw:cursor-pointer"
|
||||
_="on click showOnlyAccountDataset('{{ account.account.name }}')">
|
||||
{{ account.account.name }}
|
||||
</div>
|
||||
<div class="dotted-line flex-grow-1"></div>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
{% if account.exchanged and account.exchanged.total_final %}
|
||||
<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>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</c-ui.info-card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-xl-7">
|
||||
<div class="chart-container position-relative tw:min-h-[40vh] tw:h-full">
|
||||
<canvas id="accountBalanceChart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-xl-7">
|
||||
<div class="chart-container position-relative tw:min-h-[40vh] tw:h-full">
|
||||
<canvas id="accountBalanceChart"></canvas>
|
||||
</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,
|
||||
},
|
||||
ticks: {
|
||||
display: false,
|
||||
format: {maximumFractionDigits: 40, minimumFractionDigits: 0}
|
||||
},
|
||||
border: {
|
||||
display: false
|
||||
}
|
||||
}
|
||||
]))
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
</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,
|
||||
},
|
||||
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,
|
||||
ticks: {
|
||||
display: false,
|
||||
format: {maximumFractionDigits: 40, minimumFractionDigits: 0}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
</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)
|
||||
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 datasetName
|
||||
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>
|
||||
|
||||
<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,
|
||||
},
|
||||
ticks: {
|
||||
display: false,
|
||||
format: {maximumFractionDigits: 40, minimumFractionDigits: 0}
|
||||
},
|
||||
border: {
|
||||
display: false
|
||||
}
|
||||
}
|
||||
]))
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
</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,
|
||||
},
|
||||
ticks: {
|
||||
display: false,
|
||||
format: {maximumFractionDigits: 40, minimumFractionDigits: 0}
|
||||
},
|
||||
border: {
|
||||
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)
|
||||
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()
|
||||
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()
|
||||
end
|
||||
</script>
|
||||
</div>
|
||||
<c-ui.transactions_fab></c-ui.transactions_fab>
|
||||
<c-ui.transactions_fab></c-ui.transactions_fab>
|
||||
{% endblock %}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
{% load i18n %}
|
||||
{% load crispy_forms_tags %}
|
||||
|
||||
{% block title %}{% translate 'Edit transaction rule' %}{% endblock %}
|
||||
{% block title %}{% trans 'Test' %} - {% trans 'Create' %}{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<form hx-post="{% url 'transaction_rule_dry_run_created' pk=rule.id %}" hx-target="#generic-offcanvas"
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
{% load i18n %}
|
||||
{% load crispy_forms_tags %}
|
||||
|
||||
{% block title %}{% translate 'Edit transaction rule' %}{% endblock %}
|
||||
{% block title %}{% trans 'Test' %} - {% trans 'Delete' %}{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<form hx-post="{% url 'transaction_rule_dry_run_deleted' pk=rule.id %}" hx-target="#generic-offcanvas"
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
{% load i18n %}
|
||||
{% load crispy_forms_tags %}
|
||||
|
||||
{% block title %}{% translate 'Edit transaction rule' %}{% endblock %}
|
||||
{% block title %}{% trans 'Test' %} - {% trans 'Update' %}{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<form hx-post="{% url 'transaction_rule_dry_run_updated' pk=rule.id %}" hx-target="#generic-offcanvas"
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
{% block content %}
|
||||
<div>
|
||||
<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">
|
||||
{% settings "DEMO" as demo_mode %}
|
||||
{% if demo_mode %}
|
||||
|
||||
Reference in New Issue
Block a user