mirror of
https://github.com/eitchtee/WYGIWYH.git
synced 2026-05-27 18:09:25 +02:00
Compare commits
65 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2f99021d0b | |||
| 2f33b5989f | |||
| 31cf62e277 | |||
| a544dc4943 | |||
| b1178198e9 | |||
| 02a488bfff | |||
| b05285947b | |||
| d7b7dd28c7 | |||
| 9353d498ef | |||
| 4f6903e8e4 | |||
| 7d3d6ea2fc | |||
| cce9c7a7a5 | |||
| f80f74a01a | |||
| df47ffc49c | |||
| 4f35647a22 | |||
| 368342853f | |||
| 5a675f674d | |||
| 9ef8fdec49 | |||
| f29a8d8bc0 | |||
| 8c43365ec0 | |||
| 2cdcc4ee26 | |||
| 84852012f9 | |||
| edf0e2c66f | |||
| f90a31f2b9 | |||
| dd1f6a6ef2 | |||
| 57f98ba171 | |||
| f2e93f7df9 | |||
| 26cfa493b3 | |||
| c6e003ed86 | |||
| 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 |
@@ -140,9 +140,10 @@ To create the first user, open the container's console using Unraid's UI, by cli
|
||||
| ENABLE_SOFT_DELETE | true\|false | false | Whether to enable transactions soft delete, if enabled, deleted transactions will remain in the database. Useful for imports and avoiding duplicate entries. |
|
||||
| KEEP_DELETED_TRANSACTIONS_FOR | int | 365 | Time in days to keep soft deleted transactions for. If 0, will keep all transactions indefinitely. Only works if ENABLE_SOFT_DELETE is true. |
|
||||
| TASK_WORKERS | int | 1 | How many workers to have for async tasks. One should be enough for most use cases |
|
||||
| DEMO | true\|false | false | If demo mode is enabled. |
|
||||
| ADMIN_EMAIL | string | None | Automatically creates an admin account with this email. Must have `ADMIN_PASSWORD` also set. |
|
||||
| ADMIN_PASSWORD | string | None | Automatically creates an admin account with this password. Must have `ADMIN_EMAIL` also set. |
|
||||
| DEMO | true\|false | false | If demo mode is enabled. |
|
||||
| ADMIN_EMAIL | string | None | Automatically creates an admin account with this email. Must have `ADMIN_PASSWORD` also set. |
|
||||
| ADMIN_PASSWORD | string | None | Automatically creates an admin account with this password. Must have `ADMIN_EMAIL` also set. |
|
||||
| CHECK_FOR_UPDATES | bool | true | Check and notify users about new versions. The check is done by doing a single query to Github's API every 12 hours. |
|
||||
|
||||
## OIDC Configuration
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -535,6 +537,7 @@ PWA_APP_SCREENSHOTS = [
|
||||
PWA_SERVICE_WORKER_PATH = BASE_DIR / "templates" / "pwa" / "serviceworker.js"
|
||||
|
||||
ENABLE_SOFT_DELETE = os.getenv("ENABLE_SOFT_DELETE", "false").lower() == "true"
|
||||
CHECK_FOR_UPDATES = os.getenv("CHECK_FOR_UPDATES", "true").lower() == "true"
|
||||
KEEP_DELETED_TRANSACTIONS_FOR = int(os.getenv("KEEP_DELETED_ENTRIES_FOR", "365"))
|
||||
APP_VERSION = os.getenv("APP_VERSION", "unknown")
|
||||
DEMO = os.getenv("DEMO", "false").lower() == "true"
|
||||
|
||||
+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,49 @@ 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):
|
||||
if not settings.CHECK_FOR_UPDATES:
|
||||
return "CHECK_FOR_UPDATES is disabled"
|
||||
|
||||
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",
|
||||
}
|
||||
@@ -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'),
|
||||
),
|
||||
]
|
||||
@@ -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
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
from crispy_bootstrap5.bootstrap5 import Switch, BS5Accordion
|
||||
from crispy_forms.bootstrap import FormActions, AccordionGroup
|
||||
from crispy_forms.bootstrap import FormActions, AccordionGroup, AppendedText
|
||||
from crispy_forms.helper import FormHelper
|
||||
from crispy_forms.layout import (
|
||||
Layout,
|
||||
@@ -963,6 +963,7 @@ class RecurringTransactionForm(forms.ModelForm):
|
||||
"notes",
|
||||
"add_notes_to_transaction",
|
||||
"entities",
|
||||
"keep_at_most",
|
||||
]
|
||||
widgets = {
|
||||
"reference_date": AirMonthYearPickerInput(),
|
||||
@@ -1042,6 +1043,7 @@ class RecurringTransactionForm(forms.ModelForm):
|
||||
Column("end_date", css_class="form-group col-md-4 mb-0"),
|
||||
css_class="form-row",
|
||||
),
|
||||
AppendedText("keep_at_most", _("future transactions")),
|
||||
)
|
||||
|
||||
self.fields["amount"].widget = ArbitraryDecimalDisplayNumberInput()
|
||||
|
||||
@@ -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'),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,19 @@
|
||||
# Generated by Django 5.2.4 on 2025-08-06 14:51
|
||||
|
||||
import django.core.validators
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('transactions', '0047_alter_transactioncategory_owner_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='recurringtransaction',
|
||||
name='keep_at_most',
|
||||
field=models.PositiveIntegerField(default=6, validators=[django.core.validators.MinValueValidator(1)], verbose_name='Keep at most'),
|
||||
),
|
||||
]
|
||||
@@ -722,6 +722,9 @@ class RecurringTransaction(models.Model):
|
||||
recurrence_interval = models.PositiveIntegerField(
|
||||
verbose_name=_("Recurrence Interval"),
|
||||
)
|
||||
keep_at_most = models.PositiveIntegerField(
|
||||
verbose_name=_("Keep at most"), default=6, validators=[MinValueValidator(1)]
|
||||
)
|
||||
|
||||
last_generated_date = models.DateField(
|
||||
verbose_name=_("Last Generated Date"), null=True, blank=True
|
||||
@@ -759,8 +762,10 @@ class RecurringTransaction(models.Model):
|
||||
current_date = self.start_date
|
||||
reference_date = self.reference_date
|
||||
end_date = min(
|
||||
self.end_date or timezone.now().date() + (self.get_recurrence_delta() * 5),
|
||||
timezone.now().date() + (self.get_recurrence_delta() * 5),
|
||||
self.end_date
|
||||
or timezone.now().date()
|
||||
+ (self.get_recurrence_delta() * self.keep_at_most),
|
||||
timezone.now().date() + (self.get_recurrence_delta() * self.keep_at_most),
|
||||
)
|
||||
|
||||
while current_date <= end_date:
|
||||
@@ -837,8 +842,16 @@ class RecurringTransaction(models.Model):
|
||||
current_date = start_date
|
||||
end_date = min(
|
||||
recurring_transaction.end_date
|
||||
or today + (recurring_transaction.get_recurrence_delta() * 6),
|
||||
today + (recurring_transaction.get_recurrence_delta() * 6),
|
||||
or today
|
||||
+ (
|
||||
recurring_transaction.get_recurrence_delta()
|
||||
* recurring_transaction.keep_at_most
|
||||
),
|
||||
today
|
||||
+ (
|
||||
recurring_transaction.get_recurrence_delta()
|
||||
* recurring_transaction.keep_at_most
|
||||
),
|
||||
)
|
||||
|
||||
logger.info(f"End date: {end_date}")
|
||||
@@ -976,3 +989,6 @@ class QuickTransaction(OwnedObject):
|
||||
|
||||
self.full_clean()
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
@@ -28,23 +28,18 @@ def generate_recurring_transactions(timestamp=None):
|
||||
@app.periodic(cron="10 1 * * *")
|
||||
@app.task(name="cleanup_deleted_transactions")
|
||||
def cleanup_deleted_transactions(timestamp=None):
|
||||
with cachalot_disabled():
|
||||
if settings.ENABLE_SOFT_DELETE and settings.KEEP_DELETED_TRANSACTIONS_FOR == 0:
|
||||
return "KEEP_DELETED_TRANSACTIONS_FOR is 0, no cleanup performed."
|
||||
if settings.ENABLE_SOFT_DELETE and settings.KEEP_DELETED_TRANSACTIONS_FOR == 0:
|
||||
return "KEEP_DELETED_TRANSACTIONS_FOR is 0, no cleanup performed."
|
||||
|
||||
if not settings.ENABLE_SOFT_DELETE:
|
||||
# Hard delete all soft-deleted transactions
|
||||
deleted_count, _ = Transaction.userless_deleted_objects.all().hard_delete()
|
||||
return (
|
||||
f"Hard deleted {deleted_count} transactions (soft deletion disabled)."
|
||||
)
|
||||
if not settings.ENABLE_SOFT_DELETE:
|
||||
# Hard delete all soft-deleted transactions
|
||||
deleted_count, _ = Transaction.userless_deleted_objects.all().hard_delete()
|
||||
return f"Hard deleted {deleted_count} transactions (soft deletion disabled)."
|
||||
|
||||
# Calculate the cutoff date
|
||||
cutoff_date = timezone.now() - timedelta(
|
||||
days=settings.KEEP_DELETED_TRANSACTIONS_FOR
|
||||
)
|
||||
|
||||
invalidate()
|
||||
# Calculate the cutoff date
|
||||
cutoff_date = timezone.now() - timedelta(
|
||||
days=settings.KEEP_DELETED_TRANSACTIONS_FOR
|
||||
)
|
||||
|
||||
# Hard delete soft-deleted transactions older than the cutoff date
|
||||
old_transactions = Transaction.userless_deleted_objects.filter(
|
||||
|
||||
@@ -71,6 +71,16 @@ urlpatterns = [
|
||||
views.transaction_mute,
|
||||
name="transaction_mute",
|
||||
),
|
||||
path(
|
||||
"transaction/<int:transaction_id>/change-month/<str:change_type>/",
|
||||
views.transaction_change_month,
|
||||
name="transaction_change_month",
|
||||
),
|
||||
path(
|
||||
"transaction/<int:transaction_id>/move-to-today/",
|
||||
views.transaction_move_to_today,
|
||||
name="transaction_move_to_today",
|
||||
),
|
||||
path(
|
||||
"transaction/<int:transaction_id>/delete/",
|
||||
views.transaction_delete,
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import datetime
|
||||
from copy import deepcopy
|
||||
|
||||
from dateutil.relativedelta import relativedelta
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.core.paginator import Paginator
|
||||
@@ -408,6 +409,47 @@ def transaction_mute(request, transaction_id):
|
||||
return response
|
||||
|
||||
|
||||
@only_htmx
|
||||
@login_required
|
||||
@require_http_methods(["GET"])
|
||||
def transaction_change_month(request, transaction_id, change_type):
|
||||
transaction: Transaction = get_object_or_404(Transaction, pk=transaction_id)
|
||||
|
||||
if change_type == "next":
|
||||
transaction.reference_date = transaction.reference_date + relativedelta(
|
||||
months=1
|
||||
)
|
||||
transaction.save()
|
||||
transaction_updated.send(sender=transaction)
|
||||
elif change_type == "previous":
|
||||
transaction.reference_date = transaction.reference_date - relativedelta(
|
||||
months=1
|
||||
)
|
||||
transaction.save()
|
||||
transaction_updated.send(sender=transaction)
|
||||
|
||||
return HttpResponse(
|
||||
status=204,
|
||||
headers={"HX-Trigger": "updated"},
|
||||
)
|
||||
|
||||
|
||||
@only_htmx
|
||||
@login_required
|
||||
@require_http_methods(["GET"])
|
||||
def transaction_move_to_today(request, transaction_id):
|
||||
transaction: Transaction = get_object_or_404(Transaction, pk=transaction_id)
|
||||
|
||||
transaction.date = timezone.localdate(timezone.now())
|
||||
transaction.save()
|
||||
transaction_updated.send(sender=transaction)
|
||||
|
||||
return HttpResponse(
|
||||
status=204,
|
||||
headers={"HX-Trigger": "updated"},
|
||||
)
|
||||
|
||||
|
||||
@login_required
|
||||
@require_http_methods(["GET"])
|
||||
def transaction_all_index(request):
|
||||
|
||||
@@ -45,7 +45,7 @@ class LoginForm(AuthenticationForm):
|
||||
self.helper.layout = Layout(
|
||||
"username",
|
||||
"password",
|
||||
Submit("Submit", "Login"),
|
||||
Submit("Submit", "Login", css_class="btn btn-primary w-100"),
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ from . import views
|
||||
|
||||
urlpatterns = [
|
||||
path("", views.index, name="index"),
|
||||
path("setup/", views.setup, name="setup"),
|
||||
path("login/", views.UserLoginView.as_view(), name="login"),
|
||||
# path("login/fallback/", views.UserLoginView.as_view(), name="fallback_login"),
|
||||
path("logout/", views.logout_view, name="logout"),
|
||||
|
||||
@@ -21,6 +21,8 @@ from apps.users.forms import (
|
||||
)
|
||||
from apps.users.models import UserSettings
|
||||
from apps.common.decorators.demo import disabled_on_demo
|
||||
from apps.currencies.models import Currency
|
||||
from apps.accounts.models import Account
|
||||
|
||||
|
||||
def logout_view(request):
|
||||
@@ -48,6 +50,28 @@ def index(request):
|
||||
return redirect(reverse("monthly_index"))
|
||||
|
||||
|
||||
@login_required
|
||||
def setup(request):
|
||||
has_currency = Currency.objects.exists()
|
||||
has_account = Account.objects.exists()
|
||||
# return render(
|
||||
# request,
|
||||
# "users/setup/setup.html",
|
||||
# {"has_currency": has_currency, "has_account": has_account},
|
||||
# )
|
||||
if not has_currency or not has_account:
|
||||
return render(
|
||||
request,
|
||||
"users/setup/setup.html",
|
||||
{"has_currency": has_currency, "has_account": has_account},
|
||||
)
|
||||
else:
|
||||
return HttpResponse(
|
||||
status=200,
|
||||
headers={"HX-Reswap": "delete"},
|
||||
)
|
||||
|
||||
|
||||
class UserLoginView(LoginView):
|
||||
form_class = LoginForm
|
||||
template_name = "users/login.html"
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
)
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
+246
-212
File diff suppressed because it is too large
Load Diff
+214
-170
File diff suppressed because it is too large
Load Diff
+244
-186
File diff suppressed because it is too large
Load Diff
+269
-248
File diff suppressed because it is too large
Load Diff
+220
-171
File diff suppressed because it is too large
Load Diff
+223
-172
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+214
-170
File diff suppressed because it is too large
Load Diff
+216
-170
File diff suppressed because it is too large
Load Diff
@@ -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>
|
||||
@@ -120,8 +120,8 @@
|
||||
<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 tw:group-hover/transaction:visible d-flex flex-row card">
|
||||
<div class="card-body p-1 shadow-lg">
|
||||
class="transaction-actions tw:absolute! tw:right-[15px] tw:top-[50%] tw:md:right-auto tw:md:left-1/2 tw:md:top-0 tw:md:-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 d-flex flex-column flex-md-row gap-1">
|
||||
{% if not transaction.deleted %}
|
||||
<a class="btn btn-secondary btn-sm transaction-action"
|
||||
role="button"
|
||||
@@ -145,7 +145,7 @@
|
||||
<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">
|
||||
<ul class="dropdown-menu dropdown-menu-end dropdown-menu-md-start">
|
||||
{% if transaction.category.mute %}
|
||||
<li>
|
||||
<a class="dropdown-item disabled d-flex align-items-center" aria-disabled="true">
|
||||
@@ -163,6 +163,10 @@
|
||||
{% 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_change_month' transaction_id=transaction.id change_type='previous' %}"><i class="fa-solid fa-calendar-minus fa-fw me-2"></i>{% translate 'Move to previous month' %}</a></li>
|
||||
<li><a class="dropdown-item" href="#" hx-get="{% url 'transaction_change_month' transaction_id=transaction.id change_type='next' %}"><i class="fa-solid fa-calendar-plus fa-fw me-2"></i>{% translate 'Move to next month' %}</a></li>
|
||||
<li><a class="dropdown-item" href="#" hx-get="{% url 'transaction_move_to_today' transaction_id=transaction.id %}"><i class="fa-solid fa-calendar-day fa-fw me-2"></i>{% translate 'Move to today' %}</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 %}
|
||||
|
||||
@@ -17,14 +17,14 @@
|
||||
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">
|
||||
@@ -53,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>
|
||||
|
||||
@@ -144,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-dvh">
|
||||
<div class="tw:h-dvh 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,33 @@
|
||||
</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 class="container" hx-get="{% url 'setup' %}" hx-trigger="load, updated from:window"></div>
|
||||
|
||||
{% include 'includes/offcanvas.html' %}
|
||||
{% include 'includes/toasts.html' %}
|
||||
<div id="content">
|
||||
{% block content %}{% endblock %}
|
||||
</div>
|
||||
|
||||
{% include 'includes/offcanvas.html' %}
|
||||
{% include 'includes/toasts.html' %}
|
||||
</main>
|
||||
</div>
|
||||
|
||||
{% include 'includes/tools/calculator.html' %}
|
||||
|
||||
@@ -9,7 +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" class="show-loading" hx-get="" hx-target="body">
|
||||
<div hx-trigger="every 60m, updated from:window" hx-include="#view-type" class="show-loading" hx-get="" hx-target="body">
|
||||
<div class="h-100 text-center mb-4 pt-2">
|
||||
<div class="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">
|
||||
|
||||
@@ -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,9 +107,9 @@
|
||||
hx-include="#filter">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<c-ui.transactions_fab></c-ui.transactions_fab>
|
||||
<c-ui.transactions_fab></c-ui.transactions_fab>
|
||||
{% endblock %}
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
{% load i18n %}
|
||||
<div class="card shadow-sm rounded-3">
|
||||
<div class="card-header border-bottom-0 pt-4 px-4">
|
||||
<h5 class="card-title fw-bold"><i class="fas fa-rocket me-2"></i>Let's Get You Set Up</h5>
|
||||
</div>
|
||||
<div class="card-body p-4">
|
||||
<!-- Explanation Text -->
|
||||
<div class="alert alert-info border-0" role="alert">
|
||||
<div class="d-flex align-items-center">
|
||||
<i class="fas fa-info-circle fa-lg me-3"></i>
|
||||
<div>
|
||||
Welcome to <strong>WYGIWYH</strong>! To get started, you need to configure a currency and create your first account. This will establish the foundation for managing your finances.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Task Lists -->
|
||||
<div class="mt-4 border rounded-3 overflow-hidden">
|
||||
<!-- Required Section -->
|
||||
<div class="p-3 text-bg-secondary text-muted text-uppercase fw-bold small">Required Steps</div>
|
||||
<ul class="list-group list-group-flush">
|
||||
<!-- Task 1: Create Currency -->
|
||||
<li class="list-group-item d-flex justify-content-between align-items-center p-3">
|
||||
<div class="d-flex align-items-center">
|
||||
<i class="fa-regular fa-circle{% if has_currency %}-check{% endif %} fa-fw text-primary me-3"></i>
|
||||
<span class="fw-medium {% if has_currency %}tw:line-through{% endif %}">{% trans 'Add' %} {% trans 'Currency' %}</span>
|
||||
</div>
|
||||
<a href="#" class="btn btn-primary btn-sm"
|
||||
role="button"
|
||||
hx-get="{% url 'currency_add' %}"
|
||||
hx-target="#generic-offcanvas">{% trans 'Add' %} <i class="fas fa-arrow-right ms-1"></i></a>
|
||||
</li>
|
||||
<!-- Task 2: Create Account -->
|
||||
<li class="list-group-item d-flex justify-content-between align-items-center p-3">
|
||||
<div class="d-flex align-items-center">
|
||||
<i class="fa-regular fa-circle{% if has_account %}-check{% endif %} fa-fw text-primary me-3"></i>
|
||||
<span class="fw-medium {% if has_account %}tw:line-through{% endif %}">{% trans 'Add' %} {% trans 'Account' %}</span>
|
||||
</div>
|
||||
<a class="btn btn-primary btn-sm"
|
||||
role="button"
|
||||
hx-get="{% url 'account_add' %}"
|
||||
hx-target="#generic-offcanvas">{% trans 'Add' %} <i class="fas fa-arrow-right ms-1"></i></a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<!-- Optional Section -->
|
||||
<div class="p-3 text-bg-secondary text-muted text-uppercase fw-bold small border-top">Optional Steps</div>
|
||||
<ul class="list-group list-group-flush">
|
||||
<!-- Task 3: Import Data -->
|
||||
<li class="list-group-item d-flex justify-content-between align-items-center p-3">
|
||||
<div class="d-flex align-items-center">
|
||||
<i class="fas fa-upload fa-fw text-secondary me-3"></i>
|
||||
<span class="fw-medium">Import data from another app</span>
|
||||
</div>
|
||||
<a href="#" class="btn btn-outline-secondary btn-sm">Import</a>
|
||||
</li>
|
||||
<!-- Task 4: Invite Team -->
|
||||
<li class="list-group-item d-flex justify-content-between align-items-center p-3">
|
||||
<div class="d-flex align-items-center">
|
||||
<i class="fas fa-user-plus fa-fw text-secondary me-3"></i>
|
||||
<span class="fw-medium">Invite team members</span>
|
||||
</div>
|
||||
<a href="#" class="btn btn-outline-secondary btn-sm">Invite</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -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, every 10m"
|
||||
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, every 10m"
|
||||
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,3 +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