mirror of
https://github.com/eitchtee/WYGIWYH.git
synced 2026-05-27 18:09:25 +02:00
Compare commits
81 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f80f74a01a | |||
| 368342853f | |||
| 9ef8fdec49 | |||
| f29a8d8bc0 | |||
| 2cdcc4ee26 | |||
| f90a31f2b9 | |||
| dd1f6a6ef2 | |||
| c09ad0e49d | |||
| 9250131396 | |||
| 5f503149ce | |||
| d45b4f2942 | |||
| 4a8493c7d9 | |||
| c39c3ccacb | |||
| 4bb8ef6582 | |||
| d711ccca69 | |||
| 76d59f1038 | |||
| 5b6c123fa1 | |||
| 782ab11ae4 | |||
| 8db885f47d | |||
| 01bd8710d8 | |||
| 569d08711c | |||
| a285f055e4 | |||
| 6aae9b1207 | |||
| 9d2206f8a4 | |||
| d7e3c50c2c | |||
| 789fd4eb80 | |||
| 586b3a5d44 | |||
| 9248e8bd77 | |||
| c44247f6a5 | |||
| 8ba89434f8 | |||
| f2f41981a3 | |||
| 1153fd6b0a | |||
| 76822224a0 | |||
| 31b2b98eb9 | |||
| d7a4e79321 | |||
| 985f07e792 | |||
| 5465bb1eeb | |||
| 451a85a998 | |||
| 54c74e7c07 | |||
| d6e9e123b7 | |||
| 80c9c43a02 | |||
| 3e34f088fc | |||
| 5b9e5c6003 | |||
| c266b8809f | |||
| 8cda4116bc | |||
| c2510b2261 | |||
| dcdaf756f9 | |||
| 50ca08165a | |||
| f85618fa01 | |||
| 635f87a8ad | |||
| 1a073ba53d | |||
| 5412e5b12c | |||
| 2103ba1b38 | |||
| 04fb15224c | |||
| 2fc526beac | |||
| cc3ca4f4a3 | |||
| 8d3844c431 | |||
| 5e7e918085 | |||
| c3f02320b5 | |||
| da8bbbfb0b | |||
| e3f74538d2 | |||
| d8234950c6 | |||
| 58f19ce1ca | |||
| ef5f3580a0 | |||
| efe0f99cb4 | |||
| dccb5079ad | |||
| 6c90150661 | |||
| c33d6fab69 | |||
| c0c57a6d77 | |||
| f19d58a2bd | |||
| dfe99093e9 | |||
| d737e573cc | |||
| 805d3f419e | |||
| 9777aac746 | |||
| 61b782104d | |||
| 79dec2b515 | |||
| db23e162c4 | |||
| d81d89d9f6 | |||
| 6826cfe79a | |||
| 0832ec75ca | |||
| 3090f632de |
@@ -29,15 +29,15 @@ Managing money can feel unnecessarily complex, but it doesn’t have to be. WYGI
|
||||
|
||||
By sticking to this straightforward approach, you avoid dipping into your savings while still keeping tabs on where your money goes.
|
||||
|
||||
While this philosophy is simple, finding tools to make it work wasn’t. I initially used a spreadsheet, which served me well for years—until it became unwieldy as I started managing multiple currencies, accounts, and investments. I tried various financial management apps, but none met my key requirements:
|
||||
While this philosophy is simple, finding tools to make it work wasn’t. I initially used a spreadsheet, which served me well for years, until it became unwieldy as I started managing multiple currencies, accounts, and investments. I tried various financial management apps, but none met my key requirements:
|
||||
|
||||
1. **Multi-currency support** to track income and expenses in different currencies.
|
||||
2. **Not a budgeting app** — as I dislike budgeting constraints.
|
||||
2. **Not a budgeting app** as I dislike budgeting constraints.
|
||||
3. **Web app usability** (ideally with mobile support, though optional).
|
||||
4. **Automation-ready API** to integrate with other tools and services.
|
||||
5. **Custom transaction rules** for credit card billing cycles or similar quirks.
|
||||
|
||||
Frustrated by the lack of comprehensive options, I set out to build **WYGIWYH** — an opinionated yet powerful tool that I believe will resonate with like-minded users.
|
||||
Frustrated by the lack of comprehensive options, I set out to build **WYGIWYH**, an opinionated yet powerful tool that I believe will resonate with like-minded users.
|
||||
|
||||
# Key Features
|
||||
|
||||
|
||||
@@ -487,6 +487,8 @@ else:
|
||||
|
||||
CACHALOT_UNCACHABLE_TABLES = ("django_migrations", "procrastinate_jobs")
|
||||
|
||||
# Procrastinate
|
||||
PROCRASTINATE_ON_APP_READY = "apps.common.procrastinate.on_app_ready"
|
||||
|
||||
# PWA
|
||||
PWA_APP_NAME = SITE_TITLE
|
||||
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
# Generated by Django 5.2.4 on 2025-07-28 02:15
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('accounts', '0014_alter_account_options_alter_accountgroup_options'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='account',
|
||||
name='owner',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s_owned', to=settings.AUTH_USER_MODEL, verbose_name='Owner'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='account',
|
||||
name='shared_with',
|
||||
field=models.ManyToManyField(blank=True, related_name='%(class)s_shared', to=settings.AUTH_USER_MODEL, verbose_name='Shared with users'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='account',
|
||||
name='visibility',
|
||||
field=models.CharField(choices=[('private', 'Private'), ('public', 'Public')], default='private', max_length=10, verbose_name='Visibility'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='accountgroup',
|
||||
name='owner',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s_owned', to=settings.AUTH_USER_MODEL, verbose_name='Owner'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='accountgroup',
|
||||
name='shared_with',
|
||||
field=models.ManyToManyField(blank=True, related_name='%(class)s_shared', to=settings.AUTH_USER_MODEL, verbose_name='Shared with users'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='accountgroup',
|
||||
name='visibility',
|
||||
field=models.CharField(choices=[('private', 'Private'), ('public', 'Public')], default='private', max_length=10, verbose_name='Visibility'),
|
||||
),
|
||||
]
|
||||
@@ -1,7 +1,26 @@
|
||||
from django.contrib import admin
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
|
||||
@admin.action(description=_("Make public"))
|
||||
def make_public(modeladmin, request, queryset):
|
||||
queryset.update(visibility="public")
|
||||
|
||||
|
||||
@admin.action(description=_("Make private"))
|
||||
def make_private(modeladmin, request, queryset):
|
||||
queryset.update(visibility="private")
|
||||
|
||||
|
||||
class SharedObjectModelAdmin(admin.ModelAdmin):
|
||||
actions = [make_public, make_private]
|
||||
|
||||
list_display = ("__str__", "visibility", "owner", "get_shared_with")
|
||||
|
||||
@admin.display(description=_("Shared with users"))
|
||||
def get_shared_with(self, obj):
|
||||
return ", ".join([p.email for p in obj.shared_with.all()])
|
||||
|
||||
def get_queryset(self, request):
|
||||
# Use the all_objects manager to show all transactions, including deleted ones
|
||||
return self.model.all_objects.all()
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
from django.apps import AppConfig
|
||||
from django.core.cache import cache
|
||||
|
||||
|
||||
class CommonConfig(AppConfig):
|
||||
@@ -18,3 +19,7 @@ class CommonConfig(AppConfig):
|
||||
admin.site.unregister(SocialAccount)
|
||||
admin.site.unregister(SocialApp)
|
||||
admin.site.unregister(SocialToken)
|
||||
|
||||
# Delete the cache for update checks to prevent false-positives when the app is restarted
|
||||
# this will be recreated by the check_for_updates task
|
||||
cache.delete("update_check")
|
||||
|
||||
@@ -36,12 +36,19 @@ class SharedObject(models.Model):
|
||||
related_name="%(class)s_owned",
|
||||
null=True,
|
||||
blank=True,
|
||||
verbose_name=_("Owner"),
|
||||
)
|
||||
visibility = models.CharField(
|
||||
max_length=10, choices=Visibility.choices, default=Visibility.private
|
||||
max_length=10,
|
||||
choices=Visibility.choices,
|
||||
default=Visibility.private,
|
||||
verbose_name=_("Visibility"),
|
||||
)
|
||||
shared_with = models.ManyToManyField(
|
||||
settings.AUTH_USER_MODEL, related_name="%(class)s_shared", blank=True
|
||||
settings.AUTH_USER_MODEL,
|
||||
related_name="%(class)s_shared",
|
||||
blank=True,
|
||||
verbose_name=_("Shared with users"),
|
||||
)
|
||||
|
||||
# Use as abstract base class
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
import procrastinate
|
||||
|
||||
|
||||
def on_app_ready(app: procrastinate.App):
|
||||
"""This function is ran upon procrastinate initialization."""
|
||||
...
|
||||
@@ -1,13 +1,17 @@
|
||||
import logging
|
||||
from packaging.version import parse as parse_version, InvalidVersion
|
||||
|
||||
from asgiref.sync import sync_to_async
|
||||
from django.conf import settings
|
||||
from django.core import management
|
||||
from django.db import DEFAULT_DB_ALIAS
|
||||
from django.core.cache import cache
|
||||
|
||||
from procrastinate import builtin_tasks
|
||||
from procrastinate.contrib.django import app
|
||||
|
||||
import requests
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -79,3 +83,46 @@ def reset_demo_data(timestamp=None):
|
||||
except Exception as e:
|
||||
logger.exception(f"Error during daily demo data reset: {e}")
|
||||
raise
|
||||
|
||||
|
||||
@app.periodic(cron="0 */12 * * *") # Every 12 hours
|
||||
@app.task(
|
||||
name="check_for_updates",
|
||||
)
|
||||
def check_for_updates(timestamp=None):
|
||||
url = "https://api.github.com/repos/eitchtee/WYGIWYH/releases/latest"
|
||||
|
||||
try:
|
||||
response = requests.get(url, timeout=60)
|
||||
response.raise_for_status() # Raise an exception for bad status codes (4xx or 5xx)
|
||||
|
||||
data = response.json()
|
||||
latest_version = data.get("tag_name")
|
||||
|
||||
if latest_version:
|
||||
try:
|
||||
current_v = parse_version(settings.APP_VERSION)
|
||||
except InvalidVersion:
|
||||
current_v = parse_version("0.0.0")
|
||||
try:
|
||||
latest_v = parse_version(latest_version)
|
||||
except InvalidVersion:
|
||||
latest_v = parse_version("0.0.0")
|
||||
|
||||
update_info = {
|
||||
"update_available": False,
|
||||
"current_version": str(current_v),
|
||||
"latest_version": str(latest_v),
|
||||
}
|
||||
|
||||
if latest_v > current_v:
|
||||
update_info["update_available"] = True
|
||||
|
||||
# Cache the entire dictionary
|
||||
cache.set("update_check", update_info, 60 * 60 * 25)
|
||||
logger.info(f"Update check complete. Result: {update_info}")
|
||||
else:
|
||||
logger.warning("Could not find 'tag_name' in GitHub API response.")
|
||||
|
||||
except requests.exceptions.RequestException as e:
|
||||
logger.error(f"Failed to fetch updates from GitHub: {e}")
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
# core/templatetags/update_tags.py
|
||||
from django import template
|
||||
from django.core.cache import cache
|
||||
|
||||
register = template.Library()
|
||||
|
||||
|
||||
@register.simple_tag
|
||||
def get_update_check():
|
||||
"""
|
||||
Retrieves the update status dictionary from the cache.
|
||||
Returns a default dictionary if nothing is found.
|
||||
"""
|
||||
return cache.get("update_check") or {
|
||||
"update_available": False,
|
||||
"latest_version": "N/A",
|
||||
}
|
||||
@@ -37,6 +37,7 @@ class ArbitraryDecimalDisplayNumberInput(forms.TextInput):
|
||||
"x-data": "",
|
||||
"x-mask:dynamic": f"$money($input, '{get_format('DECIMAL_SEPARATOR')}', "
|
||||
f"'{get_format('THOUSAND_SEPARATOR')}', '30')",
|
||||
"x-on:keyup": "$el.dispatchEvent(new Event('input'))",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
# Generated by Django 5.2.4 on 2025-07-28 02:15
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('dca', '0003_dcastrategy_owner_dcastrategy_shared_with_and_more'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='dcastrategy',
|
||||
name='owner',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s_owned', to=settings.AUTH_USER_MODEL, verbose_name='Owner'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='dcastrategy',
|
||||
name='shared_with',
|
||||
field=models.ManyToManyField(blank=True, related_name='%(class)s_shared', to=settings.AUTH_USER_MODEL, verbose_name='Shared with users'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='dcastrategy',
|
||||
name='visibility',
|
||||
field=models.CharField(choices=[('private', 'Private'), ('public', 'Public')], default='private', max_length=10, verbose_name='Visibility'),
|
||||
),
|
||||
]
|
||||
@@ -91,6 +91,8 @@ def get_transactions(request, include_unpaid=True, include_silent=False):
|
||||
transactions = transactions.filter(is_paid=True)
|
||||
|
||||
if not include_silent:
|
||||
transactions = transactions.exclude(Q(category__mute=True) & ~Q(category=None))
|
||||
transactions = transactions.exclude(
|
||||
Q(Q(category__mute=True) & ~Q(category=None)) | Q(mute=True)
|
||||
)
|
||||
|
||||
return transactions
|
||||
|
||||
@@ -260,6 +260,7 @@ def emergency_fund(request):
|
||||
reference_date__gte=start_date,
|
||||
reference_date__lte=end_date,
|
||||
category__mute=False,
|
||||
mute=False,
|
||||
)
|
||||
.values("reference_date", "account__currency")
|
||||
.annotate(monthly_total=Sum("amount"))
|
||||
|
||||
@@ -109,7 +109,7 @@ def monthly_summary(request, month: int, year: int):
|
||||
# Base queryset with all required filters
|
||||
base_queryset = Transaction.objects.filter(
|
||||
reference_date__year=year, reference_date__month=month, account__is_asset=False
|
||||
).exclude(Q(category__mute=True) & ~Q(category=None))
|
||||
).exclude(Q(Q(category__mute=True) & ~Q(category=None)) | Q(mute=True))
|
||||
|
||||
data = calculate_currency_totals(base_queryset, ignore_empty=True)
|
||||
percentages = calculate_percentage_distribution(data)
|
||||
@@ -143,7 +143,7 @@ def monthly_account_summary(request, month: int, year: int):
|
||||
base_queryset = Transaction.objects.filter(
|
||||
reference_date__year=year,
|
||||
reference_date__month=month,
|
||||
).exclude(Q(category__mute=True) & ~Q(category=None))
|
||||
).exclude(Q(Q(category__mute=True) & ~Q(category=None)) | Q(mute=True))
|
||||
|
||||
account_data = calculate_account_totals(transactions_queryset=base_queryset.all())
|
||||
account_percentages = calculate_percentage_distribution(account_data)
|
||||
@@ -168,7 +168,7 @@ def monthly_currency_summary(request, month: int, year: int):
|
||||
base_queryset = Transaction.objects.filter(
|
||||
reference_date__year=year,
|
||||
reference_date__month=month,
|
||||
).exclude(Q(category__mute=True) & ~Q(category=None))
|
||||
).exclude(Q(Q(category__mute=True) & ~Q(category=None)) | Q(mute=True))
|
||||
|
||||
currency_data = calculate_currency_totals(base_queryset.all(), ignore_empty=True)
|
||||
currency_percentages = calculate_percentage_distribution(currency_data)
|
||||
|
||||
@@ -5,4 +5,5 @@ from . import views
|
||||
urlpatterns = [
|
||||
path("net-worth/current/", views.net_worth_current, name="net_worth_current"),
|
||||
path("net-worth/projected/", views.net_worth_projected, name="net_worth_projected"),
|
||||
path("net-worth/", views.net_worth, name="net_worth"),
|
||||
]
|
||||
|
||||
+44
-113
@@ -2,7 +2,7 @@ import json
|
||||
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.core.serializers.json import DjangoJSONEncoder
|
||||
from django.shortcuts import render
|
||||
from django.shortcuts import render, redirect
|
||||
from django.views.decorators.http import require_http_methods
|
||||
|
||||
from apps.net_worth.utils.calculate_net_worth import (
|
||||
@@ -18,18 +18,38 @@ from apps.transactions.utils.calculations import (
|
||||
|
||||
@login_required
|
||||
@require_http_methods(["GET"])
|
||||
def net_worth_current(request):
|
||||
transactions_currency_queryset = Transaction.objects.filter(
|
||||
is_paid=True, account__is_archived=False
|
||||
).order_by(
|
||||
"account__currency__name",
|
||||
)
|
||||
transactions_account_queryset = Transaction.objects.filter(
|
||||
is_paid=True, account__is_archived=False
|
||||
).order_by(
|
||||
"account__group__name",
|
||||
"account__name",
|
||||
)
|
||||
def net_worth(request):
|
||||
if "view_type" in request.GET:
|
||||
print(request.GET["view_type"])
|
||||
view_type = request.GET["view_type"]
|
||||
request.session["networth_view_type"] = view_type
|
||||
else:
|
||||
view_type = request.session.get("networth_view_type", "current")
|
||||
|
||||
if view_type == "current":
|
||||
transactions_currency_queryset = Transaction.objects.filter(
|
||||
is_paid=True, account__is_archived=False
|
||||
).order_by(
|
||||
"account__currency__name",
|
||||
)
|
||||
transactions_account_queryset = Transaction.objects.filter(
|
||||
is_paid=True, account__is_archived=False
|
||||
).order_by(
|
||||
"account__group__name",
|
||||
"account__name",
|
||||
)
|
||||
else:
|
||||
transactions_currency_queryset = Transaction.objects.filter(
|
||||
account__is_archived=False
|
||||
).order_by(
|
||||
"account__currency__name",
|
||||
)
|
||||
transactions_account_queryset = Transaction.objects.filter(
|
||||
account__is_archived=False
|
||||
).order_by(
|
||||
"account__group__name",
|
||||
"account__name",
|
||||
)
|
||||
|
||||
currency_net_worth = calculate_currency_totals(
|
||||
transactions_queryset=transactions_currency_queryset, deep_search=True
|
||||
@@ -116,111 +136,22 @@ def net_worth_current(request):
|
||||
"currencies": currencies,
|
||||
"chart_data_accounts_json": chart_data_accounts_json,
|
||||
"accounts": accounts,
|
||||
"type": "current",
|
||||
"type": view_type,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@login_required
|
||||
@require_http_methods(["GET"])
|
||||
def net_worth_current(request):
|
||||
request.session["networth_view_type"] = "current"
|
||||
|
||||
return redirect("net_worth")
|
||||
|
||||
|
||||
@login_required
|
||||
@require_http_methods(["GET"])
|
||||
def net_worth_projected(request):
|
||||
transactions_currency_queryset = Transaction.objects.filter(
|
||||
account__is_archived=False
|
||||
).order_by(
|
||||
"account__currency__name",
|
||||
)
|
||||
transactions_account_queryset = Transaction.objects.filter(
|
||||
account__is_archived=False
|
||||
).order_by(
|
||||
"account__group__name",
|
||||
"account__name",
|
||||
)
|
||||
request.session["networth_view_type"] = "projected"
|
||||
|
||||
currency_net_worth = calculate_currency_totals(
|
||||
transactions_queryset=transactions_currency_queryset, deep_search=True
|
||||
)
|
||||
account_net_worth = calculate_account_totals(
|
||||
transactions_queryset=transactions_account_queryset
|
||||
)
|
||||
|
||||
historical_currency_net_worth = calculate_historical_currency_net_worth(
|
||||
queryset=transactions_currency_queryset
|
||||
)
|
||||
|
||||
labels = (
|
||||
list(historical_currency_net_worth.keys())
|
||||
if historical_currency_net_worth
|
||||
else []
|
||||
)
|
||||
currencies = (
|
||||
list(historical_currency_net_worth[labels[0]].keys())
|
||||
if historical_currency_net_worth
|
||||
else []
|
||||
)
|
||||
|
||||
datasets = []
|
||||
for i, currency in enumerate(currencies):
|
||||
data = [
|
||||
float(month_data[currency])
|
||||
for month_data in historical_currency_net_worth.values()
|
||||
]
|
||||
datasets.append(
|
||||
{
|
||||
"label": currency,
|
||||
"data": data,
|
||||
"yAxisID": f"y{i}",
|
||||
"fill": False,
|
||||
"tension": 0.1,
|
||||
}
|
||||
)
|
||||
|
||||
chart_data_currency = {"labels": labels, "datasets": datasets}
|
||||
|
||||
chart_data_currency_json = json.dumps(chart_data_currency, cls=DjangoJSONEncoder)
|
||||
|
||||
historical_account_balance = calculate_historical_account_balance(
|
||||
queryset=transactions_account_queryset
|
||||
)
|
||||
|
||||
labels = (
|
||||
list(historical_account_balance.keys()) if historical_account_balance else []
|
||||
)
|
||||
accounts = (
|
||||
list(historical_account_balance[labels[0]].keys())
|
||||
if historical_account_balance
|
||||
else []
|
||||
)
|
||||
|
||||
datasets = []
|
||||
for i, account in enumerate(accounts):
|
||||
data = [
|
||||
float(month_data[account])
|
||||
for month_data in historical_account_balance.values()
|
||||
]
|
||||
datasets.append(
|
||||
{
|
||||
"label": account,
|
||||
"data": data,
|
||||
"fill": False,
|
||||
"tension": 0.1,
|
||||
"yAxisID": f"y-axis-{i}", # Assign each dataset to its own Y-axis
|
||||
}
|
||||
)
|
||||
|
||||
chart_data_accounts = {"labels": labels, "datasets": datasets}
|
||||
|
||||
chart_data_accounts_json = json.dumps(chart_data_accounts, cls=DjangoJSONEncoder)
|
||||
|
||||
return render(
|
||||
request,
|
||||
"net_worth/net_worth.html",
|
||||
{
|
||||
"currency_net_worth": currency_net_worth,
|
||||
"account_net_worth": account_net_worth,
|
||||
"chart_data_currency_json": chart_data_currency_json,
|
||||
"currencies": currencies,
|
||||
"chart_data_accounts_json": chart_data_accounts_json,
|
||||
"accounts": accounts,
|
||||
"type": "projected",
|
||||
},
|
||||
)
|
||||
return redirect("net_worth")
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
# Generated by Django 5.2.4 on 2025-07-28 02:15
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('rules', '0013_transactionrule_on_delete'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='transactionrule',
|
||||
name='owner',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s_owned', to=settings.AUTH_USER_MODEL, verbose_name='Owner'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='transactionrule',
|
||||
name='shared_with',
|
||||
field=models.ManyToManyField(blank=True, related_name='%(class)s_shared', to=settings.AUTH_USER_MODEL, verbose_name='Shared with users'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='transactionrule',
|
||||
name='visibility',
|
||||
field=models.CharField(choices=[('private', 'Private'), ('public', 'Public')], default='private', max_length=10, verbose_name='Visibility'),
|
||||
),
|
||||
]
|
||||
@@ -7,6 +7,7 @@ from apps.transactions.models import (
|
||||
InstallmentPlan,
|
||||
RecurringTransaction,
|
||||
TransactionEntity,
|
||||
QuickTransaction,
|
||||
)
|
||||
from apps.common.admin import SharedObjectModelAdmin
|
||||
|
||||
@@ -49,19 +50,22 @@ class TransactionInline(admin.TabularInline):
|
||||
|
||||
|
||||
@admin.register(InstallmentPlan)
|
||||
class InstallmentPlanAdmin(SharedObjectModelAdmin):
|
||||
class InstallmentPlanAdmin(admin.ModelAdmin):
|
||||
inlines = [
|
||||
TransactionInline,
|
||||
]
|
||||
|
||||
|
||||
@admin.register(RecurringTransaction)
|
||||
class RecurringTransactionAdmin(SharedObjectModelAdmin):
|
||||
class RecurringTransactionAdmin(admin.ModelAdmin):
|
||||
inlines = [
|
||||
TransactionInline,
|
||||
]
|
||||
|
||||
|
||||
admin.site.register(QuickTransaction)
|
||||
|
||||
|
||||
@admin.register(TransactionCategory)
|
||||
class TransactionCategoryModelAdmin(SharedObjectModelAdmin):
|
||||
pass
|
||||
|
||||
@@ -290,11 +290,15 @@ class QuickTransactionForm(forms.ModelForm):
|
||||
"category",
|
||||
"tags",
|
||||
"entities",
|
||||
"mute",
|
||||
]
|
||||
widgets = {
|
||||
"notes": forms.Textarea(attrs={"rows": 3}),
|
||||
"account": TomSelect(clear_button=False, group_by="group"),
|
||||
}
|
||||
help_texts = {
|
||||
"mute": _("Muted transactions won't be displayed on monthly summaries")
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
@@ -356,6 +360,7 @@ class QuickTransactionForm(forms.ModelForm):
|
||||
css_class="form-row",
|
||||
),
|
||||
"notes",
|
||||
Switch("mute"),
|
||||
)
|
||||
|
||||
if self.instance and self.instance.pk:
|
||||
@@ -494,6 +499,13 @@ class TransferForm(forms.Form):
|
||||
label=_("Notes"),
|
||||
)
|
||||
|
||||
mute = forms.BooleanField(
|
||||
label=_("Mute"),
|
||||
initial=True,
|
||||
required=False,
|
||||
help_text=_("Muted transactions won't be displayed on monthly summaries"),
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
@@ -512,6 +524,7 @@ class TransferForm(forms.Form):
|
||||
),
|
||||
Field("description"),
|
||||
Field("notes"),
|
||||
Switch("mute"),
|
||||
Row(
|
||||
Column(
|
||||
Row(
|
||||
@@ -594,6 +607,8 @@ class TransferForm(forms.Form):
|
||||
return cleaned_data
|
||||
|
||||
def save(self):
|
||||
mute = self.cleaned_data["mute"]
|
||||
|
||||
from_account = self.cleaned_data["from_account"]
|
||||
to_account = self.cleaned_data["to_account"]
|
||||
from_amount = self.cleaned_data["from_amount"]
|
||||
@@ -616,6 +631,7 @@ class TransferForm(forms.Form):
|
||||
description=description,
|
||||
category=from_category,
|
||||
notes=notes,
|
||||
mute=mute,
|
||||
)
|
||||
from_transaction.tags.set(self.cleaned_data.get("from_tags", []))
|
||||
|
||||
@@ -630,6 +646,7 @@ class TransferForm(forms.Form):
|
||||
description=description,
|
||||
category=to_category,
|
||||
notes=notes,
|
||||
mute=mute,
|
||||
)
|
||||
to_transaction.tags.set(self.cleaned_data.get("to_tags", []))
|
||||
|
||||
@@ -868,7 +885,7 @@ class TransactionCategoryForm(forms.ModelForm):
|
||||
fields = ["name", "mute", "active"]
|
||||
labels = {"name": _("Category name")}
|
||||
help_texts = {
|
||||
"mute": _("Muted categories won't count towards your monthly total")
|
||||
"mute": _("Muted categories won't be displayed on monthly summaries")
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 5.1.11 on 2025-07-19 18:12
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('transactions', '0044_alter_quicktransaction_unique_together'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='transaction',
|
||||
name='mute',
|
||||
field=models.BooleanField(default=False, verbose_name='Mute'),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 5.1.11 on 2025-07-19 18:59
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('transactions', '0045_transaction_mute'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='quicktransaction',
|
||||
name='mute',
|
||||
field=models.BooleanField(default=False, verbose_name='Mute'),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,61 @@
|
||||
# Generated by Django 5.2.4 on 2025-07-28 02:15
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('transactions', '0046_quicktransaction_mute'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='transactioncategory',
|
||||
name='owner',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s_owned', to=settings.AUTH_USER_MODEL, verbose_name='Owner'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='transactioncategory',
|
||||
name='shared_with',
|
||||
field=models.ManyToManyField(blank=True, related_name='%(class)s_shared', to=settings.AUTH_USER_MODEL, verbose_name='Shared with users'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='transactioncategory',
|
||||
name='visibility',
|
||||
field=models.CharField(choices=[('private', 'Private'), ('public', 'Public')], default='private', max_length=10, verbose_name='Visibility'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='transactionentity',
|
||||
name='owner',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s_owned', to=settings.AUTH_USER_MODEL, verbose_name='Owner'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='transactionentity',
|
||||
name='shared_with',
|
||||
field=models.ManyToManyField(blank=True, related_name='%(class)s_shared', to=settings.AUTH_USER_MODEL, verbose_name='Shared with users'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='transactionentity',
|
||||
name='visibility',
|
||||
field=models.CharField(choices=[('private', 'Private'), ('public', 'Public')], default='private', max_length=10, verbose_name='Visibility'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='transactiontag',
|
||||
name='owner',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s_owned', to=settings.AUTH_USER_MODEL, verbose_name='Owner'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='transactiontag',
|
||||
name='shared_with',
|
||||
field=models.ManyToManyField(blank=True, related_name='%(class)s_shared', to=settings.AUTH_USER_MODEL, verbose_name='Shared with users'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='transactiontag',
|
||||
name='visibility',
|
||||
field=models.CharField(choices=[('private', 'Private'), ('public', 'Public')], default='private', max_length=10, verbose_name='Visibility'),
|
||||
),
|
||||
]
|
||||
@@ -299,6 +299,7 @@ class Transaction(OwnedObject):
|
||||
is_paid = models.BooleanField(default=True, verbose_name=_("Paid"))
|
||||
date = models.DateField(verbose_name=_("Date"))
|
||||
reference_date = MonthYearModelField(verbose_name=_("Reference Date"))
|
||||
mute = models.BooleanField(default=False, verbose_name=_("Mute"))
|
||||
|
||||
amount = models.DecimalField(
|
||||
max_digits=42,
|
||||
@@ -918,6 +919,7 @@ class QuickTransaction(OwnedObject):
|
||||
verbose_name=_("Type"),
|
||||
)
|
||||
is_paid = models.BooleanField(default=True, verbose_name=_("Paid"))
|
||||
mute = models.BooleanField(default=False, verbose_name=_("Mute"))
|
||||
|
||||
amount = models.DecimalField(
|
||||
max_digits=42,
|
||||
@@ -974,3 +976,6 @@ class QuickTransaction(OwnedObject):
|
||||
|
||||
self.full_clean()
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
@@ -66,6 +66,11 @@ urlpatterns = [
|
||||
views.transaction_pay,
|
||||
name="transaction_pay",
|
||||
),
|
||||
path(
|
||||
"transaction/<int:transaction_id>/mute/",
|
||||
views.transaction_mute,
|
||||
name="transaction_mute",
|
||||
),
|
||||
path(
|
||||
"transaction/<int:transaction_id>/delete/",
|
||||
views.transaction_delete,
|
||||
@@ -342,4 +347,9 @@ urlpatterns = [
|
||||
views.quick_transaction_add_as_transaction,
|
||||
name="quick_transaction_add_as_transaction",
|
||||
),
|
||||
path(
|
||||
"transactions/<int:transaction_id>/add-as-quick-transaction/",
|
||||
views.quick_transaction_add_as_quick_transaction,
|
||||
name="quick_transaction_add_as_quick_transaction",
|
||||
),
|
||||
]
|
||||
|
||||
@@ -62,7 +62,7 @@ def bulk_unpay_transactions(request):
|
||||
@login_required
|
||||
def bulk_delete_transactions(request):
|
||||
selected_transactions = request.GET.getlist("transactions", [])
|
||||
transactions = Transaction.all_objects.filter(id__in=selected_transactions)
|
||||
transactions = Transaction.objects.filter(id__in=selected_transactions)
|
||||
count = transactions.count()
|
||||
transactions.delete()
|
||||
|
||||
|
||||
@@ -129,7 +129,15 @@ def quick_transaction_add_as_transaction(request, quick_transaction_id):
|
||||
|
||||
quick_transaction_data = model_to_dict(
|
||||
quick_transaction,
|
||||
exclude=["id", "name", "owner", "account", "category", "tags", "entities"],
|
||||
exclude=[
|
||||
"id",
|
||||
"name",
|
||||
"owner",
|
||||
"account",
|
||||
"category",
|
||||
"tags",
|
||||
"entities",
|
||||
],
|
||||
)
|
||||
|
||||
new_transaction = Transaction(**quick_transaction_data)
|
||||
@@ -152,3 +160,70 @@ def quick_transaction_add_as_transaction(request, quick_transaction_id):
|
||||
"HX-Trigger": "updated, hide_offcanvas",
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@only_htmx
|
||||
@login_required
|
||||
@require_http_methods(["GET"])
|
||||
def quick_transaction_add_as_quick_transaction(request, transaction_id):
|
||||
transaction: Transaction = get_object_or_404(Transaction, pk=transaction_id)
|
||||
|
||||
if (
|
||||
transaction.description
|
||||
and QuickTransaction.objects.filter(
|
||||
name__startswith=transaction.description
|
||||
).exists()
|
||||
) or QuickTransaction.objects.filter(
|
||||
name__startswith=_("Quick Transaction")
|
||||
).exists():
|
||||
if transaction.description:
|
||||
count = QuickTransaction.objects.filter(
|
||||
name__startswith=transaction.description
|
||||
).count()
|
||||
qt_name = transaction.description + f" ({count + 1})"
|
||||
else:
|
||||
count = QuickTransaction.objects.filter(
|
||||
name__startswith=_("Quick Transaction")
|
||||
).count()
|
||||
qt_name = _("Quick Transaction") + f" ({count + 1})"
|
||||
else:
|
||||
qt_name = transaction.description or _("Quick Transaction")
|
||||
|
||||
transaction_data = model_to_dict(
|
||||
transaction,
|
||||
exclude=[
|
||||
"id",
|
||||
"name",
|
||||
"owner",
|
||||
"account",
|
||||
"category",
|
||||
"tags",
|
||||
"entities",
|
||||
"date",
|
||||
"reference_date",
|
||||
"installment_plan",
|
||||
"installment_id",
|
||||
"recurring_transaction",
|
||||
"deleted",
|
||||
"deleted_at",
|
||||
],
|
||||
)
|
||||
|
||||
new_quick_transaction = QuickTransaction(**transaction_data)
|
||||
new_quick_transaction.account = transaction.account
|
||||
new_quick_transaction.category = transaction.category
|
||||
|
||||
new_quick_transaction.name = qt_name
|
||||
|
||||
new_quick_transaction.save()
|
||||
new_quick_transaction.tags.set(transaction.tags.all())
|
||||
new_quick_transaction.entities.set(transaction.entities.all())
|
||||
|
||||
messages.success(request, _("Item added successfully"))
|
||||
|
||||
return HttpResponse(
|
||||
status=204,
|
||||
headers={
|
||||
"HX-Trigger": "toasts",
|
||||
},
|
||||
)
|
||||
|
||||
@@ -4,7 +4,7 @@ from copy import deepcopy
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.core.paginator import Paginator
|
||||
from django.db.models import Q
|
||||
from django.db.models import Q, When, Case, Value, IntegerField
|
||||
from django.http import HttpResponse, JsonResponse
|
||||
from django.shortcuts import render, get_object_or_404
|
||||
from django.utils import timezone
|
||||
@@ -388,6 +388,26 @@ def transaction_pay(request, transaction_id):
|
||||
return response
|
||||
|
||||
|
||||
@only_htmx
|
||||
@login_required
|
||||
@require_http_methods(["GET"])
|
||||
def transaction_mute(request, transaction_id):
|
||||
transaction = get_object_or_404(Transaction, pk=transaction_id)
|
||||
|
||||
new_mute = False if transaction.mute else True
|
||||
transaction.mute = new_mute
|
||||
transaction.save()
|
||||
transaction_updated.send(sender=transaction)
|
||||
|
||||
response = render(
|
||||
request,
|
||||
"transactions/fragments/item.html",
|
||||
context={"transaction": transaction, **request.GET},
|
||||
)
|
||||
response.headers["HX-Trigger"] = "selective_update"
|
||||
return response
|
||||
|
||||
|
||||
@login_required
|
||||
@require_http_methods(["GET"])
|
||||
def transaction_all_index(request):
|
||||
@@ -586,11 +606,26 @@ def get_recent_transactions(request, filter_type=None):
|
||||
# Get search term from query params
|
||||
search_term = request.GET.get("q", "").strip()
|
||||
|
||||
today = timezone.localdate(timezone.now())
|
||||
yesterday = today - timezone.timedelta(days=1)
|
||||
tomorrow = today + timezone.timedelta(days=1)
|
||||
|
||||
# Base queryset with selected fields
|
||||
queryset = (
|
||||
Transaction.objects.filter(deleted=False)
|
||||
.annotate(
|
||||
date_order=Case(
|
||||
When(date=today, then=Value(0)),
|
||||
When(date=tomorrow, then=Value(1)),
|
||||
When(date=yesterday, then=Value(2)),
|
||||
When(date__gt=tomorrow, then=Value(3)),
|
||||
When(date__lt=yesterday, then=Value(4)),
|
||||
default=Value(5),
|
||||
output_field=IntegerField(),
|
||||
)
|
||||
)
|
||||
.select_related("account", "category")
|
||||
.order_by("-created_at")
|
||||
.order_by("date_order", "date", "id")
|
||||
)
|
||||
|
||||
if filter_type:
|
||||
|
||||
@@ -3,6 +3,7 @@ from django.urls import path
|
||||
from . import views
|
||||
|
||||
urlpatterns = [
|
||||
path("yearly/", views.index, name="yearly_index"),
|
||||
path("yearly/currency/", views.index_by_currency, name="yearly_index_currency"),
|
||||
path("yearly/account/", views.index_by_account, name="yearly_index_account"),
|
||||
path(
|
||||
|
||||
@@ -16,6 +16,22 @@ from apps.transactions.utils.calculations import (
|
||||
)
|
||||
|
||||
|
||||
@login_required
|
||||
def index(request):
|
||||
if "view_type" in request.GET:
|
||||
view_type = request.GET["view_type"]
|
||||
request.session["yearly_view_type"] = view_type
|
||||
else:
|
||||
view_type = request.session.get("yearly_view_type", "currency")
|
||||
|
||||
now = timezone.localdate(timezone.now())
|
||||
|
||||
if view_type == "currency":
|
||||
return redirect(to="yearly_overview_currency", year=now.year)
|
||||
else:
|
||||
return redirect(to="yearly_overview_account", year=now.year)
|
||||
|
||||
|
||||
@login_required
|
||||
def index_by_currency(request):
|
||||
now = timezone.localdate(timezone.now())
|
||||
@@ -32,6 +48,8 @@ def index_by_account(request):
|
||||
|
||||
@login_required
|
||||
def index_yearly_overview_by_currency(request, year: int):
|
||||
request.session["yearly_view_type"] = "currency"
|
||||
|
||||
next_year = year + 1
|
||||
previous_year = year - 1
|
||||
|
||||
@@ -49,6 +67,7 @@ def index_yearly_overview_by_currency(request, year: int):
|
||||
"previous_year": previous_year,
|
||||
"months": month_options,
|
||||
"currencies": currency_options,
|
||||
"type": "currency",
|
||||
},
|
||||
)
|
||||
|
||||
@@ -75,7 +94,7 @@ def yearly_overview_by_currency(request, year: int):
|
||||
|
||||
transactions = (
|
||||
Transaction.objects.filter(**filter_params)
|
||||
.exclude(Q(category__mute=True) & ~Q(category=None))
|
||||
.exclude(Q(Q(category__mute=True) & ~Q(category=None)) | Q(mute=True))
|
||||
.order_by("account__currency__name")
|
||||
)
|
||||
|
||||
@@ -95,6 +114,7 @@ def yearly_overview_by_currency(request, year: int):
|
||||
|
||||
@login_required
|
||||
def index_yearly_overview_by_account(request, year: int):
|
||||
request.session["yearly_view_type"] = "account"
|
||||
next_year = year + 1
|
||||
previous_year = year - 1
|
||||
|
||||
@@ -115,6 +135,7 @@ def index_yearly_overview_by_account(request, year: int):
|
||||
"previous_year": previous_year,
|
||||
"months": month_options,
|
||||
"accounts": account_options,
|
||||
"type": "account",
|
||||
},
|
||||
)
|
||||
|
||||
@@ -141,7 +162,7 @@ def yearly_overview_by_account(request, year: int):
|
||||
|
||||
transactions = (
|
||||
Transaction.objects.filter(**filter_params)
|
||||
.exclude(Q(category__mute=True) & ~Q(category=None))
|
||||
.exclude(Q(Q(category__mute=True) & ~Q(category=None)) | Q(mute=True))
|
||||
.order_by(
|
||||
"account__group__name",
|
||||
"account__name",
|
||||
|
||||
+297
-260
File diff suppressed because it is too large
Load Diff
+271
-225
File diff suppressed because it is too large
Load Diff
+299
-241
File diff suppressed because it is too large
Load Diff
+322
-299
File diff suppressed because it is too large
Load Diff
+276
-232
File diff suppressed because it is too large
Load Diff
+279
-225
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+271
-225
File diff suppressed because it is too large
Load Diff
+273
-225
File diff suppressed because it is too large
Load Diff
@@ -52,7 +52,7 @@
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="show-loading" hx-get="{% url 'calendar_list' month=month year=year %}"
|
||||
hx-trigger="load, updated from:window, selective_update from:window"></div>
|
||||
hx-trigger="load, updated from:window, selective_update from:window, every 10m"></div>
|
||||
</div>
|
||||
</div>
|
||||
<c-ui.transactions_fab></c-ui.transactions_fab>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<div class="tw:min-h-16">
|
||||
<div
|
||||
id="fab-wrapper"
|
||||
class="tw:fixed tw:bottom-5 tw:right-5 tw:ml-auto tw:w-max tw:flex tw:flex-col tw:items-end mt-5">
|
||||
class="tw:fixed tw:bottom-5 tw:right-5 tw:ml-auto tw:w-max tw:flex tw:flex-col tw:items-end mt-5 tw:z-20">
|
||||
<div
|
||||
id="menu"
|
||||
class="tw:flex tw:flex-col tw:items-end tw:space-y-6 tw:transition-all tw:duration-300 tw:ease-in-out tw:opacity-0 tw:invisible tw:hidden tw:mb-2">
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
<li class="tw:lg:hidden tw:lg:group-hover:block">
|
||||
<div class="d-flex align-items-center" data-bs-toggle="collapse" href="#{{ title|slugify }}" role="button"
|
||||
aria-expanded="false" aria-controls="{{ title|slugify }}">
|
||||
<span
|
||||
class="text-muted small fw-bold text-uppercase tw:lg:hidden tw:lg:group-hover:inline me-2">{{ title }}</span>
|
||||
<hr class="flex-grow-1"/>
|
||||
<i class="fas fa-chevron-down text-muted tw:lg:before:hidden tw:lg:group-hover:before:inline tw:ml-2 tw:lg:ml-0 tw:lg:group-hover:ml-2"></i>
|
||||
</div>
|
||||
</li>
|
||||
<div class="collapse tw:lg:hidden tw:lg:group-hover:block" id="{{ title|slugify }}">
|
||||
{{ slot }}
|
||||
</div>
|
||||
@@ -0,0 +1,6 @@
|
||||
<li>
|
||||
<div class="d-flex align-items-center">
|
||||
<span class="text-muted small fw-bold text-uppercase tw:lg:hidden tw:lg:group-hover:inline me-2">{{ title }}</span>
|
||||
<hr class="flex-grow-1"/>
|
||||
</div>
|
||||
</li>
|
||||
@@ -0,0 +1,14 @@
|
||||
{% load active_link %}
|
||||
<li>
|
||||
<a href="{% url url %}"
|
||||
class="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=active css_class="sidebar-active" %}"
|
||||
{% if tooltip %}
|
||||
data-bs-placement="right"
|
||||
data-bs-toggle="tooltip"
|
||||
data-bs-title="{{ tooltip }}"
|
||||
{% endif %}>
|
||||
<i class="{{ icon }} fa-fw"></i>
|
||||
<span
|
||||
class="ms-3 fw-medium tw:lg:invisible tw:lg:group-hover:visible tw:lg:group-hover:truncate tw:lg:group-focus:truncate tw:lg:group-hover:text-ellipsis tw:lg:group-focus:text-ellipsis">{{ title }}</span>
|
||||
</a>
|
||||
</li>
|
||||
@@ -0,0 +1,16 @@
|
||||
{% load active_link %}
|
||||
<li>
|
||||
<a href="{{ url }}"
|
||||
hx-boost="false"
|
||||
class="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=active css_class="sidebar-active" %}"
|
||||
{% if tooltip %}
|
||||
data-bs-placement="right"
|
||||
data-bs-toggle="tooltip"
|
||||
data-bs-title="{{ tooltip }}"
|
||||
{% endif %}>
|
||||
|
||||
<i class="{{ icon }} fa-fw"></i>
|
||||
<span
|
||||
class="ms-3 fw-medium tw:lg:invisible tw:lg:group-hover:visible tw:lg:group-hover:truncate tw:lg:group-focus:truncate tw:lg:group-hover:text-ellipsis tw:lg:group-focus:text-ellipsis">{{ title }}</span>
|
||||
</a>
|
||||
</li>
|
||||
@@ -1,6 +1,6 @@
|
||||
{% load markdown %}
|
||||
{% load i18n %}
|
||||
<div class="transaction {% if transaction.type == "EX" %}expense{% else %}income{% endif %}">
|
||||
<div class="transaction {% if transaction.type == "EX" %}expense{% else %}income{% endif %} tw:group/transaction tw:relative tw:hover:z-10">
|
||||
<div class="d-flex my-1">
|
||||
{% if not disable_selection %}
|
||||
<label class="px-3 d-flex align-items-center justify-content-center">
|
||||
@@ -9,11 +9,9 @@
|
||||
</label>
|
||||
{% endif %}
|
||||
<div class="tw:border-s-4 tw:border-e-0 tw:border-t-0 tw:border-b-0 border-bottom
|
||||
tw:hover: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 %} tw:relative
|
||||
w-100 transaction-item"
|
||||
_="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">
|
||||
tw:hover: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 %} tw:relative
|
||||
w-100 transaction-item">
|
||||
<div class="row font-monospace tw:text-sm align-items-center">
|
||||
<div
|
||||
class="col-lg-auto col-12 d-flex align-items-center tw:text-2xl tw:lg:text-xl text-lg-center text-center p-0 ps-1">
|
||||
@@ -35,7 +33,7 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="col-lg col-12">
|
||||
<div class="col-lg col-12 {% if transaction.category.mute or transaction.mute %}tw:brightness-80{% endif %}">
|
||||
{# Date#}
|
||||
<div class="row mb-2 mb-lg-1 tw:text-gray-400">
|
||||
<div class="col-auto pe-1"><i class="fa-solid fa-calendar fa-fw me-1 fa-xs"></i></div>
|
||||
@@ -48,13 +46,13 @@
|
||||
<span class="{% if transaction.description %}me-2{% endif %}">{{ transaction.description }}</span>
|
||||
{% if transaction.installment_plan and transaction.installment_id %}
|
||||
<span
|
||||
class="badge text-bg-secondary">{{ transaction.installment_id }}/{{ transaction.installment_plan.installment_total_number }}</span>
|
||||
class="badge text-bg-secondary mx-1">{{ transaction.installment_id }}/{{ transaction.installment_plan.installment_total_number }}</span>
|
||||
{% endif %}
|
||||
{% if transaction.recurring_transaction %}
|
||||
<span class="text-primary tw:text-xs"><i class="fa-solid fa-arrows-rotate fa-fw"></i></span>
|
||||
<span class="text-primary tw:text-xs mx-1"><i class="fa-solid fa-arrows-rotate fa-fw"></i></span>
|
||||
{% endif %}
|
||||
{% if transaction.dca_expense_entries.all or transaction.dca_income_entries.all %}
|
||||
<span class="badge text-bg-secondary">{% trans 'DCA' %}</span>
|
||||
<span class="badge text-bg-secondary mx-1">{% trans 'DCA' %}</span>
|
||||
{% endif %}
|
||||
{% endspaceless %}
|
||||
</div>
|
||||
@@ -93,7 +91,7 @@
|
||||
{% endwith %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-auto col-12 text-lg-end align-self-end">
|
||||
<div class="col-lg-auto col-12 text-lg-end align-self-end {% if transaction.category.mute or transaction.mute %}tw:brightness-80{% endif %}">
|
||||
<div class="main-amount mb-2 mb-lg-0">
|
||||
<c-amount.display
|
||||
:amount="transaction.amount"
|
||||
@@ -122,7 +120,7 @@
|
||||
<div>
|
||||
{# Item actions#}
|
||||
<div
|
||||
class="transaction-actions tw:absolute! tw:left-1/2 tw:top-0 tw:-translate-x-1/2 tw:-translate-y-1/2 tw:invisible d-flex flex-row card">
|
||||
class="transaction-actions tw:absolute! tw:left-1/2 tw:top-0 tw:-translate-x-1/2 tw:-translate-y-1/2 tw:invisible tw:group-hover/transaction:visible d-flex flex-row card">
|
||||
<div class="card-body p-1 shadow-lg">
|
||||
{% if not transaction.deleted %}
|
||||
<a class="btn btn-secondary btn-sm transaction-action"
|
||||
@@ -132,14 +130,6 @@
|
||||
hx-get="{% url 'transaction_edit' transaction_id=transaction.id %}"
|
||||
hx-target="#generic-offcanvas" hx-swap="innerHTML">
|
||||
<i class="fa-solid fa-pencil fa-fw"></i></a>
|
||||
<a class="btn btn-secondary btn-sm transaction-action"
|
||||
role="button"
|
||||
data-bs-toggle="tooltip"
|
||||
data-bs-title="{% translate "Duplicate" %}"
|
||||
hx-get="{% url 'transaction_clone' transaction_id=transaction.id %}"
|
||||
_="on click if event.ctrlKey set @hx-get to `{% url 'transaction_clone' transaction_id=transaction.id %}?edit=true` then call htmx.process(me) end then trigger ready"
|
||||
hx-trigger="ready">
|
||||
<i class="fa-solid fa-clone fa-fw"></i></a>
|
||||
<a class="btn btn-secondary btn-sm transaction-action"
|
||||
role="button"
|
||||
data-bs-toggle="tooltip"
|
||||
@@ -152,6 +142,29 @@
|
||||
data-confirm-text="{% translate "Yes, delete it!" %}"
|
||||
_="install prompt_swal"><i class="fa-solid fa-trash fa-fw text-danger"></i>
|
||||
</a>
|
||||
<button class="btn btn-secondary btn-sm transaction-action" type="button" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
<i class="fa-solid fa-ellipsis fa-fw"></i>
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
{% if transaction.category.mute %}
|
||||
<li>
|
||||
<a class="dropdown-item disabled d-flex align-items-center" aria-disabled="true">
|
||||
<i class="fa-solid fa-eye fa-fw me-2"></i>
|
||||
<div>
|
||||
{% translate 'Show on summaries' %}
|
||||
<div class="d-block text-body-secondary tw:text-xs tw:font-medium">{% translate 'Controlled by category' %}</div>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
{% elif transaction.mute %}
|
||||
<li><a class="dropdown-item" href="#" hx-get="{% url 'transaction_mute' transaction_id=transaction.id %}" hx-target="closest .transaction" hx-swap="outerHTML"><i class="fa-solid fa-eye fa-fw me-2"></i>{% translate 'Show on summaries' %}</a></li>
|
||||
{% else %}
|
||||
<li><a class="dropdown-item" href="#" hx-get="{% url 'transaction_mute' transaction_id=transaction.id %}" hx-target="closest .transaction" hx-swap="outerHTML"><i class="fa-solid fa-eye-slash fa-fw me-2"></i>{% translate 'Hide from summaries' %}</a></li>
|
||||
{% endif %}
|
||||
<li><a class="dropdown-item" href="#" hx-get="{% url 'quick_transaction_add_as_quick_transaction' transaction_id=transaction.id %}"><i class="fa-solid fa-person-running fa-fw me-2"></i>{% translate 'Add as quick transaction' %}</a></li>
|
||||
<li><hr class="dropdown-divider"></li>
|
||||
<li><a class="dropdown-item" href="#" hx-get="{% url 'transaction_clone' transaction_id=transaction.id %}"><i class="fa-solid fa-clone fa-fw me-2"></i>{% translate 'Duplicate' %}</a></li>
|
||||
</ul>
|
||||
{% else %}
|
||||
<a class="btn btn-secondary btn-sm transaction-action"
|
||||
role="button"
|
||||
|
||||
@@ -10,18 +10,21 @@
|
||||
end
|
||||
else
|
||||
if #actions-bar
|
||||
remove .tw:hidden from #actions-bar
|
||||
set #selected-count's innerHTML to length of <input[type='checkbox']:checked/> in #transactions-list
|
||||
then remove .tw:hidden from #actions-bar
|
||||
then trigger selected_transactions_updated
|
||||
end
|
||||
end
|
||||
end
|
||||
end">
|
||||
<div class="card slide-in-bottom">
|
||||
<div class="card-body p-2 d-flex justify-content-between align-items-center gap-3">
|
||||
<div class="card slide-in-bottom tw:max-w-[90vw]">
|
||||
<div class="card-body p-2 d-flex justify-content-between align-items-center gap-3 tw:overflow-x-auto">
|
||||
{% spaceless %}
|
||||
<div class="tw:font-bold tw:text-md ms-2" id="selected-count">0</div>
|
||||
<div class="vr tw:align-middle"></div>
|
||||
<div class="dropdown">
|
||||
<button class="btn btn-secondary btn-sm dropdown-toggle" type="button" data-bs-toggle="dropdown"
|
||||
aria-expanded="false">
|
||||
aria-expanded="false" data-bs-popper-config='{"strategy":"fixed"}'>
|
||||
<i class="fa-regular fa-square-check fa-fw"></i>
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
@@ -50,7 +53,8 @@
|
||||
<i class="fa-solid fa-pencil"></i>
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm btn-secondary dropdown-toggle dropdown-toggle-split"
|
||||
data-bs-toggle="dropdown" aria-expanded="false" data-bs-auto-close="outside">
|
||||
data-bs-toggle="dropdown" data-bs-popper-config='{"strategy":"fixed"}' aria-expanded="false"
|
||||
data-bs-auto-close="outside">
|
||||
<span class="visually-hidden">{% trans "Toggle Dropdown" %}</span>
|
||||
</button>
|
||||
|
||||
@@ -141,7 +145,8 @@
|
||||
<span class="d-none d-md-inline-block" id="real-total-front">0</span>
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm btn-secondary dropdown-toggle dropdown-toggle-split"
|
||||
data-bs-toggle="dropdown" aria-expanded="false" data-bs-auto-close="outside">
|
||||
data-bs-toggle="dropdown" aria-expanded="false" data-bs-auto-close="outside"
|
||||
data-bs-popper-config='{"strategy":"fixed"}'>
|
||||
<span class="visually-hidden">{% trans "Toggle Dropdown" %}</span>
|
||||
</button>
|
||||
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
{% load cache_access %}
|
||||
{% load settings %}
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||
{% load active_link %}
|
||||
<nav class="navbar border-bottom bg-body-tertiary d-flex d-lg-none" hx-boost="true">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand fw-bold text-primary font-base" href="{% url 'index' %}">
|
||||
<img src="{% static 'img/logo-icon.svg' %}" alt="WYGIWYH Logo" height="40" width="40" title="WYGIWYH"/>
|
||||
</a>
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="offcanvas" data-bs-target="#sidebar"
|
||||
aria-controls="sidebar" aria-label={% translate "Toggle navigation" %}>
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
</div>
|
||||
</nav>
|
||||
@@ -1,3 +1,4 @@
|
||||
{% load cache_access %}
|
||||
{% load settings %}
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||
@@ -162,6 +163,12 @@
|
||||
</li>
|
||||
</ul>
|
||||
<ul class="navbar-nav mb-2 mb-lg-0 gap-3">
|
||||
{% get_update_check as update_check %}
|
||||
{% if update_check.update_available %}
|
||||
<li class="nav-item my-auto">
|
||||
<a class="badge text-bg-secondary text-decoration-none tw:cursor-pointer" href="https://github.com/eitchtee/WYGIWYH/releases/latest" target="_blank"><i class="fa-solid fa-circle-info fa-fw me-2"></i>v.{{ update_check.latest_version }} {% translate 'is available' %}!</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
<li class="nav-item">
|
||||
<div class="nav-link tw:lg:text-2xl! tw:cursor-pointer"
|
||||
data-bs-toggle="tooltip" data-bs-placement="left" data-bs-title="{% trans "Calculator" %}"
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
{% load settings %}
|
||||
{% load i18n %}
|
||||
<div class="dropdown">
|
||||
<div class="nav-link tw:lg:text-2xl! tw:cursor-pointer" type="button" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
<i class="fa-solid fa-user"></i>
|
||||
<span class="d-lg-none d-inline">{% trans "Profile" %}</span>
|
||||
<div class="btn btn-secondary btn-sm" type="button" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
<i class="fa-solid fa-cog"></i>
|
||||
</div>
|
||||
<ul class="dropdown-menu dropdown-menu-start dropdown-menu-lg-end">
|
||||
<li class="dropdown-item-text">{{ user.email }}</li>
|
||||
<li><hr class="dropdown-divider"></li>
|
||||
<li><a class="dropdown-item"
|
||||
hx-get="{% url 'user_settings' %}"
|
||||
hx-target="#generic-offcanvas"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<div id="persistent-generic-offcanvas" class="offcanvas offcanvas-end offcanvas-size-xl"
|
||||
<div id="persistent-generic-offcanvas" class="offcanvas offcanvas-end offcanvas-size-xl tw:z-1100!"
|
||||
data-bs-backdrop="static"
|
||||
tabindex="-1"
|
||||
_="on htmx:afterSettle call bootstrap.Offcanvas.getOrCreateInstance(me).show() end
|
||||
@@ -6,7 +6,7 @@
|
||||
on force_hide_offcanvas call bootstrap.Offcanvas.getOrCreateInstance(me).hide() end
|
||||
on hidden.bs.offcanvas set my innerHTML to '' end">
|
||||
</div>
|
||||
<div id="persistent-generic-offcanvas-left" class="offcanvas offcanvas-start offcanvas-size-xl"
|
||||
<div id="persistent-generic-offcanvas-left" class="offcanvas offcanvas-start offcanvas-size-xl tw:z-1100!"
|
||||
data-bs-backdrop="static"
|
||||
tabindex="-1"
|
||||
_="on htmx:afterSettle call bootstrap.Offcanvas.getOrCreateInstance(me).show() end
|
||||
@@ -16,7 +16,7 @@
|
||||
</div>
|
||||
|
||||
|
||||
<div id="generic-offcanvas" class="offcanvas offcanvas-end offcanvas-size-xl"
|
||||
<div id="generic-offcanvas" class="offcanvas offcanvas-end offcanvas-size-xl tw:z-1100!"
|
||||
data-bs-backdrop="static"
|
||||
tabindex="-1"
|
||||
_="on htmx:afterSettle call bootstrap.Offcanvas.getOrCreateInstance(me).show() end
|
||||
@@ -24,7 +24,7 @@
|
||||
on htmx:beforeOnLoad[detail.boosted] call bootstrap.Offcanvas.getOrCreateInstance(me).hide()
|
||||
on hidden.bs.offcanvas set my innerHTML to '' end">
|
||||
</div>
|
||||
<div id="generic-offcanvas-left" class="offcanvas offcanvas-start offcanvas-size-xl"
|
||||
<div id="generic-offcanvas-left" class="offcanvas offcanvas-start offcanvas-size-xl tw:z-1100!"
|
||||
data-bs-backdrop="static"
|
||||
tabindex="-1"
|
||||
_="on htmx:afterSettle call bootstrap.Offcanvas.getOrCreateInstance(me).show() end
|
||||
|
||||
@@ -8,6 +8,7 @@ behavior htmx_error_handler
|
||||
title: '{% trans "Access Denied" %}',
|
||||
text: '{% trans "You do not have permission to perform this action or access this resource." %}',
|
||||
icon: 'warning',
|
||||
timer: 60000,
|
||||
customClass: {
|
||||
confirmButton: 'btn btn-warning' -- Optional: different button style
|
||||
},
|
||||
@@ -18,6 +19,7 @@ behavior htmx_error_handler
|
||||
title: '{% trans "Something went wrong loading your data" %}',
|
||||
text: '{% trans "Try reloading the page or check the console for more information." %}',
|
||||
icon: 'error',
|
||||
timer: 60000,
|
||||
customClass: {
|
||||
confirmButton: 'btn btn-primary'
|
||||
},
|
||||
|
||||
@@ -0,0 +1,289 @@
|
||||
{% load active_link %}
|
||||
{% load cache_access %}
|
||||
{% load settings %}
|
||||
{% load i18n %}
|
||||
{% load static %}
|
||||
|
||||
<div
|
||||
class="tw:group tw:lg:w-16 tw:lg:hover:w-112 tw:transition-all tw:duration-100 tw:fixed tw:top-0 tw:start-0 tw:h-full tw:z-1020">
|
||||
<nav
|
||||
id="sidebar"
|
||||
hx-boost="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 tw:lg:w-16 tw:lg:group-hover:w-104 tw:transition-all tw:duration-100 tw:overflow-hidden">
|
||||
<div
|
||||
class="tw:hidden tw:lg:group-hover:block tw:absolute tw:top-0 tw:left-104 tw:w-8 tw:h-full tw:bg-transparent tw:pointer-events-auto tw:z-10"></div>
|
||||
|
||||
<a href="{% url 'index' %}" class="d-none d-lg-flex tw:justify-start p-3 text-decoration-none">
|
||||
<img src="{% static 'img/logo-icon.svg' %}" alt="WYGIWYH Logo" height="30" width="30" title="WYGIWYH"/>
|
||||
<span class="fs-4 fw-bold ms-3 tw:lg:invisible tw:lg:group-hover:visible">WYGIWYH</span>
|
||||
</a>
|
||||
|
||||
<div class="offcanvas-header">
|
||||
<a href="{% url 'index' %}" class="offcanvas-title d-flex tw:justify-start text-decoration-none">
|
||||
<img src="{% static 'img/logo-icon.svg' %}" alt="WYGIWYH Logo" height="30" width="30" title="WYGIWYH"/>
|
||||
<span class="fs-4 fw-bold ms-3 tw:lg:invisible tw:lg:group-hover:visible">WYGIWYH</span>
|
||||
</a>
|
||||
<button type="button" class="btn-close" data-bs-target="#sidebar" data-bs-dismiss="offcanvas"
|
||||
aria-label={% translate 'Close' %}></button>
|
||||
</div>
|
||||
<hr class="m-0">
|
||||
|
||||
<ul class="list-unstyled p-3 d-flex flex-column gap-0 tw:group-hover:lg:overflow-y-auto tw:lg:overflow-y-hidden tw:overflow-y-auto tw:overflow-x-hidden tw:text-nowrap tw:lg:group-hover:animate-[disable-pointer-events]"
|
||||
style="animation-duration: 100ms">
|
||||
|
||||
<c-components.sidebar-menu-item
|
||||
title="{% translate 'Monthly' %}"
|
||||
url='monthly_index'
|
||||
active="monthly_overview"
|
||||
icon="fa-solid fa-list">
|
||||
</c-components.sidebar-menu-item>
|
||||
<c-components.sidebar-menu-item
|
||||
title="{% translate 'Yearly' %}"
|
||||
url='yearly_index'
|
||||
active="yearly_overview_currency||yearly_overview_account"
|
||||
icon="fa-solid fa-calendar-plus">
|
||||
</c-components.sidebar-menu-item>
|
||||
<c-components.sidebar-menu-item
|
||||
title="{% translate 'Calendar' %}"
|
||||
url='calendar_index'
|
||||
active="calendar"
|
||||
icon="fa-solid fa-calendar-days">
|
||||
</c-components.sidebar-menu-item>
|
||||
<c-components.sidebar-menu-item
|
||||
title="{% translate 'Insights' %}"
|
||||
url='insights_index'
|
||||
active="insights_index"
|
||||
icon="fa-solid fa-chart-simple">
|
||||
</c-components.sidebar-menu-item>
|
||||
<c-components.sidebar-menu-item
|
||||
title="{% translate 'Net Worth' %}"
|
||||
url='net_worth'
|
||||
active="net_worth"
|
||||
icon="fa-solid fa-money-bill">
|
||||
</c-components.sidebar-menu-item>
|
||||
|
||||
<c-components.sidebar-menu-header title="{% translate 'Transactions' %}"></c-components.sidebar-menu-header>
|
||||
<c-components.sidebar-menu-item
|
||||
title="{% translate 'All' %}"
|
||||
url='transactions_all_index'
|
||||
active="transactions_all_index"
|
||||
icon="fa-solid fa-right-left">
|
||||
</c-components.sidebar-menu-item>
|
||||
{% settings "ENABLE_SOFT_DELETE" as enable_soft_delete %}
|
||||
{% if enable_soft_delete %}
|
||||
<c-components.sidebar-menu-item
|
||||
title="{% translate 'Trash Can' %}"
|
||||
url='transactions_trash_index'
|
||||
active="transactions_trash_index"
|
||||
icon="fa-solid fa-trash">
|
||||
</c-components.sidebar-menu-item>
|
||||
{% endif %}
|
||||
<c-components.sidebar-menu-item
|
||||
title="{% translate 'Quick Transactions' %}"
|
||||
url='quick_transactions_index'
|
||||
active="quick_transactions_index"
|
||||
icon="fa-solid fa-person-running">
|
||||
</c-components.sidebar-menu-item>
|
||||
<c-components.sidebar-menu-item
|
||||
title="{% translate 'Installment Plans' %}"
|
||||
url='installment_plans_index'
|
||||
active="installment_plans_index"
|
||||
icon="fa-solid fa-divide">
|
||||
</c-components.sidebar-menu-item>
|
||||
<c-components.sidebar-menu-item
|
||||
title="{% translate 'Recurring Transactions' %}"
|
||||
url='recurring_trasanctions_index'
|
||||
active="recurring_trasanctions_index"
|
||||
icon="fa-solid fa-repeat">
|
||||
</c-components.sidebar-menu-item>
|
||||
|
||||
<c-components.sidebar-menu-header title="{% translate 'Tools' %}"></c-components.sidebar-menu-header>
|
||||
<c-components.sidebar-menu-item
|
||||
title="{% translate 'Dollar Cost Average Tracker' %}"
|
||||
url='dca_strategy_index'
|
||||
active="dca_strategy_index||dca_strategy_detail_index"
|
||||
icon="fa-solid fa-magnifying-glass-chart">
|
||||
</c-components.sidebar-menu-item>
|
||||
<c-components.sidebar-menu-item
|
||||
title="{% translate 'Unit Price Calculator' %}"
|
||||
url='unit_price_calculator'
|
||||
active="unit_price_calculator"
|
||||
icon="fa-solid fa-tags">
|
||||
</c-components.sidebar-menu-item>
|
||||
<c-components.sidebar-menu-item
|
||||
title="{% translate 'Currency Converter' %}"
|
||||
url='currency_converter'
|
||||
active="currency_converter"
|
||||
icon="fa-solid fa-money-bill-transfer">
|
||||
</c-components.sidebar-menu-item>
|
||||
|
||||
<div>
|
||||
<hr>
|
||||
</div>
|
||||
|
||||
<div type="button"
|
||||
data-bs-toggle="collapse"
|
||||
data-bs-target="#collapsible-panel"
|
||||
aria-expanded="false"
|
||||
aria-controls="collapsible-panel"
|
||||
class="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:invisible tw:lg:group-hover:visible 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>
|
||||
</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:group-hover:lg:overflow-y-auto tw:lg:overflow-y-hidden tw:max-h-screen">
|
||||
<div class="tw:h-screen tw:backdrop-blur-3xl">
|
||||
<!-- Header -->
|
||||
<div
|
||||
class="tw:flex tw:justify-between tw:items-center tw:p-4 tw:border-b tw:border-gray-600 tw:lg:hidden tw:lg:group-hover:flex">
|
||||
<h5 class="tw:text-lg tw:font-semibold tw:text-gray-800 tw:lg:invisible tw:lg:group-hover:visible">
|
||||
{% trans 'Management' %}
|
||||
</h5>
|
||||
|
||||
<button type="button" class="btn-close tw:lg:hidden tw:lg:group-hover:inline" aria-label="Close"
|
||||
data-bs-toggle="collapse"
|
||||
data-bs-target="#collapsible-panel"
|
||||
aria-expanded="true"
|
||||
aria-controls="collapsible-panel">
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<ul class="list-unstyled p-3 d-flex flex-column gap-1 tw:group-hover:lg:overflow-y-auto tw:lg:overflow-y-hidden tw:overflow-y-auto tw:overflow-x-hidden tw:text-nowrap tw:lg:group-hover:animate-[disable-pointer-events]"
|
||||
style="animation-duration: 100ms">
|
||||
<c-components.sidebar-menu-header title="{% translate 'Transactions' %}"></c-components.sidebar-menu-header>
|
||||
<c-components.sidebar-menu-item
|
||||
title="{% translate 'Categories' %}"
|
||||
url='categories_index'
|
||||
active="categories_index"
|
||||
icon="fa-solid fa-icons">
|
||||
</c-components.sidebar-menu-item>
|
||||
<c-components.sidebar-menu-item
|
||||
title="{% translate 'Tags' %}"
|
||||
url='tags_index'
|
||||
active="tags_index"
|
||||
icon="fa-solid fa-hashtag">
|
||||
</c-components.sidebar-menu-item>
|
||||
<c-components.sidebar-menu-item
|
||||
title="{% translate 'Entities' %}"
|
||||
url='entities_index'
|
||||
active="entities_index"
|
||||
icon="fa-solid fa-user-group">
|
||||
</c-components.sidebar-menu-item>
|
||||
|
||||
<c-components.sidebar-menu-header title="{% translate 'Accounts' %}"></c-components.sidebar-menu-header>
|
||||
<c-components.sidebar-menu-item
|
||||
title="{% translate 'Accounts' %}"
|
||||
url='accounts_index'
|
||||
active="accounts_index"
|
||||
icon="fa-solid fa-wallet">
|
||||
</c-components.sidebar-menu-item>
|
||||
<c-components.sidebar-menu-item
|
||||
title="{% translate 'Account Groups' %}"
|
||||
url='account_groups_index'
|
||||
active="account_groups_index"
|
||||
icon="fa-solid fa-wallet">
|
||||
</c-components.sidebar-menu-item>
|
||||
|
||||
<c-components.sidebar-menu-header title="{% translate 'Currencies' %}"></c-components.sidebar-menu-header>
|
||||
<c-components.sidebar-menu-item
|
||||
title="{% translate 'Currencies' %}"
|
||||
url='currencies_index'
|
||||
active="currencies_index"
|
||||
icon="fa-solid fa-coins">
|
||||
</c-components.sidebar-menu-item>
|
||||
<c-components.sidebar-menu-item
|
||||
title="{% translate 'Exchange Rates' %}"
|
||||
url='exchange_rates_index'
|
||||
active="exchange_rates_index"
|
||||
icon="fa-solid fa-right-left">
|
||||
</c-components.sidebar-menu-item>
|
||||
|
||||
<c-components.sidebar-menu-header title="{% translate 'Automation' %}"></c-components.sidebar-menu-header>
|
||||
<c-components.sidebar-menu-item
|
||||
title="{% translate 'Rules' %}"
|
||||
url='rules_index'
|
||||
active="rules_index"
|
||||
icon="fa-solid fa-pen-ruler">
|
||||
</c-components.sidebar-menu-item>
|
||||
<c-components.sidebar-menu-item
|
||||
title="{% translate 'Import' %}"
|
||||
url='import_profiles_index'
|
||||
active="import_profiles_index"
|
||||
icon="fa-solid fa-file-import">
|
||||
</c-components.sidebar-menu-item>
|
||||
{% if user.is_superuser %}
|
||||
<c-components.sidebar-menu-item
|
||||
title="{% translate 'Export and Restore' %}"
|
||||
url='export_index'
|
||||
active="export_index"
|
||||
icon="fa-solid fa-file-export">
|
||||
</c-components.sidebar-menu-item>
|
||||
{% endif %}
|
||||
<c-components.sidebar-menu-item
|
||||
title="{% translate 'Automatic Exchange Rates' %}"
|
||||
url='automatic_exchange_rates_index'
|
||||
active="automatic_exchange_rates_index"
|
||||
icon="fa-solid fa-right-left">
|
||||
</c-components.sidebar-menu-item>
|
||||
|
||||
{% if user.is_superuser %}
|
||||
<c-components.sidebar-menu-header title="{% translate 'Admin' %}"></c-components.sidebar-menu-header>
|
||||
<c-components.sidebar-menu-item
|
||||
title="{% translate 'Users' %}"
|
||||
url='users_index'
|
||||
active="users_index"
|
||||
icon="fa-solid fa-users">
|
||||
</c-components.sidebar-menu-item>
|
||||
<c-components.sidebar-menu-url-item
|
||||
title="{% translate 'Django Admin' %}"
|
||||
tooltip="{% translate "Only use this if you know what you're doing" %}"
|
||||
url='/admin/'
|
||||
icon="fa-solid fa-screwdriver-wrench">
|
||||
</c-components.sidebar-menu-url-item>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
{% get_update_check as update_check %}
|
||||
{% if update_check.update_available %}
|
||||
<div class="my-3">
|
||||
<a class="px-3 badge text-bg-primary text-decoration-none tw:cursor-pointer w-100 tw:text-xs!"
|
||||
href="https://github.com/eitchtee/WYGIWYH/releases/latest" target="_blank"><i
|
||||
class="fa-solid fa-circle-exclamation fa-fw me-2"></i><span
|
||||
class="tw:lg:invisible tw:lg:group-hover:visible">v.{{ update_check.latest_version }} {% translate 'is available' %}!</span></a>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="btn-group w-100" role="group">
|
||||
<button type="button" class="btn btn-secondary btn-sm" data-bs-toggle="tooltip"
|
||||
data-bs-title="{% trans "Calculator" %}"
|
||||
_="on click trigger show on #calculator">
|
||||
<i class="fa-solid fa-calculator fa-fw"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<hr class="my-1">
|
||||
<div
|
||||
class="ps-4 pe-2 py-2 d-flex align-items-center text-body-secondary text-decoration-none justify-content-between tw:text-wrap tw:lg:text-nowrap">
|
||||
<div>
|
||||
<i class="fa-solid fa-circle-user"></i>
|
||||
<strong class="ms-2 tw:lg:invisible tw:lg:group-hover:visible">{{ user.email }}</strong>
|
||||
</div>
|
||||
<div class="tw:lg:invisible tw:lg:group-hover:visible">
|
||||
{% include 'includes/navbar/user_menu.html' %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
<div
|
||||
class="tw:hidden tw:lg:group-hover:block tw:absolute tw:top-0 tw:left-104 tw:w-16 tw:h-full tw:bg-transparent"></div>
|
||||
</div>
|
||||
@@ -273,7 +273,6 @@
|
||||
function setupChart() {
|
||||
var rawData = JSON.parse(document.getElementById('categoryOverviewData').textContent);
|
||||
var showing_string = JSON.parse(document.getElementById('showingString').textContent);
|
||||
console.log(showing_string)
|
||||
|
||||
// --- Dynamic Data Processing ---
|
||||
var categories = [];
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
<div class="mb-2 w-100 d-lg-inline-flex d-grid gap-2 flex-wrap justify-content-lg-center" role="group"
|
||||
_="on change
|
||||
set type to event.target.value
|
||||
add .tw:hidden to <#picker-form > div:not(.tw:hidden)/>
|
||||
add .tw:hidden to <#picker-form > div:not(.tw\\:hidden)/>
|
||||
|
||||
if type == 'month'
|
||||
remove .tw:hidden from #month-form
|
||||
|
||||
@@ -14,6 +14,8 @@
|
||||
<tr>
|
||||
<th scope="col" class="col-auto"></th>
|
||||
<th scope="col" class="col">{% translate 'Name' %}</th>
|
||||
<th scope="col" class="col">{% translate 'Account' %}</th>
|
||||
<th scope="col" class="col">{% translate 'Amount' %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -69,6 +71,17 @@
|
||||
</div>
|
||||
<div class="tw:text-sm tw:text-gray-400">{{ installment_plan.notes|linebreaksbr }}</div>
|
||||
</td>
|
||||
<td class="col-auto">
|
||||
{% if installment_plan.account.group %}{{ installment_plan.account.group }} • {% endif %}{{ installment_plan.account }}
|
||||
</td>
|
||||
<td class="col-auto">
|
||||
<c-amount.display
|
||||
:amount="installment_plan.installment_amount"
|
||||
:prefix="installment_plan.account.currency.prefix"
|
||||
:suffix="installment_plan.account.currency.suffix"
|
||||
:decimal_places="installment_plan.account.currency.decimal_places"
|
||||
color="{% if installment_plan.type == "EX" %}red{% else %}green{% endif %}"></c-amount.display>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
|
||||
@@ -28,27 +28,31 @@
|
||||
</head>
|
||||
<body class="font-monospace">
|
||||
<div _="install hide_amounts
|
||||
install htmx_error_handler
|
||||
{% block body_hyperscript %}{% endblock %}"
|
||||
install htmx_error_handler
|
||||
{% block body_hyperscript %}{% endblock %}"
|
||||
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'>
|
||||
{% include 'includes/navbar.html' %}
|
||||
|
||||
{% settings "DEMO" as demo_mode %}
|
||||
{% if demo_mode %}
|
||||
<div class="px-3 m-0" id="demo-mode-alert" hx-preserve>
|
||||
<div class="alert alert-warning alert-dismissible fade show my-3" role="alert">
|
||||
<strong>{% trans 'This is a demo!' %}</strong> {% trans 'Any data you add here will be wiped in 24hrs or less' %}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
{% include 'includes/mobile_navbar.html' %}
|
||||
{% include 'includes/sidebar.html' %}
|
||||
|
||||
<main class="tw:p-4 tw:lg:ml-16">
|
||||
{% settings "DEMO" as demo_mode %}
|
||||
{% if demo_mode %}
|
||||
<div class="px-3 m-0" id="demo-mode-alert" hx-preserve>
|
||||
<div class="alert alert-warning alert-dismissible fade show my-3" role="alert">
|
||||
<strong>{% trans 'This is a demo!' %}</strong> {% trans 'Any data you add here will be wiped in 24hrs or less' %}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
<div id="content">
|
||||
{% block content %}{% endblock %}
|
||||
</div>
|
||||
<div id="content">
|
||||
{% block content %}{% endblock %}
|
||||
</div>
|
||||
|
||||
{% include 'includes/offcanvas.html' %}
|
||||
{% include 'includes/toasts.html' %}
|
||||
{% include 'includes/offcanvas.html' %}
|
||||
{% include 'includes/toasts.html' %}
|
||||
</main>
|
||||
</div>
|
||||
|
||||
{% include 'includes/tools/calculator.html' %}
|
||||
|
||||
@@ -104,7 +104,7 @@
|
||||
<div id="summary"
|
||||
hx-get="{% url 'monthly_summary' month=month year=year %}"
|
||||
class="show-loading"
|
||||
hx-trigger="load, updated from:window, selective_update from:window">
|
||||
hx-trigger="load, updated from:window, selective_update from:window, every 10m">
|
||||
</div>
|
||||
</div>
|
||||
<div class="tab-pane fade {% if summary_tab == 'currency' %}show active{% endif %}"
|
||||
@@ -115,7 +115,7 @@
|
||||
<div id="currency-summary"
|
||||
hx-get="{% url 'monthly_currency_summary' month=month year=year %}"
|
||||
class="show-loading"
|
||||
hx-trigger="load, updated from:window, selective_update from:window">
|
||||
hx-trigger="load, updated from:window, selective_update from:window, every 10m">
|
||||
</div>
|
||||
</div>
|
||||
<div class="tab-pane fade {% if summary_tab == 'account' %}show active{% endif %}"
|
||||
@@ -126,7 +126,7 @@
|
||||
<div id="account-summary"
|
||||
hx-get="{% url 'monthly_account_summary' month=month year=year %}"
|
||||
class="show-loading"
|
||||
hx-trigger="load, updated from:window, selective_update from:window">
|
||||
hx-trigger="load, updated from:window, selective_update from:window, every 10m">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -191,7 +191,7 @@
|
||||
<div id="transactions"
|
||||
class="show-loading"
|
||||
hx-get="{% url 'monthly_transactions_list' month=month year=year %}"
|
||||
hx-trigger="load, updated from:window" hx-include="#filter, #order">
|
||||
hx-trigger="load, updated from:window, every 10m" hx-include="#filter, #order">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -9,6 +9,29 @@
|
||||
{% 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>
|
||||
|
||||
<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 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">
|
||||
@@ -318,5 +341,6 @@
|
||||
call currencyChart.update()
|
||||
end
|
||||
</script>
|
||||
|
||||
</div>
|
||||
<c-ui.transactions_fab></c-ui.transactions_fab>
|
||||
{% endblock %}
|
||||
|
||||
@@ -11,6 +11,8 @@
|
||||
<tr>
|
||||
<th scope="col" class="col-auto"></th>
|
||||
<th scope="col" class="col">{% translate 'Name' %}</th>
|
||||
<th scope="col" class="col">{% translate 'Account' %}</th>
|
||||
<th scope="col" class="col">{% translate 'Amount' %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -46,6 +48,17 @@
|
||||
{{ qt.name }}
|
||||
</div>
|
||||
</td>
|
||||
<td class="col-auto">
|
||||
{% if qt.account.group %}{{ qt.account.group }} • {% endif %}{{ qt.account }}
|
||||
</td>
|
||||
<td class="col-auto">
|
||||
<c-amount.display
|
||||
:amount="qt.amount"
|
||||
:prefix="qt.account.currency.prefix"
|
||||
:suffix="qt.account.currency.suffix"
|
||||
:decimal_places="qt.account.currency.decimal_places"
|
||||
color="{% if qt.type == "EX" %}red{% else %}green{% endif %}"></c-amount.display>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
|
||||
@@ -16,6 +16,8 @@
|
||||
<tr>
|
||||
<th scope="col" class="col-auto"></th>
|
||||
<th scope="col" class="col">{% translate 'Name' %}</th>
|
||||
<th scope="col" class="col">{% translate 'Account' %}</th>
|
||||
<th scope="col" class="col">{% translate 'Amount' %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -105,6 +107,17 @@
|
||||
</div>
|
||||
<div class="tw:text-sm tw:text-gray-400">{{ recurring_transaction.notes|linebreaksbr }}</div>
|
||||
</td>
|
||||
<td class="col-auto">
|
||||
{% if recurring_transaction.account.group %}{{ recurring_transaction.account.group }} • {% endif %}{{ recurring_transaction.account }}
|
||||
</td>
|
||||
<td class="col-auto">
|
||||
<c-amount.display
|
||||
:amount="recurring_transaction.amount"
|
||||
:prefix="recurring_transaction.account.currency.prefix"
|
||||
:suffix="recurring_transaction.account.currency.suffix"
|
||||
:decimal_places="recurring_transaction.account.currency.decimal_places"
|
||||
color="{% if recurring_transaction.type == "EX" %}red{% else %}green{% endif %}"></c-amount.display>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
|
||||
@@ -5,47 +5,56 @@
|
||||
{% block title %}{% translate 'Transactions' %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container-fluid px-md-3 py-3 column-gap-5">
|
||||
<div class="row gx-xl-4 gy-3">
|
||||
<div class="col-12 col-xl-3 order-0 order-xl-0">
|
||||
{# Filter transactions#}
|
||||
<div class="row mb-1">
|
||||
<div class="col-12">
|
||||
<div class="d-flex mb-3 align-self-center">
|
||||
<div class="me-auto"><h4><i class="fa-solid fa-filter me-2"></i>{% translate 'Filter' %}</h4></div>
|
||||
<div class="align-self-center">
|
||||
<button class="btn btn-outline-danger btn-sm" _="on click call #filter.reset() then trigger change on #filter">{% translate 'Clear' %}</button>
|
||||
</div>
|
||||
<div class="container px-md-3 py-3 column-gap-5">
|
||||
<div class="row gx-xl-4 gy-3">
|
||||
<div class="col-12 col-xl-8 order-2 order-xl-1">
|
||||
<div class="row mb-1">
|
||||
<div class="col-sm-6 col-12">
|
||||
{# Filter transactions button #}
|
||||
<button type="button" class="btn btn-sm btn-outline-primary dropdown-toggle" type="button"
|
||||
data-bs-toggle="collapse" data-bs-target="#collapse-filter" aria-expanded="false"
|
||||
aria-controls="collapse-filter">
|
||||
<i class="fa-solid fa-filter fa-fw me-2"></i>{% translate 'Filter transactions' %}
|
||||
</button>
|
||||
</div>
|
||||
{# Ordering button#}
|
||||
<div class="col-sm-6 col-12 tw:content-center my-3 my-sm-0">
|
||||
<div class="text-sm-end" _="on change trigger updated on window">
|
||||
<label for="order">{% translate "Order by" %}</label>
|
||||
<select
|
||||
class="tw:border-0 tw:focus-visible:outline-0 w-full pe-2 tw:leading-normal text-bg-tertiary tw:font-medium rounded bg-body text-body"
|
||||
name="order" id="order">
|
||||
<option value="default"
|
||||
{% if order == 'default' %}selected{% endif %}>{% translate 'Default' %}</option>
|
||||
<option value="older"
|
||||
{% if order == 'older' %}selected{% endif %}>{% translate 'Oldest first' %}</option>
|
||||
<option value="newer"
|
||||
{% if order == 'newer' %}selected{% endif %}>{% translate 'Newest first' %}</option>
|
||||
</select>
|
||||
</div>
|
||||
<hr>
|
||||
<form hx-get="{% url 'transactions_all_list' %}" hx-trigger="change, submit, search"
|
||||
hx-target="#transactions" id="filter" hx-indicator="#transactions"
|
||||
_="install init_tom_select
|
||||
install init_datepicker">
|
||||
{% crispy filter.form %}
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-xl-6 order-2 order-xl-1">
|
||||
<div class="text-end tw:justify-end tw:flex tw:text-sm mb-3">
|
||||
<div class="tw:content-center" _="on change trigger updated on window">
|
||||
<label for="order">{% translate "Order by" %}</label>
|
||||
<select class="tw:border-0 tw:focus-visible:outline-0 w-full pe-2 tw:leading-normal text-bg-tertiary tw:font-medium rounded bg-body text-body" name="order" id="order">
|
||||
<option value="default" {% if order == 'default' %}selected{% endif %}>{% translate 'Default' %}</option>
|
||||
<option value="older" {% if order == 'older' %}selected{% endif %}>{% translate 'Oldest first' %}</option>
|
||||
<option value="newer" {% if order == 'newer' %}selected{% endif %}>{% translate 'Newest first' %}</option>
|
||||
</select>
|
||||
{# Filter transactions form#}
|
||||
<div class="collapse" id="collapse-filter">
|
||||
<div class="card card-body">
|
||||
<form _="on change or submit or search trigger updated on window end
|
||||
install init_tom_select
|
||||
install init_datepicker"
|
||||
id="filter">
|
||||
{% crispy filter.form %}
|
||||
</form>
|
||||
<button class="btn btn-outline-danger btn-sm"
|
||||
_="on click call #filter.reset() then trigger change on #filter">{% translate 'Clear' %}</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="transactions"
|
||||
class="show-loading"
|
||||
hx-get="{% url 'transactions_all_list' %}"
|
||||
hx-trigger="load, updated from:window" hx-include="#filter, #page, #order">
|
||||
</div>
|
||||
</div>
|
||||
<div id="transactions"
|
||||
class="show-loading"
|
||||
hx-get="{% url 'transactions_all_list' %}"
|
||||
hx-trigger="load, updated from:window" hx-include="#filter, #page, #order">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-xl-3 order-1 order-xl-2">
|
||||
<ul class="nav nav-tabs" id="all-transactions-summary" role="tablist">
|
||||
<div class="col-12 col-xl-4 order-1 order-xl-2">
|
||||
<ul class="nav nav-tabs" id="all-transactions-summary" role="tablist">
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link {% if summary_tab == 'currency' %}active{% endif %}"
|
||||
id="currency-tab"
|
||||
@@ -73,7 +82,7 @@
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="tab-content" id="all-transactions-content">
|
||||
<div class="tab-content" id="all-transactions-content">
|
||||
<div class="tab-pane fade {% if summary_tab == 'currency' %}show active{% endif %}"
|
||||
id="currency-tab-pane"
|
||||
role="tabpanel"
|
||||
@@ -98,8 +107,9 @@
|
||||
hx-include="#filter">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<c-ui.transactions_fab></c-ui.transactions_fab>
|
||||
{% endblock %}
|
||||
|
||||
@@ -12,7 +12,18 @@
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container px-md-3 py-3 column-gap-5">
|
||||
<div class="h-100 text-center mb-4 pt-2">
|
||||
<div class="btn-group gap-3" role="group">
|
||||
<a href="{% url 'yearly_overview_currency' year=year %}" class="btn {% if type != 'currency' %}btn-outline-primary{% else %}btn-primary{% endif %} rounded-5" hx-boost>
|
||||
<i class="fa-solid fa-solid fa-coins fa-fw me-2"></i>{% trans 'Currency' %}
|
||||
</a>
|
||||
|
||||
<a href="{% url 'yearly_overview_account' year=year %}" class="btn {% if type != 'account' %}btn-outline-primary{% else %}btn-primary{% endif %} rounded-5" hx-boost>
|
||||
<i class="fa-solid fa-wallet fa-fw me-2"></i>{% trans 'Account' %}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container px-md-3 py-3 column-gap-5" id="yearly-content">
|
||||
<div class="row mb-3 gx-xl-4 gy-3 mb-4">
|
||||
{# Date picker#}
|
||||
<div class="col-12 col-xl-2 flex-row align-items-center d-flex">
|
||||
@@ -113,7 +124,7 @@
|
||||
<div id="data-content"
|
||||
class="show-loading"
|
||||
hx-get="{% url 'yearly_overview_account_data' year=year %}"
|
||||
hx-trigger="load"
|
||||
hx-trigger="load, every 10m, updated from:window"
|
||||
hx-include="[name='account'], [name='month']"
|
||||
hx-swap="innerHTML">
|
||||
</div>
|
||||
|
||||
@@ -14,7 +14,18 @@
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container px-md-3 py-3 column-gap-5">
|
||||
<div class="h-100 text-center mb-4 pt-2">
|
||||
<div class="btn-group gap-3" role="group">
|
||||
<a href="{% url 'yearly_overview_currency' year=year %}" class="btn {% if type != 'currency' %}btn-outline-primary{% else %}btn-primary{% endif %} rounded-5" hx-boost>
|
||||
<i class="fa-solid fa-solid fa-coins fa-fw me-2"></i>{% trans 'Currency' %}
|
||||
</a>
|
||||
|
||||
<a href="{% url 'yearly_overview_account' year=year %}" class="btn {% if type != 'account' %}btn-outline-primary{% else %}btn-primary{% endif %} rounded-5" hx-boost>
|
||||
<i class="fa-solid fa-wallet fa-fw me-2"></i>{% trans 'Account' %}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container px-md-3 py-3 column-gap-5" id="yearly-content">
|
||||
<div class="row mb-3 gx-xl-4 gy-3 mb-4">
|
||||
{# Date picker#}
|
||||
<div class="col-12 col-xl-2 flex-row align-items-center d-flex">
|
||||
@@ -115,7 +126,7 @@
|
||||
<div id="data-content"
|
||||
class="show-loading"
|
||||
hx-get="{% url 'yearly_overview_currency_data' year=year %}"
|
||||
hx-trigger="load"
|
||||
hx-trigger="load, every 10m, updated from:window"
|
||||
hx-include="[name='currency'], [name='month']"
|
||||
hx-swap="innerHTML">
|
||||
</div>
|
||||
|
||||
@@ -209,11 +209,11 @@
|
||||
|
||||
|
||||
.slide-in-bottom {
|
||||
animation: slide-in-bottom 0.3s cubic-bezier(0.250, 0.460, 0.450, 0.940) both;
|
||||
animation: slide-in-bottom 0.3s cubic-bezier(0.250, 0.460, 0.450, 0.940) both;
|
||||
}
|
||||
|
||||
.slide-in-bottom-reverse {
|
||||
animation: slide-in-bottom 0.3s cubic-bezier(0.250, 0.460, 0.450, 0.940) reverse both;
|
||||
animation: slide-in-bottom 0.3s cubic-bezier(0.250, 0.460, 0.450, 0.940) reverse both;
|
||||
}
|
||||
|
||||
/* ----------------------------------------------
|
||||
@@ -238,3 +238,12 @@
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes disable-pointer-events {
|
||||
0%, 99% {
|
||||
pointer-events: none;
|
||||
}
|
||||
100% {
|
||||
pointer-events: auto;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,3 +77,11 @@ select[multiple] {
|
||||
margin-top: 20px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
[data-bs-toggle="collapse"] .fa-chevron-down {
|
||||
transition: transform 0.25s ease-in-out;
|
||||
}
|
||||
|
||||
[data-bs-toggle="collapse"][aria-expanded="true"] .fa-chevron-down {
|
||||
transform: rotate(-180deg);
|
||||
}
|
||||
|
||||
@@ -1,2 +1,11 @@
|
||||
@import "tailwindcss" prefix(tw) source("../../../app/templates/");
|
||||
@custom-variant dark (&:where([data-bs-theme=dark], [data-bs-theme=dark] *));
|
||||
@custom-variant hover (&:hover);
|
||||
|
||||
.sidebar-active {
|
||||
@apply tw:bg-gray-700 tw:text-white;
|
||||
}
|
||||
|
||||
.sidebar-item:not(.sidebar-active) {
|
||||
@apply tw:text-gray-300 tw:hover:text-white;
|
||||
}
|
||||
|
||||
+11
-11
@@ -1,27 +1,27 @@
|
||||
Django~=5.1
|
||||
psycopg[binary]==3.2.6
|
||||
Django~=5.2
|
||||
psycopg[binary]==3.2.9
|
||||
python-webpack-boilerplate==1.0.4
|
||||
django-crispy-forms==2.3
|
||||
crispy-bootstrap5==2025.4
|
||||
django-crispy-forms==2.4
|
||||
crispy-bootstrap5==2025.6
|
||||
django-browser-reload==1.18.0
|
||||
django-hijack==3.7.1
|
||||
django-hijack==3.7.3
|
||||
django-filter==25.1
|
||||
django-debug-toolbar==4.4.6
|
||||
django-cachalot~=2.7.0
|
||||
django-cotton~=1.5.2
|
||||
django-cachalot~=2.8.0
|
||||
django-cotton~=2.1.3
|
||||
django-pwa~=2.0.1
|
||||
djangorestframework~=3.16.0
|
||||
drf-spectacular~=0.28.0
|
||||
django-import-export~=4.3.7
|
||||
django-import-export~=4.3.9
|
||||
|
||||
gunicorn==23.0.0
|
||||
whitenoise[brotli]==6.9.0
|
||||
|
||||
watchfiles==0.24.0 # https://github.com/samuelcolvin/watchfiles
|
||||
procrastinate[django]~=2.15.1
|
||||
watchfiles==1.1.0 # https://github.com/samuelcolvin/watchfiles
|
||||
procrastinate[django]~=3.4.0
|
||||
|
||||
requests~=2.32.3
|
||||
django-allauth[socialaccount]~=65.9.0
|
||||
django-allauth[socialaccount]~=65.10.0
|
||||
|
||||
pytz
|
||||
python-dateutil~=2.9.0.post0
|
||||
|
||||
Reference in New Issue
Block a user