This commit is contained in:
Herculino Trotta
2024-10-09 00:31:21 -03:00
parent e78e4cc5e1
commit 3dde44b1cd
139 changed files with 4965 additions and 1004 deletions
@@ -0,0 +1,11 @@
{% extends 'extends/offcanvas.html' %}
{% load i18n %}
{% load crispy_forms_tags %}
{% block title %}{% translate 'Add account group' %}{% endblock %}
{% block body %}
<form hx-post="{% url 'account_group_add' %}" hx-target="#generic-offcanvas" novalidate>
{% crispy form %}
</form>
{% endblock %}
@@ -0,0 +1,11 @@
{% extends 'extends/offcanvas.html' %}
{% load i18n %}
{% load crispy_forms_tags %}
{% block title %}{% translate 'Edit account group' %}{% endblock %}
{% block body %}
<form hx-post="{% url 'account_group_edit' pk=account_group.id %}" hx-target="#generic-offcanvas" novalidate>
{% crispy form %}
</form>
{% endblock %}
@@ -0,0 +1,74 @@
{% extends "layouts/base.html" %}
{% load i18n %}
{% block title %}{% translate 'Accounts' %}{% endblock %}
{% block content %}
<div class="container px-md-3 py-3 column-gap-5">
<div class="tw-text-3xl fw-bold font-monospace tw-w-full mb-3">
{% spaceless %}
<div>{% translate 'Account Groups' %}<span>
<a class="text-decoration-none tw-text-2xl p-1 category-action"
role="button"
data-bs-toggle="tooltip"
data-bs-title="{% translate "Add" %}"
hx-get="{% url 'account_group_add' %}"
hx-target="#generic-offcanvas"
_="">
<i class="fa-solid fa-circle-plus fa-fw"></i></a>
</span></div>
{% endspaceless %}
</div>
<div class="border p-3 rounded-3">
<table class="table table-hover table-responsive">
<thead>
<tr>
<th scope="col" class="col-auto"></th>
<th scope="col" class="col">{% translate 'Name' %}</th>
</tr>
</thead>
<tbody>
{% for account_group in account_groups %}
<tr class="account_group">
<td class="col-auto">
<a class="text-decoration-none tw-text-gray-400 p-1 tag-action"
role="button"
data-bs-toggle="tooltip"
data-bs-title="{% translate "Edit" %}"
hx-get="{% url 'account_group_edit' pk=account_group.id %}"
hx-target="#generic-offcanvas"
_="install toast">
<i class="fa-solid fa-pencil fa-fw"></i></a>
<a class="text-danger text-decoration-none p-1 tag-action"
role="button"
data-bs-toggle="tooltip"
data-bs-title="{% translate "Delete" %}"
hx-delete="{% url 'account_group_delete' pk=account_group.id %}"
hx-trigger='delete_confirmed'
_="install toast
on click
if event.ctrlKey trigger delete_confirmed
else
call Swal.fire({title: '{% translate 'Are you sure?' %}',
text: '{% blocktranslate %}You won\'t be able to revert this!{% endblocktranslate %}',
icon: 'warning',
showCancelButton: true,
confirmButtonColor: '#3085d6',
cancelButtonColor: '#d33',
cancelButtonText: '{% blocktranslate %}Cancel{% endblocktranslate %}',
confirmButtonText: '{% blocktranslate %}Yes, delete it!{% endblocktranslate %}',
customClass: {
confirmButton: 'btn btn-primary me-3',
cancelButton: 'btn btn-danger'
},
buttonsStyling: false})
if result.isConfirmed trigger delete_confirmed"><i class="fa-solid fa-trash fa-fw"></i></a></td>
<td class="col">{{ account_group.name }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endblock %}
@@ -0,0 +1,72 @@
{% extends 'extends/offcanvas.html' %}
{% load currency_display %}
{% load i18n %}
{% load crispy_forms_tags %}
{% block title %}{% translate 'Account Reconciliation' %}{% endblock %}
{% block body %}
<form hx-post="{% url 'account_reconciliation' %}">
{% csrf_token %}
{{ form.management_form }}
<div class="accordion accordion-flush" id="balanceAccordionFlush">
{% for form in form.forms %}
<div class="accordion-item">
<h2 class="accordion-header">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse"
data-bs-target="#flush-collapse-{{ forloop.counter0 }}" aria-expanded="false"
aria-controls="flush-collapseOne">
{{ form.account_name }}
</button>
</h2>
<div id="flush-collapse-{{ forloop.counter0 }}" class="accordion-collapse collapse">
<div class="accordion-body">
<div class="mb-3">
<div class="form-label">
{% translate 'Current balance' %}
</div>
<div data-amount="{{ form.current_balance }}"
data-decimal-places="{{ form.currency_decimal_places }}"
id="amount-{{ forloop.counter0 }}">
{% currency_display amount=form.current_balance prefix=form.currency_prefix suffix=form.currency_suffix decimal_places=form.currency_decimal_places %}
</div>
</div>
<div>
{% crispy form %}
</div>
<div class="mb-3">
<div class="form-label">
{% translate 'Difference' %}
</div>
<div _="on input from #id_form-{{ forloop.counter0 }}-new_balance
set original_amount to parseLocaleNumber('{{ form.current_balance }}')
then call parseLocaleNumber(#id_form-{{ forloop.counter0 }}-new_balance.value)
then set new_amount to result
then set diff to new_amount - original_amount
then set prefix to '{{ form.currency_prefix }}'
then set suffix to '{{ form.currency_suffix }}'
then set decimal_places to {{ form.currency_decimal_places }}
then set format_new_amount to
Intl.NumberFormat(
undefined,
{
minimumFractionDigits: decimal_places,
maximumFractionDigits: decimal_places,
roundingMode: 'trunc'}
).format(diff)
then set formatted_string to `${prefix}${format_new_amount}${suffix}`
then put formatted_string into me if diff else
put '' into me">-</div>
</div>
</div>
</div>
</div>
{% endfor %}
</div>
<div class="mt-3">
<div>
<input type="submit" name="submit" value="{% translate 'Reconcile balances' %}" class="btn btn-outline-primary w-100" id="submit-id-submit">
</div>
</div>
</form>
{% endblock %}
+11
View File
@@ -0,0 +1,11 @@
{% extends 'extends/offcanvas.html' %}
{% load i18n %}
{% load crispy_forms_tags %}
{% block title %}{% translate 'Add account' %}{% endblock %}
{% block body %}
<form hx-post="{% url 'account_add' %}" hx-target="#generic-offcanvas" novalidate>
{% crispy form %}
</form>
{% endblock %}
@@ -0,0 +1,11 @@
{% extends 'extends/offcanvas.html' %}
{% load i18n %}
{% load crispy_forms_tags %}
{% block title %}{% translate 'Edit account' %}{% endblock %}
{% block body %}
<form hx-post="{% url 'account_edit' pk=account.id %}" hx-target="#generic-offcanvas" novalidate>
{% crispy form %}
</form>
{% endblock %}
+85
View File
@@ -0,0 +1,85 @@
{% extends "layouts/base.html" %}
{% load i18n %}
{% block title %}{% translate 'Accounts' %}{% endblock %}
{% block content %}
<div class="container px-md-3 py-3 column-gap-5">
<div class="tw-text-3xl fw-bold font-monospace tw-w-full mb-3">
{% spaceless %}
<div>{% translate 'Accounts' %}<span>
<a class="text-decoration-none tw-text-2xl p-1 category-action"
role="button"
data-bs-toggle="tooltip"
data-bs-title="{% translate "Add" %}"
hx-get="{% url 'account_add' %}"
hx-target="#generic-offcanvas"
_="">
<i class="fa-solid fa-circle-plus fa-fw"></i></a>
</span></div>
{% endspaceless %}
</div>
<div class="border p-3 rounded-3">
<table class="table table-hover table-responsive">
<thead>
<tr>
<th scope="col" class="col-auto"></th>
<th scope="col" class="col">{% translate 'Name' %}</th>
<th scope="col" class="col">{% translate 'Currency' %}</th>
<th scope="col" class="col">{% translate 'Exchange Currency' %}</th>
<th scope="col" class="col-auto">{% translate 'Is Asset' %}</th>
</tr>
</thead>
<tbody>
{% for account in accounts %}
<tr class="currency">
<td class="col-auto">
<a class="text-decoration-none tw-text-gray-400 p-1 tag-action"
role="button"
data-bs-toggle="tooltip"
data-bs-title="{% translate "Edit" %}"
hx-get="{% url 'account_edit' pk=account.id %}"
hx-target="#generic-offcanvas"
_="on click send action_clicked to .tag-action in the closest parent .tag end
on action_clicked call bootstrap.Tooltip.getOrCreateInstance(me).dispose() end
install toast">
<i class="fa-solid fa-pencil fa-fw"></i></a>
<a class="text-danger text-decoration-none p-1 tag-action"
role="button"
data-bs-toggle="tooltip"
data-bs-title="{% translate "Delete" %}"
hx-delete="{% url 'account_delete' pk=account.id %}"
hx-trigger='delete_confirmed'
_="on click send action_clicked to .tag-action in the closest parent .tag end
on action_clicked call bootstrap.Tooltip.getOrCreateInstance(me).dispose() end
install toast
on click
if event.ctrlKey trigger delete_confirmed
else
call Swal.fire({title: '{% translate 'Are you sure?' %}',
text: '{% blocktranslate %}You won\'t be able to revert this!{% endblocktranslate %}',
icon: 'warning',
showCancelButton: true,
confirmButtonColor: '#3085d6',
cancelButtonColor: '#d33',
cancelButtonText: '{% blocktranslate %}Cancel{% endblocktranslate %}',
confirmButtonText: '{% blocktranslate %}Yes, delete it!{% endblocktranslate %}',
customClass: {
confirmButton: 'btn btn-primary me-3',
cancelButton: 'btn btn-danger'
},
buttonsStyling: false})
if result.isConfirmed trigger delete_confirmed"><i class="fa-solid fa-trash fa-fw"></i></a></td>
<td class="col">{{ account.name }}</td>
<td class="col-auto">{{ account.currency }} ({{ account.currency.code }})</td>
<td class="col-auto">{% if account.exchange_currency %}{{ account.exchange_currency }} (
{{ account.exchange_currency.code }}){% else %}-{% endif %}</td>
<td class="col-auto text-center">{% if account.is_asset %}<i class="fa-solid fa-circle text-success"></i>{% endif %}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endblock %}
+12
View File
@@ -0,0 +1,12 @@
{% extends "admin/base_site.html" %}
{% block extrahead %}
{{ block.super }}
<script>
let tz = Intl.DateTimeFormat().resolvedOptions().timeZone;
if (!tz) {
tz = "UTC"
}
document.cookie = "mytz=" + tz + ";path=/";
</script>
{% endblock %}
@@ -0,0 +1,11 @@
{% extends 'extends/offcanvas.html' %}
{% load i18n %}
{% load crispy_forms_tags %}
{% block title %}{% translate 'Add category' %}{% endblock %}
{% block body %}
<form hx-post="{% url 'category_add' %}" hx-target="#generic-offcanvas" novalidate>
{% crispy form %}
</form>
{% endblock %}
@@ -0,0 +1,11 @@
{% extends 'extends/offcanvas.html' %}
{% load i18n %}
{% load crispy_forms_tags %}
{% block title %}{% translate 'Edit category' %}{% endblock %}
{% block body %}
<form hx-post="{% url 'category_edit' category_id=category.id %}" hx-target="#generic-offcanvas" novalidate>
{% crispy form %}
</form>
{% endblock %}
+78
View File
@@ -0,0 +1,78 @@
{% extends "layouts/base.html" %}
{% load i18n %}
{% block title %}{% translate 'Categories' %}{% endblock %}
{% block content %}
<div class="container px-md-3 py-3 column-gap-5">
<div class="tw-text-3xl fw-bold font-monospace tw-w-full mb-3">
{% spaceless %}
<div>{% translate 'Categories' %}<span>
<a class="text-decoration-none tw-text-2xl p-1 category-action"
role="button"
data-bs-toggle="tooltip"
data-bs-title="{% translate "Add" %}"
hx-get="{% url 'category_add' %}"
hx-target="#generic-offcanvas"
_="">
<i class="fa-solid fa-circle-plus fa-fw"></i></a>
</span></div>
{% endspaceless %}
</div>
<div class="border p-3 rounded-3">
<table class="table table-hover table-responsive">
<thead>
<tr>
<th scope="col" class="col-auto"></th>
<th scope="col" class="col-auto">{% translate 'Muted' %}</th>
<th scope="col" class="col">{% translate 'Name' %}</th>
</tr>
</thead>
<tbody>
{% for category in categories %}
<tr class="category">
<td class="col-auto text-center">
<a class="text-decoration-none tw-text-gray-400 p-1 category-action"
role="button"
data-bs-toggle="tooltip"
data-bs-title="{% translate "Edit" %}"
hx-get="{% url 'category_edit' category_id=category.id %}"
hx-target="#generic-offcanvas"
_="install toast">
<i class="fa-solid fa-pencil fa-fw"></i></a>
<a class="text-danger text-decoration-none p-1 category-action"
role="button"
data-bs-toggle="tooltip"
data-bs-title="{% translate "Delete" %}"
hx-delete="{% url 'category_delete' category_id=category.id %}"
hx-trigger='delete_confirmed'
_="install toast
on click
if event.ctrlKey trigger delete_confirmed
else
call Swal.fire({title: '{% translate 'Are you sure?' %}',
text: '{% blocktranslate %}You won\'t be able to revert this!{% endblocktranslate %}',
icon: 'warning',
showCancelButton: true,
confirmButtonColor: '#3085d6',
cancelButtonColor: '#d33',
cancelButtonText: '{% blocktranslate %}Cancel{% endblocktranslate %}',
confirmButtonText: '{% blocktranslate %}Yes, delete it!{% endblocktranslate %}',
customClass: {
confirmButton: 'btn btn-primary me-3',
cancelButton: 'btn btn-danger'
},
buttonsStyling: false})
if result.isConfirmed trigger delete_confirmed"><i class="fa-solid fa-trash fa-fw"></i></a></td>
<td class="col-auto text-center">
{% if category.mute %}<i class="fa-solid fa-circle text-success"></i>{% endif %}
</td>
<td class="col">{{ category.name }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endblock %}
+1 -1
View File
@@ -1,6 +1,6 @@
{% load toast_bg %}
{% if messages %}
<div class="toast-container position-fixed top-0 start-50 translate-middle-x p-3">
<div class="toast-container position-fixed bottom-0 end-0 p-3">
{% for message in messages %}
<div class="toast align-items-center text-bg-{{ message.tags | toast_bg }} border-0"
role="alert"
@@ -0,0 +1,11 @@
{% extends 'extends/offcanvas.html' %}
{% load i18n %}
{% load crispy_forms_tags %}
{% block title %}{% translate 'Add currency' %}{% endblock %}
{% block body %}
<form hx-post="{% url 'currency_add' %}" hx-target="#generic-offcanvas" novalidate>
{% crispy form %}
</form>
{% endblock %}
@@ -0,0 +1,11 @@
{% extends 'extends/offcanvas.html' %}
{% load i18n %}
{% load crispy_forms_tags %}
{% block title %}{% translate 'Edit currency' %}{% endblock %}
{% block body %}
<form hx-post="{% url 'currency_edit' pk=currency.id %}" hx-target="#generic-offcanvas" novalidate>
{% crispy form %}
</form>
{% endblock %}
+80
View File
@@ -0,0 +1,80 @@
{% extends "layouts/base.html" %}
{% load i18n %}
{% block title %}{% translate 'Currencies' %}{% endblock %}
{% block content %}
<div class="container px-md-3 py-3 column-gap-5">
<div class="tw-text-3xl fw-bold font-monospace tw-w-full mb-3">
{% spaceless %}
<div>{% translate 'Currencies' %}<span>
<a class="text-decoration-none tw-text-2xl p-1 category-action"
role="button"
data-bs-toggle="tooltip"
data-bs-title="{% translate "Add" %}"
hx-get="{% url 'currency_add' %}"
hx-target="#generic-offcanvas"
_="">
<i class="fa-solid fa-circle-plus fa-fw"></i></a>
</span></div>
{% endspaceless %}
</div>
<div class="border p-3 rounded-3">
<table class="table table-hover table-responsive">
<thead>
<tr>
<th scope="col" class="col-auto"></th>
<th scope="col" class="col-auto">{% translate 'Code' %}</th>
<th scope="col" class="col">{% translate 'Name' %}</th>
</tr>
</thead>
<tbody>
{% for currency in currencies %}
<tr class="currency">
<td class="col-auto">
<a class="text-decoration-none tw-text-gray-400 p-1 tag-action"
role="button"
data-bs-toggle="tooltip"
data-bs-title="{% translate "Edit" %}"
hx-get="{% url 'currency_edit' pk=currency.id %}"
hx-target="#generic-offcanvas"
_="on click send action_clicked to .tag-action in the closest parent .tag end
on action_clicked call bootstrap.Tooltip.getOrCreateInstance(me).dispose() end
install toast">
<i class="fa-solid fa-pencil fa-fw"></i></a>
<a class="text-danger text-decoration-none p-1 tag-action"
role="button"
data-bs-toggle="tooltip"
data-bs-title="{% translate "Delete" %}"
hx-delete="{% url 'currency_delete' pk=currency.id %}"
hx-trigger='delete_confirmed'
_="on click send action_clicked to .tag-action in the closest parent .tag end
on action_clicked call bootstrap.Tooltip.getOrCreateInstance(me).dispose() end
install toast
on click
if event.ctrlKey trigger delete_confirmed
else
call Swal.fire({title: '{% translate 'Are you sure?' %}',
text: '{% blocktranslate %}You won\'t be able to revert this!{% endblocktranslate %}',
icon: 'warning',
showCancelButton: true,
confirmButtonColor: '#3085d6',
cancelButtonColor: '#d33',
cancelButtonText: '{% blocktranslate %}Cancel{% endblocktranslate %}',
confirmButtonText: '{% blocktranslate %}Yes, delete it!{% endblocktranslate %}',
customClass: {
confirmButton: 'btn btn-primary me-3',
cancelButton: 'btn btn-danger'
},
buttonsStyling: false})
if result.isConfirmed trigger delete_confirmed"><i class="fa-solid fa-trash fa-fw"></i></a></td>
<td class="col-auto">{{ currency.code }}</td>
<td class="col">{{ currency.name }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endblock %}
+2 -2
View File
@@ -3,7 +3,7 @@
<h5 class="offcanvas-title">{% block title %}{% endblock %}</h5>
<button type="button" class="btn-close" data-bs-dismiss="offcanvas" aria-label="Close"></button>
</div>
<div id="generic-offcanvas-body" class="offcanvas-body">
<div id="generic-offcanvas-body" class="offcanvas-body"
_="install init_tom_select">
{% block body %}{% endblock %}
</div>
{% javascript_pack 'select' %}
+8
View File
@@ -0,0 +1,8 @@
{% spaceless %}
<span class="tw-text-xs text-white-50 mx-1"
data-bs-toggle="tooltip"
data-bs-title="{{ content }}"
_="install toast">
<i class="fa-solid fa-circle-question fa-fw"></i>
</span>
{% endspaceless %}
+59
View File
@@ -0,0 +1,59 @@
{% load i18n %}
{% load active_link %}
<nav class="navbar navbar-expand-lg border-bottom bg-body-tertiary" hx-boost="true">
<div class="container-fluid">
<a class="navbar-brand fw-bold text-primary font-base" href="#">WYGIWYH</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarContent" aria-controls="navbarContent" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarContent">
<ul class="navbar-nav me-auto mb-2 mb-lg-0 nav-underline" hx-push-url="true">
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle {% active_link views='monthly_overview' %}"
href="#"
role="button"
data-bs-toggle="dropdown"
aria-expanded="false">
{% translate 'Overview' %}
</a>
<ul class="dropdown-menu">
<li><a class="dropdown-item {% active_link views='monthly_overview' %}" href="{% url 'monthly_index' %}">{%translate 'Monthly' %}</a></li>
</ul>
</li>
<li class="nav-item">
<a class="nav-link {% active_link views='net_worth' %}"
href="{% url 'net_worth' %}">
{% translate 'Net Worth' %}
</a>
</li>
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle {% active_link views='tags_list||categories_list||currencies_list' %}"
href="#" role="button"
data-bs-toggle="dropdown"
aria-expanded="false">
{% translate 'Management' %}
</a>
<ul class="dropdown-menu">
<li><h6 class="dropdown-header">{% trans 'Transactions' %}</h6></li>
<li><a class="dropdown-item {% active_link views='categories_list' %}" href="{% url 'categories_list' %}">{% translate 'Categories' %}</a></li>
<li><a class="dropdown-item {% active_link views='tags_list' %}" href="{% url 'tags_list' %}">{% translate 'Tags' %}</a></li>
<li><hr class="dropdown-divider"></li>
<li><h6 class="dropdown-header">{% trans 'Accounts' %}</h6></li>
<li><a class="dropdown-item {% active_link views='accounts_list' %}" href="{% url 'accounts_list' %}">{% translate 'Accounts' %}</a></li>
<li><a class="dropdown-item {% active_link views='account_groups_list' %}" href="{% url 'account_groups_list' %}">{% translate 'Account Groups' %}</a></li>
<li><a class="dropdown-item {% active_link views='currencies_list' %}" href="{% url 'currencies_list' %}">{% translate 'Currencies' %}</a></li>
</ul>
</li>
</ul>
{% spaceless %}
<a class="mx-3 tw-text-2xl" hx-get="{% url 'toggle_amount_visibility' %}">
{% if user.settings.hide_amounts %}
<i class="fa-solid fa-eye-slash fa-fw"></i><div id="settings-hide-amounts" class="d-inline tw-invisible"></div>
{% else %}
<i class="fa-solid fa-eye fa-fw"></i>
{% endif %}</a>
{% endspaceless %}
<a class="btn btn-outline-light btn-sm" href="{% url 'logout' %}"><i class="fa-solid fa-door-open me-2"></i>{% translate 'Logout' %}</a>
</div>
</div>
</nav>
+8 -2
View File
@@ -1,10 +1,16 @@
<div id="generic-offcanvas" class="offcanvas offcanvas-end offcanvas-size-xl" data-bs-backdrop="static"
<div id="generic-offcanvas" class="offcanvas offcanvas-end offcanvas-size-xl"
data-bs-backdrop="static"
tabindex="-1"
_="on htmx:afterSettle call bootstrap.Offcanvas.getOrCreateInstance(me).show() end
on hide_offcanvas call bootstrap.Offcanvas.getOrCreateInstance(me).hide() end
on htmx:beforeOnLoad[detail.boosted] call bootstrap.Offcanvas.getOrCreateInstance(me).hide() then log event
on hidden.bs.offcanvas set my innerHTML to '' end">
</div>
<div id="generic-offcanvas-left" class="offcanvas offcanvas-start offcanvas-size-xl" data-bs-backdrop="static"
<div id="generic-offcanvas-left" class="offcanvas offcanvas-start offcanvas-size-xl"
data-bs-backdrop="static"
tabindex="-1"
_="on htmx:afterSettle call bootstrap.Offcanvas.getOrCreateInstance(me).show() end
on hide_offcanvas call bootstrap.Offcanvas.getOrCreateInstance(me).hide() end
on htmx:beforeOnLoad[detail.boosted] call bootstrap.Offcanvas.getOrCreateInstance(me).hide() then log event
on hidden.bs.offcanvas set my innerHTML to '' end">
</div>
+18 -4
View File
@@ -1,6 +1,20 @@
{% load webpack_loader %}
{% javascript_pack 'bootstrap' %}
{% javascript_pack 'sweetalert2' %}
{% javascript_pack 'htmx' %}
{% javascript_pack 'jquery' %}
{% javascript_pack 'bootstrap' attrs="defer" %}
{% javascript_pack 'sweetalert2' attrs="defer" %}
{% javascript_pack 'select' attrs="defer" %}
{% include 'includes/scripts/hyperscript/init_tom_select.html' %}
{% include 'includes/scripts/hyperscript/hide_amount.html' %}
{% include 'includes/scripts/hyperscript/toast.html' %}
{% include 'includes/scripts/hyperscript/htmx_error_handler.html' %}
{% javascript_pack 'htmx' attrs="defer" %}
<script>
let tz = Intl.DateTimeFormat().resolvedOptions().timeZone;
if (!tz) {
tz = "UTC"
}
document.cookie = "mytz=" + tz + ";path=/";
</script>
@@ -0,0 +1,23 @@
<script type="text/hyperscript">
behavior hide_amounts
on load or htmx:afterSwap if I include #settings-hide-amounts
set elements to <.amount/> in me
for el in elements
set el.textContent to '•••••••••••'
end
end
on load or htmx:afterSwap if I do not include #settings-hide-amounts
set elements to <.amount/> in me
for el in elements
set el.textContent to el.dataset.originalValue
end
end
on click[target matches .amount] if I include #settings-hide-amounts
if event.target do not matches .revealed then set event.target.textContent to event.target.dataset.originalValue
else set event.target.textContent to '•••••••••••' end
then toggle .revealed on event.target
end
end
</script>
@@ -0,0 +1,8 @@
<script type="text/hyperscript">
behavior htmx_error_handler
on htmx:responseError or htmx:afterRequest[detail.failed] remove .invisible .visually-hidden .swing-in-top-fwd from
#loading-error then log event
on htmx:afterRequest[detail.successful] add .invisible .visually-hidden .swing-in-top-fwd to #loading-error then
log 'oi'
end
</script>
@@ -1,24 +0,0 @@
<script type="text/hyperscript">
behavior initTomSelect
init
set selectmultiple to .selectmultiple in me
set select to .select in me
set selectcsv to .csvselect in me
for x in selectmultiple
js(it)
new TomSelect(it, tomselect_multiple)
end
end
for x in select
js(it)
new TomSelect(it, tomselect_single)
end
end
for x in selectcsv
js(it)
new TomSelect(it, tomselect_single)
end
end
end
end
</script>
@@ -0,0 +1,12 @@
<script type="text/hyperscript">
behavior init_tom_select
init
set selects to <select/> in me
for x in selects
js(it)
TomSelect(it)
end
end
end
end
</script>
@@ -0,0 +1,6 @@
<script type="text/hyperscript">
behavior toast
on mouseenter call bootstrap.Tooltip.getOrCreateInstance(me).show() end
on mouseleave or click call bootstrap.Tooltip.getOrCreateInstance(me).dispose() end
end
</script>
+1 -1
View File
@@ -1,3 +1,3 @@
<div id="toasts" hx-get="{% url 'toasts' %}"
hx-trigger="load, toast from:window">
</div>
</div>
+29 -10
View File
@@ -1,26 +1,45 @@
{% load i18n %}
{% load title %}
{% load webpack_loader %}
{% load tz_detect %}
<!doctype html>
<html lang="en" data-bs-theme="dark">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{% block title %}WYGIWYH{% endblock %}</title>
<title>
{% filter site_title %}
{% block title %}
{% endblock title %}
{% endfilter %}
</title>
{% include 'includes/styles.html' %}
{% block extra_styles %}{% endblock %}
{% tz_detect %}
{% include 'includes/scripts.html' %}
{% block extra_js_head %}{% endblock %}
</head>
<body>
<div id="content">
{% block content %}{% endblock %}
<body class="font-monospace">
<div _="install hide_amounts
install htmx_error_handler
{% block body_hyperscript %}{% endblock %}">
{% include 'includes/navbar.html' %}
<div class="container invisible visually-hidden" id="loading-error">
<div class="alert alert-danger m-3" role="alert">
{% translate 'Something went wrong loading your data. Try reloading the page or check the console for more information.' %}
</div>
</div>
<div id="content">
{% block content %}{% endblock %}
</div>
</div>
{% include 'includes/offcanvas.html' %}
{% include 'includes/toasts.html' %}
{% include 'includes/scripts.html' %}
{% block extra_js %}{% endblock %}
{% block extra_js_body %}{% endblock %}
</body>
</html>
+28
View File
@@ -0,0 +1,28 @@
{% load title %}
{% load webpack_loader %}
<!doctype html>
<html lang="en" data-bs-theme="dark">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>
{% filter site_title %}
{% block title %}
{% endblock title %}
{% endfilter %}
</title>
{% include 'includes/styles.html' %}
{% block extra_styles %}{% endblock %}
</head>
<body>
<div id="content">
{% block content %}{% endblock %}
</div>
{% include 'includes/toasts.html' %}
{% include 'includes/scripts.html' %}
{% block extra_js %}{% endblock %}
</body>
</html>
@@ -0,0 +1,38 @@
{% load natural %}
{% load i18n %}
{% regroup transactions by date|customnaturaldate as transactions_by_date %}
<div>
{% for x in transactions_by_date %}
<div>
<div class="my-3 w-100 tw-text-base border-bottom bg-body">
<a class="text-decoration-none d-inline-block w-100"
role="button"
data-bs-toggle="collapse"
data-bs-target="#{{ x.grouper|slugify }}"
aria-expanded="true"
aria-controls="collapseExample">
{{ x.grouper }}
</a>
</div>
<div class="collapse show" id="{{ x.grouper|slugify }}">
<div class="ps-3">
{% for trans in x.list %}
{% include 'transactions/fragments/item.html' with transaction=trans %}
{% endfor %}
</div>
</div>
</div>
{% empty %}
<div class="row p-5">
<div class="col p-5">
<div class="text-center">
<i class="fa-solid fa-circle-xmark tw-text-6xl"></i>
<p class="lead mt-4 mb-0">{% translate "No transactions this month" %}</p>
<p class="tw-text-gray-500">{% translate "Try adding one" %}</p>
</div>
</div>
</div>
{% endfor %}
</div>
@@ -0,0 +1,49 @@
{% extends 'extends/offcanvas.html' %}
{% load month_name %}
{% load i18n %}
{% block title %}{% translate 'Pick a month' %}{% endblock %}
{% block body %}
{% regroup month_year_data by year as years_list %}
<ul class="nav nav-pills nav-fill" id="yearTabs" role="tablist">
{% for x in years_list %}
<li class="nav-item" role="presentation">
<button class="nav-link{% if x.grouper == current_year %} active{% endif %}"
id="{{ x.grouper }}"
data-bs-toggle="tab"
data-bs-target="#{{ x.grouper }}-pane"
type="button"
role="tab"
aria-controls="{{ x.grouper }}-pane"
aria-selected="{% if x.grouper == current_year %}true{% else %}false{% endif %}">
{{ x.grouper }}
</button>
</li>
{% endfor %}
</ul>
<div class="tab-content" id="yearTabsContent" hx-boost="true">
{% for x in years_list %}
<div class="tab-pane fade{% if x.grouper == current_year %} show active{% endif %} mt-2"
id="{{ x.grouper }}-pane"
role="tabpanel"
aria-labelledby="{{ x.grouper }}"
tabindex="0">
<ul class="list-group list-group-flush" id="month-year-list">
{% for month_data in x.list %}
<li class="list-group-item hover:tw-bg-zinc-900
{% if month_data.month == current_month and month_data.year == current_year %} disabled bg-primary{% endif %}"
{% if month_data.month == current_month and month_data.year == current_year %}aria-disabled="true"{% endif %}>
<a class="text-decoration-none stretched-link {% if month_data.month == current_month and month_data.year == current_year %} text-black{% endif %}"
href="{% url "monthly_overview" month=month_data.month year=month_data.year %}">
{{ month_data.month|month_name }}
<span class="badge text-bg-secondary">{{ month_data.transaction_count }}</span>
</a>
</li>
{% endfor %}
</ul>
</div>
{% endfor %}
</div>
{% endblock %}
@@ -0,0 +1,166 @@
{% load i18n %}
{% load currency_display %}
<div class="row row-cols-1 g-4 mb-3">
{# Daily Spending#}
<div class="col">
<div class="card tw-relative h-100 shadow">
<div class="tw-absolute tw-h-8 tw-w-8 tw-right-2 tw-top-2 tw-bg-yellow-300 tw-text-yellow-800 text-center
align-items-center d-flex justify-content-center rounded-2">
<i class="fa-solid fa-calendar-day"></i>
</div>
<div class="card-body">
<h5 class="tw-text-yellow-400 fw-bold">{% translate 'Daily Spending Allowance' %}{% include 'includes/help_icon.html' with content=_('This is the final total divided by the remaining days in the month') %}</h5>
<div class="d-flex justify-content-between mt-3">
<div class="text-end font-monospace">
<div class="tw-text-gray-400">{% translate 'today' %}</div>
</div>
<div class="text-start font-monospace">
{% for entry in totals.daily_spending_allowance %}
<div class="amount" data-original-value="{% entry_amount entry %}"></div>
{% empty %}
<div>-</div>
{% endfor %}
</div>
</div>
</div>
</div>
</div>
{# Income#}
<div class="col">
<div class="card tw-relative h-100 shadow">
<div class="tw-absolute tw-h-8 tw-w-8 tw-right-2 tw-top-2 tw-bg-green-300 tw-text-green-800 text-center
align-items-center d-flex justify-content-center rounded-2">
<i class="fa-solid fa-arrow-right-to-bracket"></i>
</div>
<div class="card-body">
<h5 class="tw-text-green-400 fw-bold">{% translate 'Income' %}</h5>
<div class="d-flex justify-content-between mt-3">
<div class="text-end font-monospace">
<div class="tw-text-gray-400">current</div>
</div>
<div class="d-flex justify-content-between text-start font-monospace">
{% for entry in totals.paid_income %}
<div class="amount" data-original-value="{% entry_amount entry %}"></div>
{% empty %}
<div>-</div>
{% endfor %}
</div>
</div>
<hr class="my-1">
<div class="d-flex justify-content-between">
<div class="text-end font-monospace">
<div class="tw-text-gray-400">projected</div>
</div>
<div class="text-start font-monospace">
{% for entry in totals.projected_income %}
<div class="amount" data-original-value="{% entry_amount entry %}"></div>
{% empty %}
<div>-</div>
{% endfor %}
</div>
</div>
</div>
</div>
</div>
{# Expenses#}
<div class="col">
<div class="card tw-relative h-100 shadow">
<div class="tw-absolute tw-h-8 tw-w-8 tw-right-2 tw-top-2 tw-bg-red-300 tw-text-red-800 text-center
align-items-center d-flex justify-content-center rounded-2">
<i class="fa-solid fa-arrow-right-from-bracket"></i>
</div>
<div class="card-body">
<h5 class="tw-text-red-400">{% translate 'Expenses' %}</h5>
<div class="d-flex justify-content-between mt-3">
<div class="text-end font-monospace">
<div class="tw-text-gray-400">current</div>
</div>
<div class="text-start font-monospace">
{% for entry in totals.paid_expenses %}
<div class="amount" data-original-value="{% entry_amount entry %}"></div>
{% empty %}
<div>-</div>
{% endfor %}
</div>
</div>
<hr class="my-1">
<div class="d-flex justify-content-between">
<div class="text-end font-monospace">
<div class="tw-text-gray-400">projected</div>
</div>
<div class="text-start font-monospace">
{% for entry in totals.projected_expenses %}
<div class="amount" data-original-value="{% entry_amount entry %}"></div>
{% empty %}
<div>-</div>
{% endfor %}
</div>
</div>
</div>
</div>
</div>
{# Total#}
<div class="col">
<div class="card tw-relative h-100 shadow">
<div class="tw-absolute tw-h-8 tw-w-8 tw-right-2 tw-top-2 tw-bg-blue-300 tw-text-blue-800 text-center
align-items-center d-flex justify-content-center rounded-2">
<i class="fa-solid fa-scale-balanced"></i>
</div>
<div class="card-body">
<h5 class="tw-text-blue-400">{% translate 'Total' %}</h5>
<div class="d-flex justify-content-between mt-3">
<div class="text-end font-monospace">
<div class="tw-text-gray-400">current</div>
</div>
<div class="text-start font-monospace">
{% for entry in totals.total_current %}
<div class="amount" data-original-value="{% entry_amount entry %}"></div>
{% empty %}
<div>-</div>
{% endfor %}
</div>
</div>
<div class="d-flex justify-content-between mt-3">
<div class="text-end font-monospace">
<div class="tw-text-gray-400">projected</div>
</div>
<div class="text-start font-monospace">
{% for entry in totals.total_projected %}
<div class="amount" data-original-value="{% entry_amount entry %}"></div>
{% empty %}
<div>-</div>
{% endfor %}
</div>
</div>
<hr class="my-1">
<div class="d-flex justify-content-end">
<div class="text-start font-monospace">
{% for entry in totals.total_final %}
<div class="amount" data-original-value="{% entry_amount entry %}"></div>
{% empty %}
<div>-</div>
{% endfor %}
</div>
</div>
</div>
</div>
</div>
</div>
{#<div class="p-2 rounded-2 shadow tw-text-sm card mt-4">#}
{# <p class="font-monospace text-light text-uppercase text-center fw-bold m-0 tw-text-base">#}
{# {% translate "Account Overview" %}</p>#}
{# <hr class="my-1">#}
{# <div>#}
{# {% for account in account_summary %}#}
{# <div class="row">#}
{# <div class="col-6">#}
{# <div class="font-monospace text-primary text-start align-self-end fw-bold m-0">{{ account.name }}</div>#}
{# </div>#}
{# <div class="col-6 text-end font-monospace">#}
{# <div class="amount" data-original-value="{% currency_display amount=account.balance prefix=account.currency__prefix suffix=account.currency__suffix decimal_places=account.currency__decimal_places %}"></div>#}
{# </div>#}
{# </div>#}
{# <div class="my-1"></div>#}
{# {% endfor %}#}
{# </div>#}
{#</div>#}
@@ -0,0 +1,125 @@
{% extends "layouts/base.html" %}
{% load crispy_forms_tags %}
{% load i18n %}
{% load month_name %}
{% load static %}
{% load webpack_loader %}
{% block title %}Monthly Overview :: {{ month|month_name }}/{{ year }}{% endblock %}
{% block body_hyperscript %}
on keyup[code is 'KeyE' and target.nodeName is 'BODY'] from body trigger 'add_expense' end
on keyup[code is 'KeyI' and target.nodeName is 'BODY'] from body trigger 'add_income' end
on keyup[code is 'KeyB' and target.nodeName is 'BODY'] from body trigger 'balance' end
on keyup[code is 'KeyT' and target.nodeName is 'BODY'] from body trigger 'add_transfer' end
on keyup[code is 'KeyN' and target.nodeName is 'BODY'] from body trigger 'installment' end
on keyup[code is 'ArrowLeft' and target.nodeName is 'BODY'] from body trigger 'previous_month' end
on keyup[code is 'ArrowRight' and target.nodeName is 'BODY'] from body trigger 'next_month' end
{% endblock %}
{% block content %}
<div class="container px-md-3 py-3 column-gap-5">
<div class="row mb-3 gx-xl-4 gy-3 mb-4">
{# Date picker#}
<div class="col-12 col-xl-4 flex-row align-items-center d-flex">
<div class="tw-text-base h-100 align-items-center d-flex">
<a role="button"
class="pe-4 py-2"
hx-boost="true"
hx-trigger="click, previous_month from:window"
href="{% url 'monthly_overview' month=previous_month year=previous_year %}"><i
class="fa-solid fa-chevron-left"></i></a>
</div>
<div class="tw-text-3xl fw-bold font-monospace tw-w-full text-center"
hx-get="{% url 'available_dates' %}"
hx-target="#generic-offcanvas-left"
hx-trigger="click, date_picker from:window"
hx-vals='{"month": {{ month }}, "year": {{ year }}}' role="button">
{{ month|month_name }} {{ year }}
</div>
<div class="tw-text-base mx-2 h-100 align-items-center d-flex">
<a role="button"
class="ps-3 py-2"
hx-boost="true"
hx-trigger="click, next_month from:window"
href="{% url 'monthly_overview' month=next_month year=next_year %}">
<i class="fa-solid fa-chevron-right"></i>
</a>
</div>
</div>
{# Action buttons#}
<div class="col-12 col-xl-8">
<div class="d-grid gap-2 d-xl-flex justify-content-xl-end">
<button class="btn btn-sm btn-outline-success"
hx-get="{% url 'transaction_add' %}"
hx-target="#generic-offcanvas"
hx-trigger="click, add_income from:window"
hx-vals='{"year": {{ year }}, "month": {{ month }}, "type": "IN"}'>
<i class="fa-solid fa-arrow-right-to-bracket me-2"></i>
{% translate "Income" %}
</button>
<button class="btn btn-sm btn-outline-danger"
hx-get="{% url 'transaction_add' %}"
hx-target="#generic-offcanvas"
hx-trigger="click, add_expense from:window"
hx-vals='{"year": {{ year }}, "month": {{ month }}, "type": "EX"}'>
<i class="fa-solid fa-arrow-right-from-bracket me-2"></i>
{% translate "Expense" %}
</button>
<button class="btn btn-sm btn-outline-warning"
hx-get="{% url 'installments_add' %}"
hx-trigger="click, installment from:window"
hx-target="#generic-offcanvas">
<i class="fa-solid fa-divide me-2"></i>
{% translate "Installment" %}
</button>
<button class="btn btn-sm btn-outline-info"
hx-get="{% url 'transactions_transfer' %}"
hx-target="#generic-offcanvas"
hx-trigger="click, add_transfer from:window"
hx-vals='{"year": {{ year }}, "month": {{ month }}}'>
<i class="fa-solid fa-money-bill-transfer me-2"></i>
{% translate "Transfer" %}
</button>
<button class="btn btn-sm btn-outline-info"
hx-get="{% url 'account_reconciliation' %}"
hx-trigger="click, balance from:window"
hx-target="#generic-offcanvas">
<i class="fa-solid fa-scale-balanced me-2"></i>
{% translate "Balance" %}
</button>
</div>
</div>
</div>
{# Monthly summary#}
<div class="row gx-xl-4 gy-3">
<div class="col-12 col-xl-4 order-0 order-xl-2">
<div id="summary" hx-get="{% url 'monthly_summary' month=month year=year %}" class=" sticky-sidebar"
hx-trigger="load, transaction_updated from:window, monthly_summary_update from:window">
</div>
</div>
<div class="col-12 col-xl-8 order-2 order-xl-1">
{# Filter transactions#}
<div class="row mb-1">
<div class="col-12">
<div class="dropdown">
<button type="button" class="btn btn-sm btn-outline-primary dropdown-toggle" data-bs-toggle="dropdown"
aria-expanded="false" data-bs-auto-close="false">
<i class="fa-solid fa-filter fa-fw me-2"></i>{% translate 'Filter transactions' %}
</button>
<form hx-get="{% url 'monthly_transactions_list' month=month year=year %}" hx-trigger="change, submit, search"
hx-target="#transactions" id="filter" hx-indicator="#transactions" class="dropdown-menu p-4
w-lg-50 tw-w-full lg:tw-w-2/4"
_="install init_tom_select">
{% crispy filter.form %}
</form>
</div>
</div>
</div>
<div id="transactions"
hx-get="{% url 'monthly_transactions_list' month=month year=year %}"
hx-trigger="load, transaction_updated from:window" hx-include="#filter"></div>
</div>
</div>
</div>
{% endblock %}
+133
View File
@@ -0,0 +1,133 @@
{% extends "layouts/base.html" %}
{% load currency_display %}
{% load crispy_forms_tags %}
{% load i18n %}
{% load month_name %}
{% load static %}
{% load webpack_loader %}
{% block title %}Net Worth{% endblock %}
{% block content %}
<div class="container px-md-3 py-3 column-gap-5">
<div class="row">
<div class="col-6 row-gap-5">
{% for currency in currency_net_worth %}
<div class="row">
<div class="card-body">
<div class="card-title">
<div class="row">
<div class="col-6">
{{ currency.name }}
</div>
<div class="col-6 text-end">
<div class="amount" data-original-value="{% currency_display amount=currency.amount prefix=currency.prefix suffix=currency.suffix decimal_places=currency.decimal_places %}"></div>
</div>
</div>
</div>
</div>
</div>
{% endfor %}
</div>
<div class="col-6">oi</div>
</div>
</div>
{#<canvas id="chart" width="500" height="300"></canvas>#}
{##}
{#<script>#}
{# let ctx = document.getElementById("chart").getContext("2d");#}
{##}
{#let chart = new Chart(ctx, {#}
{# type: 'line',#}
{# responsive: true,#}
{# data: {#}
{# labels: ["2020/01", "2020/02", "2020/03", "2020/04", "2020/05"],#}
{# datasets: [#}
{# {#}
{# label: "Gross volume ($)",#}
{# data: [0.0, 0.8457, 1, 0.5],#}
{# yAxisID: 'y1',#}
{# },#}
{# {#}
{# label: "Gross volume ($) 2",#}
{# data: [85000, 89000, 96000, 100000],#}
{# yAxisID: 'y2',#}
{# }#}
{# ]#}
{# },#}
{# options: {#}
{# title: {#}
{# text: "Gross Volume in 2020",#}
{# display: true#}
{# },#}
{# responsive: true,#}
{# interaction: {#}
{# mode: 'index',#}
{# intersect: false,#}
{# scales: {#}
{# y1: {#}
{# ticks: {#}
{# display: false // This hides the y-axis labels#}
{# }#}
{# }#}
{# }#}
{# },#}
{# },#}
{# scales: {#}
{# x: {#}
{# display: true,#}
{# ticks: {#}
{# display: false // This hides the y-axis labels#}
{# }#}
{# },#}
{# y1: {#}
{# display: true,#}
{# type: 'logarithmic',#}
{# ticks: {#}
{# display: false // This hides the y-axis labels#}
{# }#}
{# },#}
{# y2: {#}
{# type: 'logarithmic',#}
{# display: false,#}
{# position: 'left',#}
{# ticks: {#}
{# display: false,#}
{# },#}
{##}
{# // grid line settings#}
{# grid: {#}
{# display: false,#}
{# drawOnChartArea: false, // only want the grid lines for one axis to show up#}
{# }},}#}
{#});#}
{#new Chart(ctx, {#}
{# type: 'line', // e.g., 'line', 'bar', etc.#}
{# data: {#}
{# labels: ["2020/01", "2020/02", "2020/03", "2020/04", "2020/05"],#}
{# datasets: [#}
{# {#}
{# label: "Gross volume ($)",#}
{# data: [0.0, 0.8457, 1, 0.5],#}
{# yAxisID: 'y1',#}
{# },#}
{# {#}
{# label: "Gross volume ($) 2",#}
{# data: [85000, 89000, 96000, 100000],#}
{# yAxisID: 'y2',#}
{# }#}
{# ]#}
{# },#}
{# options: {#}
{# scales: {#}
{# y: {#}
{# ticks: {#}
{# display: false // This hides the y-axis labels#}
{# }#}
{# }#}
{# }#}
{# }#}
{#});#}
{#</script>#}
{% endblock %}
+11
View File
@@ -0,0 +1,11 @@
{% extends 'extends/offcanvas.html' %}
{% load i18n %}
{% load crispy_forms_tags %}
{% block title %}{% translate 'Add tag' %}{% endblock %}
{% block body %}
<form hx-post="{% url 'tag_add' %}" hx-target="#generic-offcanvas" novalidate>
{% crispy form %}
</form>
{% endblock %}
+11
View File
@@ -0,0 +1,11 @@
{% extends 'extends/offcanvas.html' %}
{% load i18n %}
{% load crispy_forms_tags %}
{% block title %}{% translate 'Edit tag' %}{% endblock %}
{% block body %}
<form hx-post="{% url 'tag_edit' tag_id=tag.id %}" hx-target="#generic-offcanvas" novalidate>
{% crispy form %}
</form>
{% endblock %}
+78
View File
@@ -0,0 +1,78 @@
{% extends "layouts/base.html" %}
{% load i18n %}
{% block title %}{% translate 'Tags' %}{% endblock %}
{% block content %}
<div class="container px-md-3 py-3 column-gap-5">
<div class="tw-text-3xl fw-bold font-monospace tw-w-full mb-3">
{% spaceless %}
<div>{% translate 'Tags' %}<span>
<a class="text-decoration-none tw-text-2xl p-1 category-action"
role="button"
data-bs-toggle="tooltip"
data-bs-title="{% translate "Add" %}"
hx-get="{% url 'tag_add' %}"
hx-target="#generic-offcanvas"
_="">
<i class="fa-solid fa-circle-plus fa-fw"></i></a>
</span></div>
{% endspaceless %}
</div>
<div class="border p-3 rounded-3">
<table class="table table-hover table-responsive">
<thead>
<tr>
<th scope="col" class="col-auto"></th>
<th scope="col" class="col">{% translate 'Name' %}</th>
</tr>
</thead>
<tbody>
{% for tag in tags %}
<tr class="tag">
<td class="col-auto">
<a class="text-decoration-none tw-text-gray-400 p-1 tag-action"
role="button"
data-bs-toggle="tooltip"
data-bs-title="{% translate "Edit" %}"
hx-get="{% url 'tag_edit' tag_id=tag.id %}"
hx-target="#generic-offcanvas"
_="on click send action_clicked to .tag-action in the closest parent .tag end
on action_clicked call bootstrap.Tooltip.getOrCreateInstance(me).dispose() end
install toast">
<i class="fa-solid fa-pencil fa-fw"></i></a>
<a class="text-danger text-decoration-none p-1 tag-action"
role="button"
data-bs-toggle="tooltip"
data-bs-title="{% translate "Delete" %}"
hx-delete="{% url 'tag_delete' tag_id=tag.id %}"
hx-trigger='delete_confirmed'
_="on click send action_clicked to .tag-action in the closest parent .tag end
on action_clicked call bootstrap.Tooltip.getOrCreateInstance(me).dispose() end
install toast
on click
if event.ctrlKey trigger delete_confirmed
else
call Swal.fire({title: '{% translate 'Are you sure?' %}',
text: '{% blocktranslate %}You won\'t be able to revert this!{% endblocktranslate %}',
icon: 'warning',
showCancelButton: true,
confirmButtonColor: '#3085d6',
cancelButtonColor: '#d33',
cancelButtonText: '{% blocktranslate %}Cancel{% endblocktranslate %}',
confirmButtonText: '{% blocktranslate %}Yes, delete it!{% endblocktranslate %}',
customClass: {
confirmButton: 'btn btn-primary me-3',
cancelButton: 'btn btn-danger'
},
buttonsStyling: false})
if result.isConfirmed trigger delete_confirmed"><i class="fa-solid fa-trash fa-fw"></i></a></td>
<td class="col">{{ tag.name }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endblock %}
@@ -6,7 +6,6 @@
{% block body %}
<form hx-post="{% url 'transaction_add' %}" hx-target="#generic-offcanvas" novalidate>
{% csrf_token %}
{% crispy form %}
</form>
{% endblock %}
@@ -0,0 +1,11 @@
{% extends 'extends/offcanvas.html' %}
{% load i18n %}
{% load crispy_forms_tags %}
{% block title %}{% translate 'Add Installment Plan' %}{% endblock %}
{% block body %}
<form hx-post="{% url 'installments_add' %}" hx-target="#generic-offcanvas" novalidate>
{% crispy form %}
</form>
{% endblock %}
@@ -6,7 +6,6 @@
{% block body %}
<form hx-post="{% url 'transaction_edit' transaction_id=transaction.id %}" hx-target="#generic-offcanvas" novalidate>
{% csrf_token %}
{% crispy form %}
</form>
{% endblock %}
@@ -0,0 +1,11 @@
{% extends 'extends/offcanvas.html' %}
{% load i18n %}
{% load crispy_forms_tags %}
{% block title %}{% translate 'Edit transaction' %}{% endblock %}
{% block body %}
<form hx-post="{% url 'installments_edit' pk=installment_plan.id %}" hx-target="#generic-offcanvas" novalidate>
{% crispy form %}
</form>
{% endblock %}
+77 -53
View File
@@ -1,69 +1,103 @@
{% load i18n %}
{% load currency_display %}
<div class="tw-border-s-2 tw-border-e-0 tw-border-t-0 tw-border-b-0 border-bottom
<div class="tw-border-s-6 tw-border-e-0 tw-border-t-0 tw-border-b-0 border-bottom
hover:tw-bg-zinc-900 p-2 {% if transaction.account.is_asset %}tw-border-dashed{% else %}tw-border-solid{% endif %}
{% if transaction.type == "EX" %}tw-border-red-500{% else %}tw-border-green-500{% endif %} transaction tw-relative"
hx-trigger="dblclick"
hx-get="{% url 'transaction_edit' transaction_id=transaction.id %}"
hx-target="#generic-offcanvas"
_="on mouseover remove .tw-invisible from the first .transaction-actions in me end
on mouseout add .tw-invisible to the first .transaction-actions in me end">
{% if transaction.type == "EX" %}tw-border-red-500{% else %}tw-border-green-500{% endif %} transaction tw-relative
my-3"
_="on mouseenter remove .tw-invisible from the first .transaction-actions in me end
on mouseleave add .tw-invisible to the first .transaction-actions in me end">
<div class="row font-monospace tw-text-sm align-items-center">
<div class="col-lg-1 col-12 d-flex align-items-center tw-text-2xl lg:tw-text-xl text-lg-center">
<div class="col-lg-1 col-12 d-flex align-items-center tw-text-2xl lg:tw-text-xl text-lg-center pay-button">
<a class="text-decoration-none my-2 w-100"
role="button"
hx-get="{% url 'transaction_pay' transaction_id=transaction.id %}"
hx-target="closest .transaction"
hx-swap="outerHTML">
hx-swap="outerHTML"
_="on paid
js
paidSound.pause()
paidSound.currentTime = 0
paidSound.play()
end
end
on unpaid
js
unpaidSound.pause()
unpaidSound.currentTime = 0
unpaidSound.play()
end
end">
{% if transaction.is_paid %}<i class="fa-regular fa-circle-check tw-text-green-400"></i>{% else %}<i
class="fa-regular fa-circle tw-text-red-400"></i>{% endif %}
</a>
</div>
<div class="col-lg-8 col-12">
{# Date#}
<div class="mb-1 tw-text-gray-400">
<i class="fa-solid fa-calendar fa-fw me-1 fa-xs"></i>{{ transaction.date|date:"SHORT_DATE_FORMAT" }}</div>
<div class="mb-1 text-white tw-text-base">{{ transaction.description }}</div>
<div class="text-white-50 mb-2 d-flex align-items-center">
<i class="fa-solid fa-calendar fa-fw me-1 fa-xs"></i>
{{ transaction.date|date:"SHORT_DATE_FORMAT" }}</div>
<div class="text-white-50 mb-1 tw-text-xs">
{% for tag in transaction.tags.all %}
<span><i class="fa-solid fa-hashtag tw-text-gray-400"></i>{{ tag.name }}</span>
{% endfor %}
</div>
<div class="tw-text-sm mb-1 mb-lg-0 tw-text-gray-400 font-normal">
<i class="fa-solid fa-note-sticky me-1"></i>{{ transaction.notes | linebreaksbr }}
</div>
<div class="tw-text-sm mb-1 mb-lg-0">
{% if transaction.category %}
<i class="fa-solid fa-icons text-primary"></i>
<span class="badge rounded-0
{% if transaction.category.mute %}text-bg-secondary{% else %}text-bg-light{% endif %}">
{{ transaction.category.name }}
</span>
<div class="tw-text-gray-400 tw-text-sm">
{# Notes#}
{% if transaction.notes %}
<div class="mb-1 tw-text-gray-400">
<i class="fa-solid fa-align-left fa-fw me-1 fa-xs"></i>{{ transaction.notes | linebreaksbr }}
</div>
{% endif %}
{% if transaction.category %}
<div class="mb-1">
{% spaceless %}
<i class="fa-solid fa-icons fa-fw me-1 fa-xs"></i>
<span class="">{{ transaction.category.name }}</span>
{% endspaceless %}
</div>
{% endif %}
{# Tags#}
{% with transaction.tags.all as tags %}
{% if tags %}
<div class="mb-1">
{% for tag in tags %}
<span><i class="fa-solid fa-hashtag fa-fw fa-xs"></i>{{ tag.name }}</span>
{% endfor %}
</div>
{% endif %}
{% endwith %}
</div>
</div>
<div class="col-lg-3 col-12 text-lg-end align-self-end">
<div class="{% if transaction.type == "EX" %}tw-text-red-400{% else %}tw-text-green-400{% endif %}">
{% transaction_amount transaction %}
<div class="amount" data-original-value="{% transaction_amount transaction %}"></div>
</div>
{# Exchange Rate#}
{% with exchanged=transaction.exchanged_amount %}
{% if exchanged %}
<div class="tw-text-gray-500 amount"
data-original-value="{% currency_display amount=exchanged.amount prefix=exchanged.prefix suffix=exchanged.suffix decimal_places=exchanged.decimal_places %}">
</div>
{% endif %}
{% endwith %}
<div>{{ transaction.account.name }}</div>
</div>
<div>
<div class="transaction-actions !tw-absolute tw-top-0 tw-right-0 tw-invisible tw-text-base d-none
d-xl-flex">
{# Item actions#}
<div class="transaction-actions !tw-absolute tw-left-2/4 tw--top-1/3 tw-invisible d-none
d-xl-flex flex-xl-row tw-text-base card">
<a class="text-decoration-none tw-text-gray-400 p-2 transaction-action"
role="button"
data-bs-toggle="tooltip"
data-bs-title="{% translate "Edit" %}"
hx-get="{% url 'transaction_edit' transaction_id=transaction.id %}"
hx-target="#generic-offcanvas"
_="install toast">
<i class="fa-solid fa-pencil fa-fw"></i></a>
<a class="text-danger text-decoration-none p-2 transaction-action"
role="button"
data-bs-toggle="tooltip"
data-bs-title="{% translate "Delete" %}"
hx-get="{% url 'transaction_delete' transaction_id=transaction.id %}"
hx-trigger='confirmed'
_="on click send transaction_action_clicked to .transaction-action in the closest parent .transaction end
on transaction_action_clicked call bootstrap.Tooltip.getOrCreateInstance(me).dispose() end
on mouseenter call bootstrap.Tooltip.getOrCreateInstance(me) end
on mouseleave call bootstrap.Tooltip.getOrCreateInstance(me).dispose() end
hx-delete="{% url 'transaction_delete' transaction_id=transaction.id %}"
hx-trigger='delete_confirmed'
_="install toast
on click
if event.shiftKey trigger confirmed
if event.ctrlKey trigger delete_confirmed
else
call Swal.fire({title: '{% translate 'Are you sure?' %}',
text: '{% blocktranslate %}You won\'t be able to revert this!{% endblocktranslate %}',
@@ -78,19 +112,9 @@ hover:tw-bg-zinc-900 p-2 {% if transaction.account.is_asset %}tw-border-dashed{%
cancelButton: 'btn btn-danger'
},
buttonsStyling: false})
if result.isConfirmed trigger confirmed"><i class="fa-solid fa-trash fa-fw"></i></a>
<a class="text-decoration-none tw-text-gray-400 p-2 transaction-action"
role="button"
data-bs-toggle="tooltip"
data-bs-title="{% translate "Edit" %}"
hx-get="{% url 'transaction_edit' transaction_id=transaction.id %}"
hx-target="#generic-offcanvas"
_="on click send transaction_action_clicked to .transaction-action in the closest parent .transaction end
on transaction_action_clicked call bootstrap.Tooltip.getOrCreateInstance(me).dispose() end
on mouseenter call bootstrap.Tooltip.getOrCreateInstance(me) end
on mouseleave call bootstrap.Tooltip.getOrCreateInstance(me).dispose() end">
<i class="fa-solid fa-pencil fa-fw"></i></a>
if result.isConfirmed trigger delete_confirmed"><i class="fa-solid fa-trash fa-fw"></i></a>
</div>
{# Item actions dropdown fallback for mobile#}
<div class="dropdown !tw-absolute tw-top-0 tw-right-0 xl:tw-invisible">
<a class="btn tw-text-2xl lg:tw-text-sm lg:tw-border-none text-light" type="button"
data-bs-toggle="dropdown"
@@ -106,8 +130,8 @@ hover:tw-bg-zinc-900 p-2 {% if transaction.account.is_asset %}tw-border-dashed{%
</a></li>
<li><a class="dropdown-item"
role="button"
hx-get="{% url 'transaction_delete' transaction_id=transaction.id %}"
hx-trigger='confirmed'
hx-delete="{% url 'transaction_delete' transaction_id=transaction.id %}"
hx-trigger='delete_confirmed'
_="on click
call Swal.fire({title: '{% translate 'Are you sure?' %}',
text: '{% blocktranslate %}You won\'t be able to revert this!{% endblocktranslate %}',
@@ -122,7 +146,7 @@ hover:tw-bg-zinc-900 p-2 {% if transaction.account.is_asset %}tw-border-dashed{%
cancelButton: 'btn btn-danger'
},
buttonsStyling: false})
if result.isConfirmed trigger confirmed">
if result.isConfirmed trigger delete_confirmed">
<i class="fa-solid fa-trash me-3"></i>{% translate "Delete" %}
</a></li>
</ul>
@@ -1,16 +0,0 @@
{% load i18n %}
<div>
{% for trans in transactions %}
{% include 'transactions/fragments/item.html' with transaction=trans %}
{% empty %}
<div class="row p-5">
<div class="col p-5">
<div class="text-center">
<i class="fa-solid fa-circle-xmark tw-text-6xl"></i>
<p class="lead mt-4 mb-0">{% translate "No transactions this month" %}</p>
<p class="tw-text-gray-500">{% translate "Try adding one" %}</p>
</div>
</div>
</div>
{% endfor %}
</div>
@@ -1,46 +0,0 @@
{% extends 'extends/offcanvas.html' %}
{% load month_name %}
{% load i18n %}
{% block title %}{% translate 'Pick one' %}{% endblock %}
{% block body %}
<ul class="nav nav-pills nav-fill" id="yearTabs" role="tablist">
{% for date in available_years %}
<li class="nav-item" role="presentation">
<button class="nav-link{% if date.year == current_year %} active{% endif %}"
id="{{ date.year }}"
data-bs-toggle="tab"
data-bs-target="#{{ date.year }}-pane"
type="button"
role="tab"
aria-controls="{{ date.year }}-pane"
aria-selected="{% if date.year == current_year %}true{% else %}false{% endif %}">
{{ date.year }}
</button>
</li>
{% endfor %}
</ul>
<div class="tab-content" id="yearTabsContent">
{% for date in available_years %}
<div class="tab-pane fade{% if date.year == current_year %} show active{% endif %} mt-2"
id="{{ date.year }}-pane"
role="tabpanel"
aria-labelledby="{{ date.year }}"
tabindex="0">
<ul class="list-group list-group-flush" id="month-year-list">
{% for month in months %}
<li class="list-group-item hover:tw-bg-zinc-900
{% if month == current_month and date.year == current_year %} disabled bg-primary{% endif %}"
{% if month == current_month and date.year == current_year %}aria-disabled="true"{% endif %}>
<a class="text-decoration-none stretched-link {% if month == current_month and date.year == current_year %} text-black{% endif %}"
href="{% url "transactions_overview" month=month year=date.year %}">
{{ month|month_name }}
</a>
</li>
{% endfor %}
</ul>
</div>
{% endfor %}
</div>
{% endblock %}
@@ -1,135 +0,0 @@
{% load i18n %}
{% load currency_display %}
<div class="text-bg-secondary p-2 rounded-2 shadow tw-text-sm">
<p class="font-monospace text-light text-uppercase text-center fw-bold m-0 tw-text-base">{% translate "Summary" %}</p>
<hr class="my-1">
<div>
<p class="font-monospace text-uppercase text-center fw-bold m-0">{% translate "Income" %}</p>
<div class="row">
<div class="col-6">
<div class="font-monospace text-primary text-start align-self-end fw-bold m-0">
{% translate "Current Income" %}</div>
</div>
<div class="col-6 text-end font-monospace">
{% for entry in totals.paid_income %}
<div>{% entry_amount entry %}</div>
{% empty %}
<div>-</div>
{% endfor %}
</div>
</div>
<div class="my-3"></div>
<div class="row">
<div class="col-6">
<div class="font-monospace text-primary text-start align-self-end fw-bold m-0">
{% translate "Projected Income" %}
</div>
</div>
<div class="col-6 text-end font-monospace">
{% for entry in totals.projected_income %}
<div>{% entry_amount entry %}</div>
{% empty %}
<div>-</div>
{% endfor %}
</div>
</div>
</div>
<hr class="my-1">
<div>
<p class="font-monospace text-uppercase text-center fw-bold m-0">{% translate "Expenses" %}</p>
<div class="row">
<div class="col-6">
<div class="font-monospace text-primary text-start align-self-end fw-bold m-0">
{% translate "Current Expenses" %}</div>
</div>
<div class="col-6 text-end font-monospace">
{% for entry in totals.paid_expenses %}
<div>{% entry_amount entry %}</div>
{% empty %}
<div>-</div>
{% endfor %}
</div>
</div>
<div class="my-3"></div>
<div class="row">
<div class="col-6">
<div class="font-monospace text-primary text-start align-self-end fw-bold m-0">
{% translate "Projected Expenses" %}
</div>
</div>
<div class="col-6 text-end font-monospace">
{% for entry in totals.projected_expenses %}
<div>{% entry_amount entry %}</div>
{% empty %}
<div>-</div>
{% endfor %}
</div>
</div>
</div>
<hr class="my-1">
<div>
<p class="font-monospace text-uppercase text-center fw-bold m-0">{% translate "Totals" %}</p>
<div class="row">
<div class="col-6">
<div class="font-monospace text-primary text-start align-self-end fw-bold m-0">
{% translate "Current Total" %}
</div>
</div>
<div class="col-6 text-end font-monospace">
{% for entry in totals.total_current %}
<div>{% entry_amount entry %}</div>
{% empty %}
<div>-</div>
{% endfor %}
</div>
</div>
<div class="my-3"></div>
<div class="row">
<div class="col-6">
<div class="font-monospace text-primary text-start align-self-end fw-bold m-0">
{% translate "Projected Total" %}
</div>
</div>
<div class="col-6 text-end font-monospace">
{% for entry in totals.total_projected %}
<div>{% entry_amount entry %}</div>
{% empty %}
<div>-</div>
{% endfor %}
</div>
</div>
<div class="my-3"></div>
<div class="row">
<div class="col-6">
<div class="font-monospace text-primary text-start align-self-end fw-bold m-0">
{% translate "Final Total" %}
</div>
</div>
<div class="col-6 text-end font-monospace">
{% for entry in totals.total_final %}
<div>{% entry_amount entry %}</div>
{% empty %}
<div>-</div>
{% endfor %}
</div>
</div>
</div>
<hr class="my-1">
<div>
<div class="row">
<div class="col-6">
<div class="font-monospace text-primary text-start align-self-end fw-bold m-0">{% translate "Daily Spending Allowance" %}</div>
</div>
<div class="col-6 text-end font-monospace">
{% for entry in totals.daily_spending_allowance %}
<div>{% entry_amount entry %}</div>
{% empty %}
<div>-</div>
{% endfor %}
</div>
</div>
</div>
</div>
@@ -6,7 +6,6 @@
{% block body %}
<form hx-post="{% url 'transactions_transfer' %}" hx-target="#generic-offcanvas" novalidate>
{% csrf_token %}
{% crispy form %}
</form>
{% endblock %}
-79
View File
@@ -1,79 +0,0 @@
{% extends "layouts/base.html" %}
{% load i18n %}
{% load month_name %}
{% load static %}
{% load webpack_loader %}
{% block content %}
<div class="container-fluid px-md-3 py-5 column-gap-5">
<div class="row mb-3">
<div class="col flex-row align-items-center d-flex">
<div class="tw-text-base mx-2 h-100 align-items-center d-flex">
<a role="button" href="
{% url 'transactions_overview' month=previous_month year=previous_year %}"><i
class="fa-solid fa-chevron-left"></i></a>
</div>
<div class="tw-text-3xl fw-bold font-monospace tw-w-full xl:tw-w-72 text-center"
hx-get="{% url 'available_dates' %}"
hx-target="#generic-offcanvas-left"
hx-vals='{"month": {{ month }}, "year": {{ year }}}' role="button">
{{ month|month_name }} {{ year }}
</div>
<div class="tw-text-base mx-2 h-100 align-items-center d-flex">
<a role="button"
href="{% url 'transactions_overview' month=next_month year=next_year %}">
<i class="fa-solid fa-chevron-right"></i>
</a>
</div>
</div>
</div>
<div class="row gx-xl-4 gy-3">
<div class="col-12 col-xl-3 order-1 order-xl-0">
<div class="row">
<div class="col-6 p-1">
<button class="btn btn-sm btn-outline-success w-100"
hx-get="{% url 'transaction_add' %}"
hx-target="#generic-offcanvas"
hx-vals='{"year": {{ year }}, "month": {{ month }}, "type": "IN"}'>
<i class="fa-solid fa-circle-plus me-3"></i>
{% translate "Income" %}
</button>
</div>
<div class="col-6 p-1">
<button class="btn btn-sm btn-outline-danger w-100"
hx-get="{% url 'transaction_add' %}"
hx-target="#generic-offcanvas"
hx-vals='{"year": {{ year }}, "month": {{ month }}, "type": "EX"}'>
<i class="fa-solid fa-circle-plus me-3"></i>
{% translate "Expense" %}
</button>
</div>
<div class="col-6 p-1">
<button class="btn btn-sm btn-outline-info w-100"
hx-get="{% url 'transactions_transfer' %}"
hx-target="#generic-offcanvas"
hx-vals='{"year": {{ year }}, "month": {{ month }}}'>
<i class="fa-solid fa-money-bill-transfer me-3"></i>
{% translate "Transfer" %}
</button>
</div>
</div>
</div>
<div class="col-12 col-xl-6 order-2 order-xl-1"
id="transactions"
hx-get="{% url 'transactions_list' month=month year=year %}"
hx-trigger="load, transaction_updated from:window">
</div>
<div class="col-12 col-xl-3 order-0 order-xl-2">
<div id="total-expenses" hx-get="{% url 'monthly_summary' month=month year=year %}"
hx-trigger="load, transaction_updated from:window" class="sticky-sidebar">
</div>
{# <div id="total-spent" hx-get="{% url 'monthly_expenses' month=month year=year %}"#}
{# hx-trigger="load, transaction_updated from:window" hx-vals='{"s": "p"}'>#}
{# </div>#}
</div>
</div>
</div>
{% endblock %}
@@ -1,16 +1,28 @@
{% load crispy_forms_field %}
<div class="btn-group w-100 mb-3" role="group" aria-label="{{ field.label }}">
{% for choice in field.field.choices %}
<input type="radio"
class="btn-check"
name="{{ field.html_name }}"
id="{{ field.html_name }}_{{ forloop.counter }}"
value="{{ choice.0 }}"
{% if choice.0 == field.value %}checked{% endif %}>
<label class="btn {% if forloop.first %}btn-outline-success{% elif forloop.last %}btn-outline-danger{% else %}btn-outline-primary{% endif %}"
for="{{ field.html_name }}_{{ forloop.counter }}">
{{ choice.1 }}
</label>
{% endfor %}
<div class="form-group mb-3">
<div class="btn-group w-100" role="group" aria-label="{{ field.label }}">
{% for choice in field.field.choices %}
<input type="radio"
class="btn-check"
name="{{ field.html_name }}"
id="{{ field.html_name }}_{{ forloop.counter }}_tr"
value="{{ choice.0 }}"
{% if choice.0 == field.value %}checked{% endif %}>
<label class="btn {% if forloop.first %}btn-outline-success{% elif forloop.last %}btn-outline-danger{% else %}btn-outline-primary{% endif %} {% if field.errors %}is-invalid{% endif %}"
for="{{ field.html_name }}_{{ forloop.counter }}_tr">
{{ choice.1 }}
</label>
{% endfor %}
</div>
{% if field.errors %}
<div class="invalid-feedback d-block">
{% for error in field.errors %}
{{ error }}
{% endfor %}
</div>
{% endif %}
{% if field.help_text %}
<small class="form-text text-muted">{{ field.help_text }}</small>
{% endif %}
</div>
@@ -0,0 +1,23 @@
{% load crispy_forms_field %}
<div class="btn-group w-100 mb-3"
role="group"
aria-label="{{ field.label }}"
_="on click
if no <input[type='checkbox']:checked/> in me
set nextCheckbox to the next <input[type='checkbox']/> from event.target within me with wrapping
call nextCheckbox.click()
end">
{% for choice in field.field.choices %}
<input type="checkbox"
class="btn-check"
name="{{ field.html_name }}"
id="{{ field.html_name }}_{{ forloop.counter }}"
value="{{ choice.0 }}"
{% if choice.0 in field.value %}checked{% endif %}>
<label class="btn btn-outline-dark"
for="{{ field.html_name }}_{{ forloop.counter }}">
{{ choice.1 }}
</label>
{% endfor %}
</div>
+21
View File
@@ -0,0 +1,21 @@
{% extends "layouts/base_auth.html" %}
{% load crispy_forms_tags %}
{% block title %}Login{% endblock %}
{% block content %}
<div>
<div class="container">
<div class="row vh-100 d-flex justify-content-center align-items-center">
<div class="col-md-6 col-xl-4 col-12">
<div class="card shadow-lg">
<div class="card-body">
<h2 class="card-title text-center mb-4">Login</h2>
{% crispy form %}
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}