diff --git a/core/prediction.py b/core/prediction.py index 167e212..635519d 100644 --- a/core/prediction.py +++ b/core/prediction.py @@ -113,3 +113,66 @@ def predict_all(subjects: Iterable[Subject], past_days=30, future_days=60): prediction_list.append((subject, transactions, prediction_info)) return prediction_list + + +min_pos_color = (Decimal("50"), Decimal("150"), Decimal("220")) +max_pos_color = (Decimal("50"), Decimal("170"), Decimal("20")) +min_neg_color = (Decimal("220"), Decimal("70"), Decimal("30")) +max_neg_color = (Decimal("180"), Decimal("160"), Decimal("30")) + +global_minimum = Decimal("-300") +global_maximum = Decimal("300") + + +def limit(amount: Decimal, first_bound: Decimal, second_bound: Decimal): + lower = min(first_bound, second_bound) + upper = max(first_bound, second_bound) + return max(min(amount, upper), lower) + + +def get_color_for_amount(amount: Decimal, minimum: Decimal, maximum: Decimal): + if amount >= Decimal(): + minimum = Decimal() + maximum = min(maximum, global_maximum) + rmin, gmin, bmin = min_pos_color + rmax, gmax, bmax = max_pos_color + else: + minimum = max(minimum, global_minimum) + maximum = Decimal() + rmin, gmin, bmin = min_neg_color + rmax, gmax, bmax = max_neg_color + assert minimum < maximum + percentage = (amount - minimum) / (maximum - minimum) + return "#%02x%02x%02x" % ( + round(limit(rmin + (percentage * (rmax - rmin)), rmin, rmax)), + round(limit(gmin + (percentage * (gmax - gmin)), gmin, gmax)), + round(limit(bmin + (percentage * (bmax - bmin)), bmin, bmax)), + ) + + +def predict_balance(start_balance=Decimal("0")): + prediction_list = predict_all(Subject.objects.order_by("name")) + today = timezone.now().date() + future_transactions = [] + minimum, maximum = Decimal(), Decimal() + for _, transactions, _ in prediction_list: + for transaction in transactions: + if transaction.booking_date > today: + future_transactions.append(transaction) + minimum = min(minimum, transaction.amount) + maximum = max(maximum, transaction.amount) + + formatted_transactions = [] + current_balance = start_balance + for transaction in sorted(future_transactions, key=lambda t: t.booking_date): + current_balance += transaction.amount + formatted_transactions.append({ + "date": transaction.booking_date, + "subject": transaction.subject, + "amount": transaction.amount, + "balance": current_balance, + "predicted": transaction.pk is None, + "color": get_color_for_amount(transaction.amount, minimum, maximum), + }) + + return formatted_transactions diff --git a/core/static/core/style.css b/core/static/core/style.css index 4ba4d11..14c90de 100644 --- a/core/static/core/style.css +++ b/core/static/core/style.css @@ -28,19 +28,17 @@ body { color: #EEEEEE; } -h1, p, a { - margin: 0; - border: none; - padding: 0; - text-decoration: none; -} - h1 { - margin-bottom: 12px; + margin: 0 0 12px; + padding: 0; font-size: 1.3rem; font-weight: bold; } +p { + margin: 6px 0 10px; +} + #title, #navi, #content { flex: 0 1 0; width: 95%; @@ -51,7 +49,7 @@ h1 { #title { font-size: 1.6rem; text-align: center; - color: #6699EE; + color: #77AAEE; } #navi { @@ -62,7 +60,8 @@ h1 { } a { - color: #6699EE; + text-decoration: none; + color: #77AAEE; transition: color 100ms; } @@ -70,19 +69,20 @@ a:hover, a:focus { color: #EEEEEE; } +form { + margin: 12px 0; +} + form label { display: none; } -#login-form { - display: flex; - flex-direction: column; - align-items: center; +form input { + margin: 0 0 6px; } -#id_username, #id_password, #login-warning, #login-button { - flex: 0 1 0; - margin: 4px; +form p { + color: #EE9966; } table, thead, tbody, tfoot, tr, td { @@ -103,7 +103,7 @@ thead { } tr { - border-bottom: 1px solid #444; + border-bottom: 1px solid #444444; } td { @@ -151,3 +151,81 @@ td { max-width: 240px; } } + +#transaction-list { + margin: 12px 0; + border: none; + padding: 0; + display: flex; + flex-direction: column; +} + +.transaction-block { + flex: 0 1 0; + max-width: 800px; + margin: 0 0 8px; + border-radius: 4px; + padding: 4px; + display: flex; + flex-direction: row; +} + +.transaction-block-first { + flex: 1 0 0; + margin: 0 8px 0 0; +} + +.transaction-block-second { + flex: 0 1 0; + margin: 0 0 0 8px; + align-items: end; +} + +.transaction-block-first, .transaction-block-second { + border: none; + padding: 0; + display: flex; + flex-direction: column; + justify-content: space-between; +} + +.date-block, .subject-block, .amount-block, .balance-block { + flex: 0 1 0; + padding: 3px; +} + +.date-block, .amount-block, .balance-block { + white-space: nowrap; +} + +.amount-block, .balance-block { + text-align: right; +} + +.amount-block { + color: #BBBBBB; + background-color: #222222; + border-radius: 4px; + padding: 3px 6px; +} + +.balance-block { + color: #000000; + font-size: 1.25rem; + font-weight: bold; +} + +@media only screen and (min-width: 480px) { + .transaction-block-first, .transaction-block-second { + flex-direction: row; + align-items: center; + } + + .subject-block { + flex: 1 0 0; + } + + .subject-block, .amount-block { + margin: 0 12px; + } +} diff --git a/core/templates/core/balance.html b/core/templates/core/balance.html new file mode 100644 index 0000000..cd8f3ed --- /dev/null +++ b/core/templates/core/balance.html @@ -0,0 +1,57 @@ +{% extends "core/base.html" %} + +{% block navi %} + subjects + | + admin + | + logout +{% endblock %} + +{% block content %} + +

balance prediction

+ +
+ + + + {% if amount_error %} +

try a number, stupid

+ {% endif %} +
+ +

starting with {{ amount }}€ ...

+ + {% if future_transactions %} +
+ {% for transaction in future_transactions %} +
+
+
+ {% if transaction.predicted %}🔮{% else %}🗒{% endif %} + {{ transaction.date|date:"d.m.Y" }} +
+
+ {{ transaction.subject }} +
+
+
+
+ {{ transaction.amount }} +
+
+ {{ transaction.balance }} +
+
+
+ {% endfor %} +
+ {% else %} +

no data to show :/

+ {% endif %} + +{% endblock %} diff --git a/core/templates/core/subjects.html b/core/templates/core/subjects.html index f93f48e..07e8567 100644 --- a/core/templates/core/subjects.html +++ b/core/templates/core/subjects.html @@ -1,6 +1,8 @@ {% extends "core/base.html" %} {% block navi %} + balance + | admin | logout @@ -26,7 +28,7 @@ {% if forloop.counter == 1 %} {{ subject.name }} {% endif %} - + {% if transaction.pk %}🗒{% else %}🔮{% endif %} {{ transaction.booking_date|date:"d.m.y" }} @@ -37,9 +39,7 @@ {% else %} -

- no data to show :/ -

+

no data to show :/

{% endif %} {% endblock %} diff --git a/core/templates/registration/login.html b/core/templates/registration/login.html index a795899..bc34c4f 100644 --- a/core/templates/registration/login.html +++ b/core/templates/registration/login.html @@ -6,19 +6,17 @@ {% block content %} -
+ {% csrf_token %} - - - - - {% if form.errors %} -
- nope. -
- {% endif %} - + + + + + + {% if form.errors %} +

nope.

+ {% endif %}
{% endblock %} diff --git a/core/urls.py b/core/urls.py index d5aa229..2cdc553 100644 --- a/core/urls.py +++ b/core/urls.py @@ -2,9 +2,10 @@ from django.contrib.auth.decorators import login_required from django.urls import path from django.views.generic import RedirectView -from core.views import SubjectsView +from core.views import BalanceView, SubjectsView urlpatterns = [ path("", RedirectView.as_view(pattern_name="core:subjects", permanent=False), name="index"), + path("balance/", login_required(BalanceView.as_view()), name="balance"), path("subjects/", login_required(SubjectsView.as_view()), name="subjects"), ] diff --git a/core/views.py b/core/views.py index fdb27e6..2536eb3 100644 --- a/core/views.py +++ b/core/views.py @@ -1,7 +1,26 @@ +from decimal import Decimal, InvalidOperation + from django.views.generic import TemplateView from core.models import Subject -from core.prediction import predict_all +from core.prediction import predict_all, predict_balance + + +class BalanceView(TemplateView): + template_name = "core/balance.html" + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + amount, amount_error = Decimal(), False + if amount_query := self.request.GET.get("amount"): + try: + amount = Decimal(amount_query) + except InvalidOperation: + amount_error = True + context["amount"] = round(amount, 2) + context["amount_error"] = amount_error + context["future_transactions"] = predict_balance(amount) + return context class SubjectsView(TemplateView):