diff --git a/nummi/main/static/main/css/form.css b/nummi/main/static/main/css/form.css
index 5685fa5..f4fd25e 100644
--- a/nummi/main/static/main/css/form.css
+++ b/nummi/main/static/main/css/form.css
@@ -48,141 +48,140 @@ form {
border: 1px solid var(--gray);
padding: var(--gap);
block-size: min-content;
+ }
+ .fieldset {
+ display: grid;
+ grid-auto-columns: 1fr;
+ grid-auto-flow: column;
+ gap: inherit;
+ padding: 0;
+ margin: 0;
+ border: none;
+ }
- .fieldset {
- display: grid;
- grid-auto-columns: 1fr;
- grid-auto-flow: column;
- gap: inherit;
- padding: 0;
- margin: 0;
- border: none;
- }
+ ul.errorlist {
+ color: var(--red);
+ font-weight: 550;
+ list-style: none;
+ padding: 0;
+ margin: 0;
+ }
+
+ .field {
+ display: grid;
+ grid-auto-rows: min-content;
+ align-items: center;
+ column-gap: 0.5rem;
ul.errorlist {
- color: var(--red);
- font-weight: 550;
- list-style: none;
- padding: 0;
- margin: 0;
+ font-size: 0.8rem;
}
- .field {
- display: grid;
- grid-auto-rows: min-content;
- align-items: center;
- column-gap: 0.5rem;
-
- ul.errorlist {
- font-size: 0.8rem;
- }
-
- &:has(> textarea) {
- grid-template-rows: min-content 1fr;
- textarea {
- resize: block;
- }
- }
- &:has(> input[type="checkbox"]) {
- grid-template-columns: min-content 1fr;
- > label {
- font-size: inherit;
- grid-row: 1;
- grid-column: 2;
- padding: 0.5rem;
- line-height: initial;
- }
- > input {
- grid-row: 1;
- grid-column: 1;
- margin: 0.5rem;
- }
- &:has(> :focus) {
- background: var(--bg-01);
- }
- }
-
- > label {
- font-size: 0.8rem;
- line-height: 0.8rem;
- z-index: 10;
- }
-
- > a {
- padding: 0.5rem;
- }
-
- input,
- select,
+ &:has(> textarea) {
+ grid-template-rows: min-content 1fr;
textarea {
- font: inherit;
- line-height: initial;
- border: none;
- border: 1px solid transparent;
- border-bottom: 1px solid var(--gray);
- background: none;
- z-index: 1;
+ resize: block;
+ }
+ }
+ &:has(> input[type="checkbox"]) {
+ grid-template-columns: min-content 1fr;
+ > label {
+ font-size: inherit;
+ grid-row: 1;
+ grid-column: 2;
padding: 0.5rem;
+ line-height: initial;
+ }
+ > input {
+ grid-row: 1;
+ grid-column: 1;
+ margin: 0.5rem;
+ }
+ &:has(> :focus) {
+ background: var(--bg-1);
+ }
+ }
- &:has(~ ul.errorlist) {
- border-color: var(--red);
- }
- &.autocompleted:not(.mod) {
- border-bottom-color: var(--green);
- }
+ > label {
+ font-size: 0.8rem;
+ line-height: 0.8rem;
+ z-index: 10;
+ }
- &:not([type="checkbox"]) {
- width: 100%;
- margin: 0;
- }
+ > a {
+ padding: 0.5rem;
+ }
- &[name*="value"] {
- text-align: right;
- font-feature-settings: var(--num);
- }
- &[name*="date"] {
- font-feature-settings: var(--num);
- }
+ input,
+ select,
+ textarea {
+ font: inherit;
+ line-height: initial;
+ border: none;
+ border: 1px solid transparent;
+ border-bottom: 1px solid var(--gray);
+ background: none;
+ z-index: 1;
+ padding: 0.5rem;
- &:focus {
- outline: none;
- background: var(--bg-01);
- }
+ &:has(~ ul.errorlist) {
+ border-color: var(--red);
+ }
+ &.autocompleted:not(.mod) {
+ border-bottom-color: var(--green);
}
- > .file-input {
- display: grid;
-
- > .current {
- display: grid;
- grid-template-columns: 1fr;
- grid-auto-columns: max-content;
- grid-auto-flow: column;
- a {
- padding: 0.5rem;
- }
- }
-
- input[type="file"] {
- &::file-selector-button {
- display: none;
- }
- }
+ &:not([type="checkbox"]) {
+ width: 100%;
+ margin: 0;
}
- > .ico-input {
+
+ &[name*="value"] {
+ text-align: right;
+ font-feature-settings: var(--num);
+ }
+ &[name*="date"] {
+ font-feature-settings: var(--num);
+ }
+
+ &:focus {
+ outline: none;
+ background: var(--bg-1);
+ }
+ }
+
+ > .file-input {
+ display: grid;
+
+ > .current {
display: grid;
- grid-template-columns: min-content 1fr;
- column-gap: 0.5rem;
- align-items: center;
- span[class|="ri"] {
+ grid-template-columns: 1fr;
+ grid-auto-columns: max-content;
+ grid-auto-flow: column;
+ a {
padding: 0.5rem;
}
+ }
- &:has(> :focus) {
- background: var(--bg-01);
+ input[type="file"] {
+ &::file-selector-button {
+ display: none;
}
}
}
+ > .ico-input {
+ display: grid;
+ grid-template-columns: min-content 1fr;
+ column-gap: 0.5rem;
+ align-items: center;
+ span[class|="ri"] {
+ padding: 0.5rem;
+ }
+
+ &:has(> :focus) {
+ background: var(--bg-1);
+ }
+ }
}
.buttons {
grid-column: 1 / -1;
diff --git a/nummi/main/static/main/css/main.css b/nummi/main/static/main/css/main.css
index b0ef3b0..a3f9ed5 100644
--- a/nummi/main/static/main/css/main.css
+++ b/nummi/main/static/main/css/main.css
@@ -538,3 +538,22 @@ ul.statements {
.value {
font-feature-settings: var(--num);
}
+
+details {
+ border: var(--gray) 1px solid;
+ margin-bottom: var(--gap);
+
+ summary {
+ font-weight: 650;
+ cursor: pointer;
+ padding: var(--gap);
+ }
+
+ &[open] summary {
+ background: var(--bg-1);
+ }
+
+ form {
+ padding: var(--gap);
+ }
+}
diff --git a/nummi/main/static/main/js/base.js b/nummi/main/static/main/js/base.js
index eaafec5..3cf2d92 100644
--- a/nummi/main/static/main/js/base.js
+++ b/nummi/main/static/main/js/base.js
@@ -135,3 +135,14 @@ if (accounts) {
});
}
}
+
+const filterForm = document.querySelector("form.filter");
+if (filterForm) {
+ filterForm.addEventListener("submit", (event) => {
+ for (element of filterForm.elements) {
+ if (element.value == "") {
+ element.disabled = true;
+ }
+ }
+ });
+}
diff --git a/nummi/main/templates/main/pagination_links.html b/nummi/main/templates/main/pagination_links.html
index fcd8531..e2a55b9 100644
--- a/nummi/main/templates/main/pagination_links.html
+++ b/nummi/main/templates/main/pagination_links.html
@@ -1,13 +1,13 @@
{% load i18n main_extras %}
{% if first.show %}
- 1
+ 1
{% if first.dots %}…{% endif %}
{% endif %}
{% for page in pages %}
- {{ page.number }}
{% endfor %}
{% if last.show %}
{% if last.dots %}…{% endif %}
- {{ last.number }}
+ {{ last.number }}
{% endif %}
diff --git a/nummi/main/templatetags/main_extras.py b/nummi/main/templatetags/main_extras.py
index 9dd97e4..1b1e5dc 100644
--- a/nummi/main/templatetags/main_extras.py
+++ b/nummi/main/templatetags/main_extras.py
@@ -91,10 +91,11 @@ def balance(accounts):
)
-@register.inclusion_tag("main/pagination_links.html")
-def pagination_links(page_obj):
+@register.inclusion_tag("main/pagination_links.html", takes_context=True)
+def pagination_links(context, page_obj):
_n = 3
return {
+ "request": context["request"],
"pages": [
{"number": p, "current": p == page_obj.number}
for p in page_obj.paginator.page_range
@@ -110,3 +111,10 @@ def pagination_links(page_obj):
"number": page_obj.paginator.num_pages,
},
}
+
+
+@register.simple_tag(takes_context=True)
+def page_url(context, page):
+ query = context["request"].GET.copy()
+ query["page"] = page
+ return query.urlencode()
diff --git a/nummi/transaction/forms.py b/nummi/transaction/forms.py
index 807c00e..15d3dbd 100644
--- a/nummi/transaction/forms.py
+++ b/nummi/transaction/forms.py
@@ -3,6 +3,7 @@ import json
from category.forms import CategorySelect
from django import forms
from django.forms import formset_factory
+from django.utils.translation import gettext_lazy as _
from main.forms import DatalistInput, NummiFileInput, NummiForm
from statement.forms import StatementSelect
@@ -145,3 +146,55 @@ class MultipleInvoicesForm(forms.Form):
InvoicesFormSet = formset_factory(MultipleInvoicesForm)
+
+
+class DateInput(forms.DateInput):
+ input_type = "date"
+
+ def __init__(self, attrs=None):
+ super().__init__(attrs)
+ self.format = "%Y-%m-%d"
+
+
+class DateField(forms.DateField):
+ def __init__(self, *args, **kwargs):
+ kwargs.setdefault("widget", DateInput())
+ super().__init__(*args, **kwargs)
+
+
+class TransactionFiltersForm(forms.Form):
+ start_date = DateField(required=False)
+ end_date = DateField(required=False)
+ category = forms.ModelChoiceField(
+ queryset=None, required=False, widget=CategorySelect()
+ )
+ account = forms.ModelChoiceField(queryset=None, required=False)
+ search = forms.CharField(label=_("Search"), required=False)
+ sort_by = forms.ChoiceField(
+ label=_("Sort by"),
+ choices=[
+ ("", _("Default")),
+ ("date", _("Date +")),
+ ("-date", _("Date -")),
+ ("value", _("Value +")),
+ ("-value", _("Value -")),
+ ],
+ required=False,
+ )
+
+ def __init__(self, *args, **kwargs):
+ _user = kwargs.pop("user")
+ super().__init__(*args, **kwargs)
+
+ self.fields["category"].queryset = _user.category_set
+ self.fields["account"].queryset = _user.account_set
+
+ self.fields["category"].widget.attrs |= {
+ "class": "category",
+ "data-icons": json.dumps(
+ {
+ str(cat.id): cat.icon
+ for cat in self.fields["category"].queryset.only("id", "icon")
+ }
+ ),
+ }
diff --git a/nummi/transaction/templates/transaction/transaction_filters.html b/nummi/transaction/templates/transaction/transaction_filters.html
new file mode 100644
index 0000000..907a14f
--- /dev/null
+++ b/nummi/transaction/templates/transaction/transaction_filters.html
@@ -0,0 +1,19 @@
+{% load i18n %}
+{% translate "Filters" %}
+
+