mirror of
https://github.com/eitchtee/WYGIWYH.git
synced 2026-04-25 10:08:36 +02:00
Merge pull request #40 from eitchtee/new_datepicker
feat(datepicker): drop native datepickers in favor of AirDatePicker for better compatibility
This commit is contained in:
@@ -61,7 +61,6 @@ class DynamicModelChoiceField(forms.ModelChoiceField):
|
|||||||
self._created_instance = instance
|
self._created_instance = instance
|
||||||
return instance
|
return instance
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(e)
|
|
||||||
raise ValidationError(
|
raise ValidationError(
|
||||||
self.error_messages["invalid_choice"], code="invalid_choice"
|
self.error_messages["invalid_choice"], code="invalid_choice"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
import datetime
|
import datetime
|
||||||
|
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.db import models
|
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
|
from django.db import models
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
from apps.common.widgets.datepicker import AirMonthYearPickerInput
|
||||||
from apps.common.widgets.month_year import MonthYearWidget
|
from apps.common.widgets.month_year import MonthYearWidget
|
||||||
|
|
||||||
|
|
||||||
@@ -27,7 +29,7 @@ class MonthYearModelField(models.DateField):
|
|||||||
|
|
||||||
|
|
||||||
class MonthYearFormField(forms.DateField):
|
class MonthYearFormField(forms.DateField):
|
||||||
widget = MonthYearWidget
|
widget = AirMonthYearPickerInput
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
@@ -42,7 +44,11 @@ class MonthYearFormField(forms.DateField):
|
|||||||
date = datetime.datetime.strptime(value, "%Y-%m")
|
date = datetime.datetime.strptime(value, "%Y-%m")
|
||||||
return date.replace(day=1).date()
|
return date.replace(day=1).date()
|
||||||
except ValueError:
|
except ValueError:
|
||||||
raise ValidationError(_("Invalid date format. Use YYYY-MM."))
|
try:
|
||||||
|
date = datetime.datetime.strptime(value, "%Y-%m-%d")
|
||||||
|
return date.replace(day=1).date()
|
||||||
|
except ValueError:
|
||||||
|
raise ValidationError(_("Invalid date format. Use YYYY-MM."))
|
||||||
|
|
||||||
def prepare_value(self, value):
|
def prepare_value(self, value):
|
||||||
if isinstance(value, datetime.date):
|
if isinstance(value, datetime.date):
|
||||||
|
|||||||
32
app/apps/common/utils/django.py
Normal file
32
app/apps/common/utils/django.py
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
def django_to_python_datetime(django_format):
|
||||||
|
mapping = {
|
||||||
|
# Day
|
||||||
|
"j": "%d", # Day of the month without leading zeros
|
||||||
|
"d": "%d", # Day of the month with leading zeros
|
||||||
|
"D": "%a", # Day of the week, short version
|
||||||
|
"l": "%A", # Day of the week, full version
|
||||||
|
# Month
|
||||||
|
"n": "%m", # Month without leading zeros
|
||||||
|
"m": "%m", # Month with leading zeros
|
||||||
|
"M": "%b", # Month, short version
|
||||||
|
"F": "%B", # Month, full version
|
||||||
|
# Year
|
||||||
|
"y": "%y", # Year, 2 digits
|
||||||
|
"Y": "%Y", # Year, 4 digits
|
||||||
|
# Time
|
||||||
|
"g": "%I", # Hour (12-hour), without leading zeros
|
||||||
|
"G": "%H", # Hour (24-hour), without leading zeros
|
||||||
|
"h": "%I", # Hour (12-hour), with leading zeros
|
||||||
|
"H": "%H", # Hour (24-hour), with leading zeros
|
||||||
|
"i": "%M", # Minutes
|
||||||
|
"s": "%S", # Seconds
|
||||||
|
"a": "%p", # am/pm
|
||||||
|
"A": "%p", # AM/PM
|
||||||
|
"P": "%I:%M %p",
|
||||||
|
}
|
||||||
|
|
||||||
|
python_format = django_format
|
||||||
|
for django_code, python_code in mapping.items():
|
||||||
|
python_format = python_format.replace(django_code, python_code)
|
||||||
|
|
||||||
|
return python_format
|
||||||
185
app/apps/common/widgets/datepicker.py
Normal file
185
app/apps/common/widgets/datepicker.py
Normal file
@@ -0,0 +1,185 @@
|
|||||||
|
import datetime
|
||||||
|
|
||||||
|
from django.forms import widgets
|
||||||
|
from django.utils import formats, translation, dates
|
||||||
|
from django.utils.formats import get_format
|
||||||
|
|
||||||
|
from apps.common.utils.django import django_to_python_datetime
|
||||||
|
|
||||||
|
|
||||||
|
class AirDatePickerInput(widgets.DateInput):
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
attrs=None,
|
||||||
|
format=None,
|
||||||
|
clear_button=True,
|
||||||
|
auto_close=True,
|
||||||
|
*args,
|
||||||
|
**kwargs,
|
||||||
|
):
|
||||||
|
attrs = attrs or {}
|
||||||
|
|
||||||
|
super().__init__(attrs=attrs, format=format, *args, **kwargs)
|
||||||
|
|
||||||
|
self.clear_button = clear_button
|
||||||
|
self.auto_close = auto_close
|
||||||
|
|
||||||
|
def _get_current_language(self):
|
||||||
|
"""Get current language code in format compatible with AirDatepicker"""
|
||||||
|
lang_code = translation.get_language()
|
||||||
|
# AirDatepicker uses simple language codes
|
||||||
|
return lang_code.split("-")[0]
|
||||||
|
|
||||||
|
def build_attrs(self, base_attrs, extra_attrs=None):
|
||||||
|
attrs = super().build_attrs(base_attrs, extra_attrs)
|
||||||
|
|
||||||
|
# Add data attributes for AirDatepicker configuration
|
||||||
|
attrs["data-auto-close"] = str(self.auto_close).lower()
|
||||||
|
attrs["data-clear-button"] = str(self.clear_button).lower()
|
||||||
|
attrs["data-language"] = self._get_current_language()
|
||||||
|
attrs["data-date-format"] = self.format or get_format(
|
||||||
|
"SHORT_DATE_FORMAT", use_l10n=True
|
||||||
|
)
|
||||||
|
|
||||||
|
return attrs
|
||||||
|
|
||||||
|
def format_value(self, value):
|
||||||
|
"""Format the value for display in the widget."""
|
||||||
|
if value:
|
||||||
|
self.attrs["data-value"] = (
|
||||||
|
value # We use this to dynamically select the initial date on AirDatePicker
|
||||||
|
)
|
||||||
|
|
||||||
|
if value is None:
|
||||||
|
return ""
|
||||||
|
if isinstance(value, (datetime.date, datetime.datetime)):
|
||||||
|
return formats.date_format(
|
||||||
|
value, format=self.format or "SHORT_DATE_FORMAT", use_l10n=True
|
||||||
|
)
|
||||||
|
|
||||||
|
return str(value)
|
||||||
|
|
||||||
|
|
||||||
|
class AirDateTimePickerInput(widgets.DateTimeInput):
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
attrs=None,
|
||||||
|
format=None,
|
||||||
|
timepicker=True,
|
||||||
|
clear_button=True,
|
||||||
|
auto_close=True,
|
||||||
|
*args,
|
||||||
|
**kwargs,
|
||||||
|
):
|
||||||
|
attrs = attrs or {}
|
||||||
|
|
||||||
|
super().__init__(attrs=attrs, format=format, *args, **kwargs)
|
||||||
|
|
||||||
|
self.timepicker = timepicker
|
||||||
|
self.clear_button = clear_button
|
||||||
|
self.auto_close = auto_close
|
||||||
|
|
||||||
|
def _get_current_language(self):
|
||||||
|
"""Get current language code in format compatible with AirDatepicker"""
|
||||||
|
lang_code = translation.get_language()
|
||||||
|
# AirDatepicker uses simple language codes
|
||||||
|
return lang_code.split("-")[0]
|
||||||
|
|
||||||
|
def build_attrs(self, base_attrs, extra_attrs=None):
|
||||||
|
attrs = super().build_attrs(base_attrs, extra_attrs)
|
||||||
|
|
||||||
|
# Add data attributes for AirDatepicker configuration
|
||||||
|
attrs["data-timepicker"] = str(self.timepicker).lower()
|
||||||
|
attrs["data-auto-close"] = str(self.auto_close).lower()
|
||||||
|
attrs["data-clear-button"] = str(self.clear_button).lower()
|
||||||
|
attrs["data-language"] = self._get_current_language()
|
||||||
|
attrs["data-date-format"] = self.format or get_format(
|
||||||
|
"SHORT_DATETIME_FORMAT", use_l10n=True
|
||||||
|
)
|
||||||
|
|
||||||
|
return attrs
|
||||||
|
|
||||||
|
def format_value(self, value):
|
||||||
|
"""Format the value for display in the widget."""
|
||||||
|
if value:
|
||||||
|
self.attrs["data-value"] = (
|
||||||
|
value # We use this to dynamically select the initial date on AirDatePicker
|
||||||
|
)
|
||||||
|
|
||||||
|
if value is None:
|
||||||
|
return ""
|
||||||
|
if isinstance(value, (datetime.date, datetime.datetime)):
|
||||||
|
return formats.date_format(
|
||||||
|
value, format=self.format or "SHORT_DATETIME_FORMAT", use_l10n=True
|
||||||
|
)
|
||||||
|
|
||||||
|
return str(value)
|
||||||
|
|
||||||
|
def value_from_datadict(self, data, files, name):
|
||||||
|
"""Parse the datetime string from the form data."""
|
||||||
|
value = super().value_from_datadict(data, files, name)
|
||||||
|
if value:
|
||||||
|
try:
|
||||||
|
# This does it's best to convert Django's PHP-Style date format to a datetime format and reformat the
|
||||||
|
# value to be read by Django. Probably could be improved
|
||||||
|
return datetime.datetime.strptime(
|
||||||
|
value,
|
||||||
|
self.format
|
||||||
|
or django_to_python_datetime(get_format("SHORT_DATETIME_FORMAT")),
|
||||||
|
).strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
except (ValueError, TypeError) as e:
|
||||||
|
return value
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
class AirMonthYearPickerInput(AirDatePickerInput):
|
||||||
|
def __init__(self, attrs=None, format=None, *args, **kwargs):
|
||||||
|
super().__init__(attrs=attrs, format=format, *args, **kwargs)
|
||||||
|
# Store the display format for AirDatepicker
|
||||||
|
self.display_format = "MMMM yyyy"
|
||||||
|
# Store the Python format for internal use
|
||||||
|
self.python_format = "%B %Y"
|
||||||
|
|
||||||
|
def _get_month_names(self):
|
||||||
|
"""Get month names using Django's date translation"""
|
||||||
|
return {dates.MONTHS[i]: i for i in range(1, 13)}
|
||||||
|
|
||||||
|
def format_value(self, value):
|
||||||
|
"""Format the value for display in the widget."""
|
||||||
|
if value:
|
||||||
|
self.attrs["data-value"] = (
|
||||||
|
value # We use this to dynamically select the initial date on AirDatePicker
|
||||||
|
)
|
||||||
|
|
||||||
|
if value is None:
|
||||||
|
return ""
|
||||||
|
if isinstance(value, str):
|
||||||
|
try:
|
||||||
|
value = datetime.datetime.strptime(value, "%Y-%m-%d").date()
|
||||||
|
except ValueError:
|
||||||
|
return value
|
||||||
|
if isinstance(value, (datetime.datetime, datetime.date)):
|
||||||
|
# Use Django's date translation
|
||||||
|
month_name = dates.MONTHS[value.month]
|
||||||
|
return f"{month_name} {value.year}"
|
||||||
|
return value
|
||||||
|
|
||||||
|
def value_from_datadict(self, data, files, name):
|
||||||
|
"""Convert the value from the widget format back to a format Django can handle."""
|
||||||
|
value = super().value_from_datadict(data, files, name)
|
||||||
|
if value:
|
||||||
|
try:
|
||||||
|
# Split the value into month name and year
|
||||||
|
month_str, year_str = value.rsplit(" ", 1)
|
||||||
|
year = int(year_str)
|
||||||
|
|
||||||
|
# Get month number from translated month name
|
||||||
|
month_names = self._get_month_names()
|
||||||
|
month = month_names.get(month_str)
|
||||||
|
|
||||||
|
if month and year:
|
||||||
|
# Return the first day of the month in Django's expected format
|
||||||
|
return datetime.date(year, month, 1).strftime("%Y-%m-%d")
|
||||||
|
except (ValueError, KeyError):
|
||||||
|
return None
|
||||||
|
return None
|
||||||
@@ -6,9 +6,10 @@ from django.forms import CharField
|
|||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from apps.common.widgets.crispy.submit import NoClassSubmit
|
from apps.common.widgets.crispy.submit import NoClassSubmit
|
||||||
|
from apps.common.widgets.datepicker import AirDateTimePickerInput
|
||||||
from apps.common.widgets.decimal import ArbitraryDecimalDisplayNumberInput
|
from apps.common.widgets.decimal import ArbitraryDecimalDisplayNumberInput
|
||||||
from apps.currencies.models import Currency, ExchangeRate
|
|
||||||
from apps.common.widgets.tom_select import TomSelect
|
from apps.common.widgets.tom_select import TomSelect
|
||||||
|
from apps.currencies.models import Currency, ExchangeRate
|
||||||
|
|
||||||
|
|
||||||
class CurrencyForm(forms.ModelForm):
|
class CurrencyForm(forms.ModelForm):
|
||||||
@@ -64,9 +65,10 @@ class CurrencyForm(forms.ModelForm):
|
|||||||
|
|
||||||
class ExchangeRateForm(forms.ModelForm):
|
class ExchangeRateForm(forms.ModelForm):
|
||||||
date = forms.DateTimeField(
|
date = forms.DateTimeField(
|
||||||
widget=forms.DateTimeInput(
|
widget=AirDateTimePickerInput(
|
||||||
attrs={"type": "datetime-local"}, format="%Y-%m-%dT%H:%M"
|
clear_button=False,
|
||||||
)
|
),
|
||||||
|
label=_("Date"),
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
from crispy_forms.bootstrap import FormActions
|
from crispy_forms.bootstrap import FormActions
|
||||||
from django import forms
|
|
||||||
from crispy_forms.helper import FormHelper
|
from crispy_forms.helper import FormHelper
|
||||||
from crispy_forms.layout import Layout, Row, Column
|
from crispy_forms.layout import Layout, Row, Column
|
||||||
|
from django import forms
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
from apps.common.widgets.crispy.submit import NoClassSubmit
|
||||||
|
from apps.common.widgets.datepicker import AirDatePickerInput
|
||||||
|
from apps.common.widgets.decimal import ArbitraryDecimalDisplayNumberInput
|
||||||
from apps.common.widgets.tom_select import TomSelect
|
from apps.common.widgets.tom_select import TomSelect
|
||||||
from apps.dca.models import DCAStrategy, DCAEntry
|
from apps.dca.models import DCAStrategy, DCAEntry
|
||||||
from apps.common.widgets.decimal import ArbitraryDecimalDisplayNumberInput
|
|
||||||
from apps.common.widgets.crispy.submit import NoClassSubmit
|
|
||||||
|
|
||||||
|
|
||||||
class DCAStrategyForm(forms.ModelForm):
|
class DCAStrategyForm(forms.ModelForm):
|
||||||
@@ -61,7 +62,7 @@ class DCAEntryForm(forms.ModelForm):
|
|||||||
"notes",
|
"notes",
|
||||||
]
|
]
|
||||||
widgets = {
|
widgets = {
|
||||||
"date": forms.DateInput(attrs={"type": "date"}, format="%Y-%m-%d"),
|
"date": AirDatePickerInput(clear_button=False),
|
||||||
"notes": forms.Textarea(attrs={"rows": 3}),
|
"notes": forms.Textarea(attrs={"rows": 3}),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ from django_filters import Filter
|
|||||||
|
|
||||||
from apps.accounts.models import Account
|
from apps.accounts.models import Account
|
||||||
from apps.common.fields.month_year import MonthYearFormField
|
from apps.common.fields.month_year import MonthYearFormField
|
||||||
|
from apps.common.widgets.datepicker import AirDatePickerInput
|
||||||
from apps.common.widgets.decimal import ArbitraryDecimalDisplayNumberInput
|
from apps.common.widgets.decimal import ArbitraryDecimalDisplayNumberInput
|
||||||
from apps.common.widgets.tom_select import TomSelectMultiple
|
from apps.common.widgets.tom_select import TomSelectMultiple
|
||||||
from apps.currencies.models import Currency
|
from apps.currencies.models import Currency
|
||||||
@@ -87,13 +88,13 @@ class TransactionsFilter(django_filters.FilterSet):
|
|||||||
date_start = django_filters.DateFilter(
|
date_start = django_filters.DateFilter(
|
||||||
field_name="date",
|
field_name="date",
|
||||||
lookup_expr="gte",
|
lookup_expr="gte",
|
||||||
widget=forms.DateInput(attrs={"type": "date"}, format="%Y-%m-%d"),
|
widget=AirDatePickerInput(),
|
||||||
label=_("Date from"),
|
label=_("Date from"),
|
||||||
)
|
)
|
||||||
date_end = django_filters.DateFilter(
|
date_end = django_filters.DateFilter(
|
||||||
field_name="date",
|
field_name="date",
|
||||||
lookup_expr="lte",
|
lookup_expr="lte",
|
||||||
widget=forms.DateInput(attrs={"type": "date"}, format="%Y-%m-%d"),
|
widget=AirDatePickerInput(),
|
||||||
label=_("Until"),
|
label=_("Until"),
|
||||||
)
|
)
|
||||||
reference_date_start = MonthYearFilter(
|
reference_date_start = MonthYearFilter(
|
||||||
|
|||||||
@@ -16,10 +16,11 @@ from apps.common.fields.forms.dynamic_select import (
|
|||||||
DynamicModelChoiceField,
|
DynamicModelChoiceField,
|
||||||
DynamicModelMultipleChoiceField,
|
DynamicModelMultipleChoiceField,
|
||||||
)
|
)
|
||||||
from apps.common.fields.month_year import MonthYearFormField
|
|
||||||
from apps.common.widgets.crispy.submit import NoClassSubmit
|
from apps.common.widgets.crispy.submit import NoClassSubmit
|
||||||
|
from apps.common.widgets.datepicker import AirDatePickerInput, AirMonthYearPickerInput
|
||||||
from apps.common.widgets.decimal import ArbitraryDecimalDisplayNumberInput
|
from apps.common.widgets.decimal import ArbitraryDecimalDisplayNumberInput
|
||||||
from apps.common.widgets.tom_select import TomSelect
|
from apps.common.widgets.tom_select import TomSelect
|
||||||
|
from apps.rules.signals import transaction_created, transaction_updated
|
||||||
from apps.transactions.models import (
|
from apps.transactions.models import (
|
||||||
Transaction,
|
Transaction,
|
||||||
TransactionCategory,
|
TransactionCategory,
|
||||||
@@ -28,7 +29,6 @@ from apps.transactions.models import (
|
|||||||
RecurringTransaction,
|
RecurringTransaction,
|
||||||
TransactionEntity,
|
TransactionEntity,
|
||||||
)
|
)
|
||||||
from apps.rules.signals import transaction_created, transaction_updated
|
|
||||||
|
|
||||||
|
|
||||||
class TransactionForm(forms.ModelForm):
|
class TransactionForm(forms.ModelForm):
|
||||||
@@ -59,7 +59,14 @@ class TransactionForm(forms.ModelForm):
|
|||||||
label=_("Account"),
|
label=_("Account"),
|
||||||
widget=TomSelect(clear_button=False, group_by="group"),
|
widget=TomSelect(clear_button=False, group_by="group"),
|
||||||
)
|
)
|
||||||
reference_date = MonthYearFormField(label=_("Reference Date"), required=False)
|
|
||||||
|
date = forms.DateField(
|
||||||
|
widget=AirDatePickerInput(clear_button=False), label=_("Date")
|
||||||
|
)
|
||||||
|
|
||||||
|
reference_date = forms.DateField(
|
||||||
|
widget=AirMonthYearPickerInput(), label=_("Reference Date"), required=False
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Transaction
|
model = Transaction
|
||||||
@@ -77,7 +84,6 @@ class TransactionForm(forms.ModelForm):
|
|||||||
"entities",
|
"entities",
|
||||||
]
|
]
|
||||||
widgets = {
|
widgets = {
|
||||||
"date": forms.DateInput(attrs={"type": "date"}, format="%Y-%m-%d"),
|
|
||||||
"notes": forms.Textarea(attrs={"rows": 3}),
|
"notes": forms.Textarea(attrs={"rows": 3}),
|
||||||
"account": TomSelect(clear_button=False, group_by="group"),
|
"account": TomSelect(clear_button=False, group_by="group"),
|
||||||
}
|
}
|
||||||
@@ -118,8 +124,8 @@ class TransactionForm(forms.ModelForm):
|
|||||||
css_class="form-row",
|
css_class="form-row",
|
||||||
),
|
),
|
||||||
Row(
|
Row(
|
||||||
Column("date", css_class="form-group col-md-6 mb-0"),
|
Column(Field("date"), css_class="form-group col-md-6 mb-0"),
|
||||||
Column("reference_date", css_class="form-group col-md-6 mb-0"),
|
Column(Field("reference_date"), css_class="form-group col-md-6 mb-0"),
|
||||||
css_class="form-row",
|
css_class="form-row",
|
||||||
),
|
),
|
||||||
"description",
|
"description",
|
||||||
@@ -235,10 +241,13 @@ class TransferForm(forms.Form):
|
|||||||
)
|
)
|
||||||
|
|
||||||
date = forms.DateField(
|
date = forms.DateField(
|
||||||
label=_("Date"),
|
widget=AirDatePickerInput(clear_button=False), label=_("Date")
|
||||||
widget=forms.DateInput(attrs={"type": "date"}, format="%Y-%m-%d"),
|
|
||||||
)
|
)
|
||||||
reference_date = MonthYearFormField(label=_("Reference Date"), required=False)
|
|
||||||
|
reference_date = forms.DateField(
|
||||||
|
widget=AirMonthYearPickerInput(), label=_("Reference Date"), required=False
|
||||||
|
)
|
||||||
|
|
||||||
description = forms.CharField(max_length=500, label=_("Description"))
|
description = forms.CharField(max_length=500, label=_("Description"))
|
||||||
notes = forms.CharField(
|
notes = forms.CharField(
|
||||||
required=False,
|
required=False,
|
||||||
@@ -404,7 +413,10 @@ class InstallmentPlanForm(forms.ModelForm):
|
|||||||
queryset=TransactionEntity.objects.filter(active=True),
|
queryset=TransactionEntity.objects.filter(active=True),
|
||||||
)
|
)
|
||||||
type = forms.ChoiceField(choices=Transaction.Type.choices)
|
type = forms.ChoiceField(choices=Transaction.Type.choices)
|
||||||
reference_date = MonthYearFormField(label=_("Reference Date"), required=False)
|
|
||||||
|
reference_date = forms.DateField(
|
||||||
|
widget=AirMonthYearPickerInput(), label=_("Reference Date"), required=False
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = InstallmentPlan
|
model = InstallmentPlan
|
||||||
@@ -424,10 +436,10 @@ class InstallmentPlanForm(forms.ModelForm):
|
|||||||
"entities",
|
"entities",
|
||||||
]
|
]
|
||||||
widgets = {
|
widgets = {
|
||||||
"start_date": forms.DateInput(attrs={"type": "date"}, format="%Y-%m-%d"),
|
|
||||||
"account": TomSelect(),
|
"account": TomSelect(),
|
||||||
"recurrence": TomSelect(clear_button=False),
|
"recurrence": TomSelect(clear_button=False),
|
||||||
"notes": forms.Textarea(attrs={"rows": 3}),
|
"notes": forms.Textarea(attrs={"rows": 3}),
|
||||||
|
"start_date": AirDatePickerInput(clear_button=False),
|
||||||
}
|
}
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
@@ -646,7 +658,6 @@ class RecurringTransactionForm(forms.ModelForm):
|
|||||||
queryset=TransactionEntity.objects.filter(active=True),
|
queryset=TransactionEntity.objects.filter(active=True),
|
||||||
)
|
)
|
||||||
type = forms.ChoiceField(choices=Transaction.Type.choices)
|
type = forms.ChoiceField(choices=Transaction.Type.choices)
|
||||||
reference_date = MonthYearFormField(label=_("Reference Date"), required=False)
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = RecurringTransaction
|
model = RecurringTransaction
|
||||||
@@ -666,8 +677,9 @@ class RecurringTransactionForm(forms.ModelForm):
|
|||||||
"entities",
|
"entities",
|
||||||
]
|
]
|
||||||
widgets = {
|
widgets = {
|
||||||
"start_date": forms.DateInput(attrs={"type": "date"}, format="%Y-%m-%d"),
|
"start_date": AirDatePickerInput(clear_button=False),
|
||||||
"end_date": forms.DateInput(attrs={"type": "date"}, format="%Y-%m-%d"),
|
"end_date": AirDatePickerInput(),
|
||||||
|
"reference_date": AirMonthYearPickerInput(),
|
||||||
"recurrence_type": TomSelect(clear_button=False),
|
"recurrence_type": TomSelect(clear_button=False),
|
||||||
"notes": forms.Textarea(
|
"notes": forms.Textarea(
|
||||||
attrs={
|
attrs={
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
<button type="button" class="btn-close" data-bs-dismiss="offcanvas" aria-label={% translate 'Close' %}></button>
|
<button type="button" class="btn-close" data-bs-dismiss="offcanvas" aria-label={% translate 'Close' %}></button>
|
||||||
</div>
|
</div>
|
||||||
<div id="generic-offcanvas-body" class="offcanvas-body"
|
<div id="generic-offcanvas-body" class="offcanvas-body"
|
||||||
_="install init_tom_select">
|
_="install init_tom_select
|
||||||
|
install init_datepicker">
|
||||||
{% block body %}{% endblock %}
|
{% block body %}{% endblock %}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,19 +3,18 @@
|
|||||||
{% javascript_pack 'bootstrap' attrs="defer" %}
|
{% javascript_pack 'bootstrap' attrs="defer" %}
|
||||||
{% javascript_pack 'sweetalert2' attrs="defer" %}
|
{% javascript_pack 'sweetalert2' attrs="defer" %}
|
||||||
{% javascript_pack 'select' attrs="defer" %}
|
{% javascript_pack 'select' attrs="defer" %}
|
||||||
|
{% javascript_pack 'datepicker' %}
|
||||||
|
|
||||||
{% include 'includes/scripts/hyperscript/init_tom_select.html' %}
|
{% include 'includes/scripts/hyperscript/init_tom_select.html' %}
|
||||||
|
{% include 'includes/scripts/hyperscript/init_date_picker.html' %}
|
||||||
{% include 'includes/scripts/hyperscript/hide_amount.html' %}
|
{% include 'includes/scripts/hyperscript/hide_amount.html' %}
|
||||||
{% include 'includes/scripts/hyperscript/tooltip.html' %}
|
{% include 'includes/scripts/hyperscript/tooltip.html' %}
|
||||||
{% include 'includes/scripts/hyperscript/htmx_error_handler.html' %}
|
{% include 'includes/scripts/hyperscript/htmx_error_handler.html' %}
|
||||||
{% include 'includes/scripts/hyperscript/sounds.html' %}
|
{% include 'includes/scripts/hyperscript/sounds.html' %}
|
||||||
{% include 'includes/scripts/hyperscript/swal.html' %}
|
{% include 'includes/scripts/hyperscript/swal.html' %}
|
||||||
<script defer src="https://cdn.jsdelivr.net/npm/@alpinejs/persist@3.x.x/dist/cdn.min.js"></script>
|
|
||||||
|
|
||||||
{% javascript_pack 'htmx' attrs="defer" %}
|
{% javascript_pack 'htmx' attrs="defer" %}
|
||||||
{% javascript_pack 'charts' %}
|
{% javascript_pack 'charts' %}
|
||||||
{#<script src="https://unpkg.com/htmx-ext-alpine-morph@2.0.0/alpine-morph.js"></script>#}
|
|
||||||
|
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
let tz = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
let tz = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
<script type="text/hyperscript">
|
||||||
|
behavior init_datepicker
|
||||||
|
init
|
||||||
|
set datepickers to <.airdatepickerinput/> in me
|
||||||
|
for x in datepickers
|
||||||
|
js(it)
|
||||||
|
DatePicker(it)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
set datepickers to <.airdatetimepickerinput/> in me
|
||||||
|
for x in datepickers
|
||||||
|
js(it)
|
||||||
|
DatePicker(it)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
set datepickers to <.airmonthyearpickerinput/> in me
|
||||||
|
for x in datepickers
|
||||||
|
MonthYearPicker(it)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
</script>
|
||||||
@@ -8,7 +8,9 @@
|
|||||||
{% block title %}{% translate 'Currency Converter' %}{% endblock %}
|
{% block title %}{% translate 'Currency Converter' %}{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="container px-md-3 py-3 column-gap-5" _="install init_tom_select">
|
<div class="container px-md-3 py-3 column-gap-5"
|
||||||
|
_="install init_tom_select
|
||||||
|
install init_datepicker">
|
||||||
<div class="tw-text-3xl fw-bold font-monospace tw-w-full mb-3">
|
<div class="tw-text-3xl fw-bold font-monospace tw-w-full mb-3">
|
||||||
<div>{% translate 'Currency Converter' %}</div>
|
<div>{% translate 'Currency Converter' %}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -124,7 +124,8 @@
|
|||||||
<div class="collapse" id="collapse-filter">
|
<div class="collapse" id="collapse-filter">
|
||||||
<div class="card card-body">
|
<div class="card card-body">
|
||||||
<form _="on change or submit or search trigger updated on window end
|
<form _="on change or submit or search trigger updated on window end
|
||||||
install init_tom_select"
|
install init_tom_select
|
||||||
|
install init_datepicker"
|
||||||
id="filter">
|
id="filter">
|
||||||
{% crispy filter.form %}
|
{% crispy filter.form %}
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -21,7 +21,8 @@
|
|||||||
<hr>
|
<hr>
|
||||||
<form hx-get="{% url 'transactions_all_list' %}" hx-trigger="change, submit, search"
|
<form hx-get="{% url 'transactions_all_list' %}" hx-trigger="change, submit, search"
|
||||||
hx-target="#transactions" id="filter" hx-indicator="#transactions"
|
hx-target="#transactions" id="filter" hx-indicator="#transactions"
|
||||||
_="install init_tom_select">
|
_="install init_tom_select
|
||||||
|
install init_datepicker">
|
||||||
{% crispy filter.form %}
|
{% crispy filter.form %}
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
7
frontend/package-lock.json
generated
7
frontend/package-lock.json
generated
@@ -17,6 +17,7 @@
|
|||||||
"@babel/preset-env": "^7.16.8",
|
"@babel/preset-env": "^7.16.8",
|
||||||
"@fortawesome/fontawesome-free": "^6.6.0",
|
"@fortawesome/fontawesome-free": "^6.6.0",
|
||||||
"@popperjs/core": "^2.11.8",
|
"@popperjs/core": "^2.11.8",
|
||||||
|
"air-datepicker": "^3.5.3",
|
||||||
"alpinejs": "^3.14.1",
|
"alpinejs": "^3.14.1",
|
||||||
"autoprefixer": "^10.4.14",
|
"autoprefixer": "^10.4.14",
|
||||||
"autosize": "^6.0.1",
|
"autosize": "^6.0.1",
|
||||||
@@ -2703,6 +2704,12 @@
|
|||||||
"acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
|
"acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/air-datepicker": {
|
||||||
|
"version": "3.5.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/air-datepicker/-/air-datepicker-3.5.3.tgz",
|
||||||
|
"integrity": "sha512-Elf9gLhv/jidN1+TfeRJYMQRUfYx5apXw2dY5DuAMPRnNtQ4Iw9fTTJK772osmXSUB9xQ2Y8Q1Pt6pgBOQLPQw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/ajv": {
|
"node_modules/ajv": {
|
||||||
"version": "6.12.6",
|
"version": "6.12.6",
|
||||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
|
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
|
||||||
|
|||||||
@@ -30,6 +30,7 @@
|
|||||||
"@babel/preset-env": "^7.16.8",
|
"@babel/preset-env": "^7.16.8",
|
||||||
"@fortawesome/fontawesome-free": "^6.6.0",
|
"@fortawesome/fontawesome-free": "^6.6.0",
|
||||||
"@popperjs/core": "^2.11.8",
|
"@popperjs/core": "^2.11.8",
|
||||||
|
"air-datepicker": "^3.5.3",
|
||||||
"alpinejs": "^3.14.1",
|
"alpinejs": "^3.14.1",
|
||||||
"autoprefixer": "^10.4.14",
|
"autoprefixer": "^10.4.14",
|
||||||
"autosize": "^6.0.1",
|
"autosize": "^6.0.1",
|
||||||
|
|||||||
148
frontend/src/application/datepicker.js
Normal file
148
frontend/src/application/datepicker.js
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
import AirDatepicker from 'air-datepicker';
|
||||||
|
import en from 'air-datepicker/locale/en';
|
||||||
|
import ptBr from 'air-datepicker/locale/pt-BR';
|
||||||
|
import {createPopper} from '@popperjs/core';
|
||||||
|
|
||||||
|
const locales = {
|
||||||
|
'pt': ptBr,
|
||||||
|
'en': en
|
||||||
|
};
|
||||||
|
|
||||||
|
function isMobileDevice() {
|
||||||
|
const mobileRegex = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i;
|
||||||
|
return mobileRegex.test(navigator.userAgent);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isTouchDevice() {
|
||||||
|
return ('ontouchstart' in window) || (navigator.maxTouchPoints > 0) || (navigator.msMaxTouchPoints > 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isMobile() {
|
||||||
|
return isMobileDevice() || isTouchDevice();
|
||||||
|
}
|
||||||
|
|
||||||
|
window.DatePicker = function createDynamicDatePicker(element) {
|
||||||
|
let isOnMobile = isMobile();
|
||||||
|
|
||||||
|
let baseOpts = {
|
||||||
|
isMobile: isOnMobile,
|
||||||
|
timepicker: element.dataset.timepicker === 'true',
|
||||||
|
autoClose: element.dataset.autoClose === 'true',
|
||||||
|
buttons: element.dataset.clearButton === 'true' ? ['clear', 'today'] : ['today'],
|
||||||
|
locale: locales[element.dataset.language],
|
||||||
|
onSelect: ({date, formattedDate, datepicker}) => {
|
||||||
|
const _event = new CustomEvent("change", {
|
||||||
|
bubbles: true,
|
||||||
|
});
|
||||||
|
datepicker.$el.dispatchEvent(_event);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const positionConfig = !isOnMobile ? {
|
||||||
|
position({$datepicker, $target, $pointer, done}) {
|
||||||
|
let popper = createPopper($target, $datepicker, {
|
||||||
|
placement: 'bottom',
|
||||||
|
modifiers: [
|
||||||
|
{
|
||||||
|
name: 'flip',
|
||||||
|
options: {
|
||||||
|
padding: {
|
||||||
|
top: 64
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'offset',
|
||||||
|
options: {
|
||||||
|
offset: [0, 20]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'arrow',
|
||||||
|
options: {
|
||||||
|
element: $pointer
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
return function completeHide() {
|
||||||
|
popper.destroy();
|
||||||
|
done();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} : {};
|
||||||
|
|
||||||
|
let opts = {...baseOpts, ...positionConfig};
|
||||||
|
|
||||||
|
if (element.dataset.value) {
|
||||||
|
opts["selectedDates"] = [element.dataset.value];
|
||||||
|
opts["startDate"] = [element.dataset.value];
|
||||||
|
}
|
||||||
|
|
||||||
|
return new AirDatepicker(element, opts);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
window.MonthYearPicker = function createDynamicDatePicker(element) {
|
||||||
|
let isOnMobile = isMobile();
|
||||||
|
|
||||||
|
let baseOpts = {
|
||||||
|
isMobile: isOnMobile,
|
||||||
|
view: 'months',
|
||||||
|
minView: 'months',
|
||||||
|
dateFormat: 'MMMM yyyy',
|
||||||
|
autoClose: element.dataset.autoClose === 'true',
|
||||||
|
buttons: element.dataset.clearButton === 'true' ? ['clear', 'today'] : ['today'],
|
||||||
|
locale: locales[element.dataset.language],
|
||||||
|
onSelect: ({date, formattedDate, datepicker}) => {
|
||||||
|
const _event = new CustomEvent("change", {
|
||||||
|
bubbles: true,
|
||||||
|
});
|
||||||
|
datepicker.$el.dispatchEvent(_event);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const positionConfig = !isOnMobile ? {
|
||||||
|
position({$datepicker, $target, $pointer, done}) {
|
||||||
|
let popper = createPopper($target, $datepicker, {
|
||||||
|
placement: 'bottom',
|
||||||
|
modifiers: [
|
||||||
|
{
|
||||||
|
name: 'flip',
|
||||||
|
options: {
|
||||||
|
padding: {
|
||||||
|
top: 64
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'offset',
|
||||||
|
options: {
|
||||||
|
offset: [0, 20]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'arrow',
|
||||||
|
options: {
|
||||||
|
element: $pointer
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
return function completeHide() {
|
||||||
|
popper.destroy();
|
||||||
|
done();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} : {};
|
||||||
|
|
||||||
|
let opts = {...baseOpts, ...positionConfig};
|
||||||
|
|
||||||
|
if (element.dataset.value) {
|
||||||
|
opts["selectedDates"] = [element.dataset.value];
|
||||||
|
opts["startDate"] = [element.dataset.value];
|
||||||
|
}
|
||||||
|
return new AirDatepicker(element, opts);
|
||||||
|
};
|
||||||
89
frontend/src/styles/_datepicker.scss
Normal file
89
frontend/src/styles/_datepicker.scss
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
@import 'air-datepicker/air-datepicker.css';
|
||||||
|
|
||||||
|
.air-datepicker-global-container {
|
||||||
|
z-index: 2000; // Allows the datepicker to be shown on top of offcanvas
|
||||||
|
}
|
||||||
|
|
||||||
|
.air-datepicker {
|
||||||
|
--adp-accent-color: #fbb700;
|
||||||
|
--adp-day-name-color: #fbb700;
|
||||||
|
--adp-background-color: #303030; /* $gray-800 */
|
||||||
|
--adp-color: #fff;
|
||||||
|
--adb-color-other-month: #888; /* $gray-600 */
|
||||||
|
--adp-cell-background-color-selected: #fbb700;
|
||||||
|
|
||||||
|
--adp-border-color-inline: #444;
|
||||||
|
|
||||||
|
--adp-background-color-selected-other-month-focused: #e6a600; /* Slightly darker than $yellow */
|
||||||
|
--adp-background-color-selected-other-month: #fbb700;
|
||||||
|
|
||||||
|
--adp-color-secondary: #adb5bd; /* $gray-500 */
|
||||||
|
--adp-background-color-hover: #444;
|
||||||
|
--adp-background-color-active: #3c3c3c;
|
||||||
|
--adp-cell-background-color-selected-hover: #e6a600;
|
||||||
|
--adp-color-other-month: #888; /* $gray-600 */
|
||||||
|
--adp-color-disabled: #444; /* $gray-700 */
|
||||||
|
--adp-color-disabled-in-range: #666; /* Between $gray-600 and $gray-700 */
|
||||||
|
--adp-color-other-month-hover: #ced4da; /* $gray-400 */
|
||||||
|
--adp-time-track-color: #444; /* $gray-700 */
|
||||||
|
--adp-time-track-color-hover: #888; /* $gray-600 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.air-datepicker-cell.-selected-,
|
||||||
|
.air-datepicker-cell.-selected-.-current-,
|
||||||
|
.-selected-.air-datepicker-cell.-year-.-other-decade-,
|
||||||
|
.-selected-.air-datepicker-cell.-day-.-other-month-{
|
||||||
|
color: #222; /* $gray-900 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Additional styles for better dark theme integration */
|
||||||
|
.air-datepicker {
|
||||||
|
border-color: #444; /* $gray-700 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.air-datepicker-body--day-names {
|
||||||
|
color: #fbb700; /* $yellow */
|
||||||
|
}
|
||||||
|
|
||||||
|
.air-datepicker-cell:hover {
|
||||||
|
background-color: #444; /* $gray-700 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.air-datepicker-cell.-current- {
|
||||||
|
color: #fbb700; /* $yellow */
|
||||||
|
}
|
||||||
|
|
||||||
|
.air-datepicker-cell.-range-from-,
|
||||||
|
.air-datepicker-cell.-range-to- {
|
||||||
|
border: 1px solid #fbb700; /* $yellow */
|
||||||
|
}
|
||||||
|
|
||||||
|
.air-datepicker-cell.-range-from-::before,
|
||||||
|
.air-datepicker-cell.-range-to-::before {
|
||||||
|
background-color: rgba(251, 183, 0, 0.1); /* $yellow with opacity */
|
||||||
|
}
|
||||||
|
|
||||||
|
.air-datepicker-cell.-in-range- {
|
||||||
|
background-color: rgba(251, 183, 0, 0.1); /* $yellow with opacity */
|
||||||
|
}
|
||||||
|
|
||||||
|
.air-datepicker-time--row input[type='range']::-webkit-slider-thumb {
|
||||||
|
background-color: #fbb700; /* $yellow */
|
||||||
|
}
|
||||||
|
|
||||||
|
.air-datepicker-time--row input[type='range']::-moz-range-thumb {
|
||||||
|
background-color: #fbb700; /* $yellow */
|
||||||
|
}
|
||||||
|
|
||||||
|
.air-datepicker-button,
|
||||||
|
.air-datepicker-button:hover {
|
||||||
|
color: #fbb700; /* $yellow */
|
||||||
|
}
|
||||||
|
|
||||||
|
.air-datepicker-button:hover {
|
||||||
|
background-color: #444; /* $gray-700 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.air-datepicker--pointer:after {
|
||||||
|
background: #303030
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
@import "font-awesome.scss";
|
@import "font-awesome.scss";
|
||||||
@import "tailwind.scss";
|
@import "tailwind.scss";
|
||||||
@import "bootstrap.scss";
|
@import "bootstrap.scss";
|
||||||
|
@import "datepicker.scss";
|
||||||
@import "tom-select.scss";
|
@import "tom-select.scss";
|
||||||
@import "animations.scss";
|
@import "animations.scss";
|
||||||
@import "scrollbar.scss";
|
@import "scrollbar.scss";
|
||||||
|
|||||||
Reference in New Issue
Block a user