commit
6003247880
@ -1,90 +1,101 @@
|
||||
from django import forms
|
||||
from .models import Expense, Category, Income, Tag, Account, FuelEntry
|
||||
from .models import Expense, Category, Income, Tag, Account, FuelEntry, Goal
|
||||
|
||||
|
||||
class ExpenseForm(forms.ModelForm):
|
||||
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
# Get user
|
||||
user = kwargs.pop('user', None)
|
||||
|
||||
user = kwargs.pop("user", None)
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
|
||||
if user:
|
||||
self.fields['category'].queryset = Category.objects.filter(owner=user)
|
||||
self.fields['tags'].queryset = Tag.objects.filter(owner=user)
|
||||
self.fields['account'].queryset = Account.objects.filter(owner=user, active=True)
|
||||
self.fields['date'].input_formats = ['%Y-%m-%d']
|
||||
|
||||
self.fields["category"].queryset = Category.objects.filter(owner=user)
|
||||
self.fields["tags"].queryset = Tag.objects.filter(owner=user)
|
||||
self.fields["account"].queryset = Account.objects.filter(
|
||||
owner=user, active=True
|
||||
)
|
||||
self.fields["date"].input_formats = ["%Y-%m-%d"]
|
||||
|
||||
class Meta:
|
||||
model = Expense
|
||||
fields = [
|
||||
'date',
|
||||
'amount',
|
||||
'description',
|
||||
'category',
|
||||
'account',
|
||||
'tags',
|
||||
"date",
|
||||
"amount",
|
||||
"description",
|
||||
"category",
|
||||
"account",
|
||||
"tags",
|
||||
]
|
||||
widgets = {
|
||||
'date': forms.DateInput(
|
||||
format='%Y-%m-%d',
|
||||
attrs={'type': 'date'}),
|
||||
'widget': forms.CheckboxSelectMultiple(),
|
||||
"date": forms.DateInput(format="%Y-%m-%d", attrs={"type": "date"}),
|
||||
"widget": forms.CheckboxSelectMultiple(),
|
||||
}
|
||||
|
||||
|
||||
class TagForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = Tag
|
||||
fields = ['name']
|
||||
|
||||
fields = ["name"]
|
||||
|
||||
|
||||
class AccountForm(forms.ModelForm):
|
||||
|
||||
|
||||
class Meta:
|
||||
model = Account
|
||||
fields = ['name', 'initial_balance', 'active']
|
||||
|
||||
fields = ["name", "initial_balance", "active"]
|
||||
|
||||
|
||||
class IncomeForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = Income
|
||||
fields = ['account', 'name', 'amount', 'date']
|
||||
widgets = {
|
||||
'date': forms.DateInput(
|
||||
format='%Y-%m-%d',
|
||||
attrs={'type': 'date'})
|
||||
}
|
||||
|
||||
fields = ["account", "name", "amount", "date"]
|
||||
widgets = {"date": forms.DateInput(format="%Y-%m-%d", attrs={"type": "date"})}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
user = kwargs.pop('user')
|
||||
user = kwargs.pop("user")
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields['account'].queryset = Account.objects.filter(
|
||||
self.fields["account"].queryset = Account.objects.filter(
|
||||
owner=user,
|
||||
active=True,
|
||||
)
|
||||
|
||||
|
||||
class FuelEntryForm(forms.Form):
|
||||
# Expense fields
|
||||
date = forms.DateField(widget=forms.DateInput(
|
||||
format='%Y-%m-%d',
|
||||
attrs={'type': 'date'}))
|
||||
date = forms.DateField(
|
||||
widget=forms.DateInput(format="%Y-%m-%d", attrs={"type": "date"})
|
||||
)
|
||||
amount = forms.DecimalField(max_digits=10, decimal_places=2)
|
||||
account = forms.ModelChoiceField(queryset=None)
|
||||
|
||||
|
||||
# Specifics fuel fields
|
||||
odometer = forms.DecimalField(label='Current kilometers')
|
||||
odometer = forms.DecimalField(label="Current kilometers")
|
||||
liters = forms.DecimalField(max_digits=8, decimal_places=2)
|
||||
|
||||
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
user = kwargs.pop('user')
|
||||
user = kwargs.pop("user")
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
if user:
|
||||
self.fields['account'].queryset = (
|
||||
user.accounts.filter(active=True)
|
||||
)
|
||||
self.fields["account"].queryset = user.accounts.filter(active=True)
|
||||
|
||||
|
||||
class CategoryForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = Category
|
||||
fields = ['name', 'parent']
|
||||
fields = ["name", "parent"]
|
||||
|
||||
class GoalForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = Goal
|
||||
fields = ['name', 'target_amount', 'category', "show_on_home"]
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
user = kwargs.pop('user')
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.fields['category'].queryset = (
|
||||
user.categories.all()
|
||||
)
|
||||
27
expenses_manager/expenses/migrations/0008_goal.py
Normal file
27
expenses_manager/expenses/migrations/0008_goal.py
Normal file
@ -0,0 +1,27 @@
|
||||
# Generated by Django 5.2.10 on 2026-05-05 09:08
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('expenses', '0007_alter_category_options_and_more'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Goal',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=100)),
|
||||
('target_amount', models.DecimalField(decimal_places=2, max_digits=12)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('category', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='expenses.category')),
|
||||
('owner', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
),
|
||||
]
|
||||
@ -0,0 +1,18 @@
|
||||
# Generated by Django 5.2.10 on 2026-05-05 13:26
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('expenses', '0008_goal'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='goal',
|
||||
name='show_on_home',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
]
|
||||
@ -7,141 +7,127 @@ from django.db.models.fields import related
|
||||
from django.db.models.functions import ExtractMonth
|
||||
from django.utils.text import slugify
|
||||
|
||||
|
||||
class Category(models.Model):
|
||||
name = models.CharField(max_length=100)
|
||||
slug = models.SlugField(blank=True)
|
||||
|
||||
|
||||
owner = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL,
|
||||
on_delete=models.CASCADE,
|
||||
related_name="categories",
|
||||
)
|
||||
|
||||
|
||||
parent = models.ForeignKey(
|
||||
'self',
|
||||
"self",
|
||||
on_delete=models.CASCADE,
|
||||
null=True,
|
||||
blank=True,
|
||||
related_name="children",
|
||||
)
|
||||
|
||||
|
||||
class Meta:
|
||||
unique_together = ("name", "parent", "owner", "slug")
|
||||
verbose_name_plural = "categories"
|
||||
ordering = ["name"]
|
||||
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if not self.slug:
|
||||
from django.utils.text import slugify
|
||||
|
||||
self.slug = slugify(self.name)
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
class Account(models.Model):
|
||||
|
||||
|
||||
class Account(models.Model):
|
||||
owner = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL,
|
||||
on_delete=models.CASCADE,
|
||||
related_name="accounts",
|
||||
)
|
||||
|
||||
name = models.CharField(max_length=100)
|
||||
initial_balance = models.DecimalField(
|
||||
max_digits=12,
|
||||
decimal_places=2,
|
||||
default=0
|
||||
)
|
||||
|
||||
name = models.CharField(max_length=100)
|
||||
initial_balance = models.DecimalField(max_digits=12, decimal_places=2, default=0)
|
||||
active = models.BooleanField(default=True)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
|
||||
class Meta:
|
||||
ordering = ['name']
|
||||
|
||||
ordering = ["name"]
|
||||
|
||||
def current_balance(self):
|
||||
expenses_total = (
|
||||
self.expenses.aggregate(total=Sum('amount'))['total']
|
||||
or Decimal('0')
|
||||
)
|
||||
income_total = (
|
||||
self.incomes.aggregate(total=Sum('amount'))['total']
|
||||
or Decimal('0')
|
||||
expenses_total = self.expenses.aggregate(total=Sum("amount"))[
|
||||
"total"
|
||||
] or Decimal("0")
|
||||
income_total = self.incomes.aggregate(total=Sum("amount"))["total"] or Decimal(
|
||||
"0"
|
||||
)
|
||||
return self.initial_balance + income_total - expenses_total
|
||||
|
||||
|
||||
def monthly_balance(self, year=None):
|
||||
year = year or date.today().year
|
||||
|
||||
|
||||
incomes = (
|
||||
self.incomes
|
||||
.filter(date__year=year)
|
||||
.annotate(month=ExtractMonth('date'))
|
||||
.values('month')
|
||||
.annotate(total=Sum('amount'))
|
||||
self.incomes.filter(date__year=year)
|
||||
.annotate(month=ExtractMonth("date"))
|
||||
.values("month")
|
||||
.annotate(total=Sum("amount"))
|
||||
)
|
||||
|
||||
|
||||
expenses = (
|
||||
self.expenses
|
||||
.filter(date__year=year)
|
||||
.annotate(month=ExtractMonth('date'))
|
||||
.values('month')
|
||||
.annotate(total=Sum('amount'))
|
||||
self.expenses.filter(date__year=year)
|
||||
.annotate(month=ExtractMonth("date"))
|
||||
.values("month")
|
||||
.annotate(total=Sum("amount"))
|
||||
)
|
||||
|
||||
income_map = {i['month']: i['total'] for i in incomes}
|
||||
expenses_map = {e['month']: e['total'] for e in expenses}
|
||||
|
||||
|
||||
income_map = {i["month"]: i["total"] for i in incomes}
|
||||
expenses_map = {e["month"]: e["total"] for e in expenses}
|
||||
|
||||
balance = self.initial_balance
|
||||
data = []
|
||||
|
||||
|
||||
for month in range(1, 13):
|
||||
balance += income_map.get(month, Decimal('0'))
|
||||
balance -= expenses_map.get(month, Decimal('0'))
|
||||
|
||||
data.append({
|
||||
'month': month,
|
||||
'balance': float(balance),
|
||||
})
|
||||
balance += income_map.get(month, Decimal("0"))
|
||||
balance -= expenses_map.get(month, Decimal("0"))
|
||||
|
||||
data.append(
|
||||
{
|
||||
"month": month,
|
||||
"balance": float(balance),
|
||||
}
|
||||
)
|
||||
return data
|
||||
|
||||
|
||||
def balance_until(self, date):
|
||||
incomes_total = (
|
||||
self.incomes
|
||||
.filter(date__lte=date)
|
||||
.aggregate(total=Sum('amount'))['total'] or Decimal('0')
|
||||
)
|
||||
|
||||
expenses_total = (
|
||||
self.expenses
|
||||
.filter(date__lte=date)
|
||||
.aggregate(total=Sum('amount'))['total'] or Decimal('0')
|
||||
)
|
||||
|
||||
incomes_total = self.incomes.filter(date__lte=date).aggregate(
|
||||
total=Sum("amount")
|
||||
)["total"] or Decimal("0")
|
||||
|
||||
expenses_total = self.expenses.filter(date__lte=date).aggregate(
|
||||
total=Sum("amount")
|
||||
)["total"] or Decimal("0")
|
||||
|
||||
return self.initial_balance + incomes_total - expenses_total
|
||||
|
||||
|
||||
def monthly_net(self, year=None):
|
||||
year = year or date.today().year
|
||||
data = []
|
||||
|
||||
|
||||
for month in range(1, 13):
|
||||
income = (
|
||||
self.incomes
|
||||
.filter(date__year=year, date__month=month)
|
||||
.aggregate(total=Sum('amount'))['total'] or Decimal('0')
|
||||
)
|
||||
expense = (
|
||||
self.expenses
|
||||
.filter(date__year=year, date__month=month)
|
||||
.aggregate(total=Sum('amount'))['total'] or Decimal('0')
|
||||
)
|
||||
|
||||
data.append({
|
||||
'month': month,
|
||||
'net': float(income - expense)
|
||||
})
|
||||
|
||||
income = self.incomes.filter(date__year=year, date__month=month).aggregate(
|
||||
total=Sum("amount")
|
||||
)["total"] or Decimal("0")
|
||||
expense = self.expenses.filter(
|
||||
date__year=year, date__month=month
|
||||
).aggregate(total=Sum("amount"))["total"] or Decimal("0")
|
||||
|
||||
data.append({"month": month, "net": float(income - expense)})
|
||||
|
||||
return data
|
||||
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
@ -153,108 +139,145 @@ class Tag(models.Model):
|
||||
on_delete=models.CASCADE,
|
||||
related_name="tags",
|
||||
)
|
||||
|
||||
|
||||
class Meta:
|
||||
unique_together = ("name", "owner")
|
||||
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
|
||||
class Expense(models.Model):
|
||||
date = models.DateField()
|
||||
amount = models.DecimalField(max_digits=10, decimal_places=2)
|
||||
description = models.TextField(blank=True)
|
||||
|
||||
|
||||
owner = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL,
|
||||
on_delete=models.CASCADE,
|
||||
related_name="expenses",
|
||||
)
|
||||
|
||||
|
||||
category = models.ForeignKey(
|
||||
Category,
|
||||
on_delete=models.PROTECT,
|
||||
related_name="expenses",
|
||||
)
|
||||
|
||||
|
||||
account = models.ForeignKey(
|
||||
Account,
|
||||
on_delete=models.PROTECT,
|
||||
related_name="expenses",
|
||||
)
|
||||
|
||||
|
||||
tags = models.ManyToManyField(
|
||||
Tag,
|
||||
blank=True,
|
||||
related_name="expenses",
|
||||
)
|
||||
|
||||
|
||||
created_at = models.DateField(auto_now_add=True)
|
||||
|
||||
|
||||
class Meta:
|
||||
ordering = ["-date"]
|
||||
|
||||
|
||||
def __str__(self):
|
||||
return "{} - {}".format(self.date, self.amount)
|
||||
|
||||
|
||||
|
||||
|
||||
class Income(models.Model):
|
||||
owner = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL,
|
||||
on_delete=models.CASCADE
|
||||
)
|
||||
owner = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
|
||||
account = models.ForeignKey(
|
||||
Account,
|
||||
on_delete=models.CASCADE,
|
||||
related_name='incomes'
|
||||
Account, on_delete=models.CASCADE, related_name="incomes"
|
||||
)
|
||||
|
||||
|
||||
name = models.CharField(max_length=150)
|
||||
amount = models.DecimalField(max_digits=12, decimal_places=2)
|
||||
date = models.DateField()
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
|
||||
class Meta:
|
||||
ordering = ['-date']
|
||||
|
||||
ordering = ["-date"]
|
||||
|
||||
def __str__(self):
|
||||
return f'{self.name} - {self.amount}'
|
||||
|
||||
return f"{self.name} - {self.amount}"
|
||||
|
||||
|
||||
class FuelEntry(models.Model):
|
||||
expense = models.OneToOneField(
|
||||
Expense,
|
||||
on_delete=models.CASCADE,
|
||||
related_name="fuel_data"
|
||||
Expense, on_delete=models.CASCADE, related_name="fuel_data"
|
||||
)
|
||||
|
||||
odometer = models.PositiveIntegerField() # kilometers
|
||||
|
||||
odometer = models.PositiveIntegerField() # kilometers
|
||||
liters = models.DecimalField(max_digits=8, decimal_places=2)
|
||||
|
||||
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
|
||||
class Meta:
|
||||
ordering = ['odometer']
|
||||
ordering = ["odometer"]
|
||||
|
||||
def km_since_previous(self):
|
||||
previous = FuelEntry.objects.filter(
|
||||
expense__owner=self.expense.owner,
|
||||
odometer__lt=self.odometer
|
||||
).order_by('-odometer').first()
|
||||
|
||||
previous = (
|
||||
FuelEntry.objects.filter(
|
||||
expense__owner=self.expense.owner, odometer__lt=self.odometer
|
||||
)
|
||||
.order_by("-odometer")
|
||||
.first()
|
||||
)
|
||||
|
||||
if previous:
|
||||
return self.odometer - previous.odometer
|
||||
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def price_per_liter(self):
|
||||
if self.liters:
|
||||
return self.expense.amount / self.liters
|
||||
return 0
|
||||
|
||||
|
||||
def consumption(self):
|
||||
km = self.km_since_previous()
|
||||
if km and km > 0:
|
||||
return (self.liters / km) * 100
|
||||
return None
|
||||
|
||||
|
||||
class Goal(models.Model):
|
||||
owner = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
|
||||
name = models.CharField(max_length=100)
|
||||
target_amount = models.DecimalField(max_digits=12, decimal_places=2)
|
||||
category = models.ForeignKey("Category", on_delete=models.CASCADE)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
show_on_home = models.BooleanField(default=False)
|
||||
|
||||
def progress(self):
|
||||
"""
|
||||
Calculate the accumulated spending for the goal category.
|
||||
This method returns the sum of all expenses of the owner in the goal's category.
|
||||
|
||||
Returns:
|
||||
Decimal: Total amount spent in the goal category, or Decimal('0') when there is no spending.
|
||||
|
||||
"""
|
||||
total = Expense.objects.filter(
|
||||
owner=self.owner,
|
||||
category=self.category,
|
||||
).aggregate(total=Sum("amount"))["total"]
|
||||
|
||||
return total or 0
|
||||
|
||||
def percentage(self):
|
||||
"""
|
||||
Calculate the completion percentage of the goal.
|
||||
This method returns how much of the target amount has been reached as a percentage.
|
||||
|
||||
Returns:
|
||||
Decimal: Percentage of the goal that has been reached, or Decimal('0') when the target amount is zero.
|
||||
|
||||
"""
|
||||
if self.target_amount == 0:
|
||||
return 0
|
||||
return (self.progress() / self.target_amount) * 100
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
@ -80,7 +80,7 @@ a.danger {
|
||||
|
||||
.dashboard-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 20px
|
||||
}
|
||||
|
||||
@ -89,15 +89,14 @@ a.danger {
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.card-chart {
|
||||
max-width: 300px;
|
||||
}
|
||||
|
||||
.card-chart canvas {
|
||||
width: 100% !important;
|
||||
height: 220px !important;
|
||||
height: 500px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.chart-box {
|
||||
@ -105,6 +104,19 @@ a.danger {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.canvas-wrapper {
|
||||
flex-grow: 1;
|
||||
min-height: 0;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.card-chart canvas {
|
||||
display: block;
|
||||
width: 100% !important;
|
||||
height: 100% !important;
|
||||
}
|
||||
|
||||
.kpi-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
||||
@ -310,4 +322,96 @@ tbody tr:hover {
|
||||
|
||||
.tag:hover {
|
||||
background: #dbe3ee;
|
||||
}
|
||||
|
||||
.dropdown {
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
color: #e5e7eb;
|
||||
}
|
||||
|
||||
.dropdown.active {
|
||||
font-weight: bold;
|
||||
border-bottom: 2px solid white;
|
||||
}
|
||||
|
||||
.dropdown-toggle {
|
||||
padding: 10px;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.dropdown-menu {
|
||||
display: none;
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
background: white;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 6px;
|
||||
min-width: 150px;
|
||||
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.dropdown-menu a {
|
||||
display: block;
|
||||
padding: 10px;
|
||||
text-decoration: none;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.dropdown-menu a:hover {
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.dropdown:hover .dropdown-menu {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.progress-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.progress-label {
|
||||
min-width: 50px;
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
width: 100%;
|
||||
background: #e0e0e0;
|
||||
border-radius: 6px;
|
||||
height: 14px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.progress-fill {
|
||||
height: 100%;
|
||||
transition: width 0.6s ease-in-out;
|
||||
}
|
||||
|
||||
.progress-fill.low {
|
||||
background: #e74c3c;
|
||||
}
|
||||
|
||||
.progress-fill.medium {
|
||||
background: #f1c40f;
|
||||
}
|
||||
|
||||
.progress-fill.high {
|
||||
background: #2ecc71;
|
||||
}
|
||||
|
||||
.goals-widget {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.goal-card {
|
||||
padding: 10px;
|
||||
border-radius: 8px;
|
||||
background: #f9f9f9;
|
||||
}
|
||||
@ -18,9 +18,18 @@
|
||||
<a href="{% url 'expense_list' %}" class="nav-item {% if active_menu == 'expenses' %}active{% endif %} ">Gastos</a>
|
||||
<a href="{% url 'account_list' %}" class="nav-item {% if active_menu == 'accounts' %}active{% endif %} ">Cuentas</a>
|
||||
<a href="{% url 'income_list' %}" class="nav-item {% if active_menu == 'incomes' %}active{% endif %} ">Ingresos</a>
|
||||
<a href="{% url 'tag_list' %}" class="nav-item {% if active_menu == 'tags' %}active{% endif %} ">Etiquetas</a>
|
||||
<!-- <a href="{% url 'tag_list' %}" class="nav-item {% if active_menu == 'tags' %}active{% endif %} ">Etiquetas</a> -->
|
||||
<a href="{% url 'fuel_list' %}" class="nav-item {% if active_menu == 'fuel' %}active{% endif %} ">Repostajes</a>
|
||||
<a href="{% url 'category_list' %}" class="nav-item {% if active_menu == 'categories' %}active{% endif %} ">Categorías</a>
|
||||
<!-- <a href="{% url 'category_list' %}" class="nav-item {% if active_menu == 'categories' %}active{% endif %} ">Categorías</a> -->
|
||||
<div class="nav-item dropdown {% if active_menu == 'settings' %}active{% endif %}">
|
||||
<span class="dropdown-toggle">Configuraciones ▼</span>
|
||||
|
||||
<div class="dropdown-menu">
|
||||
<a href="{% url 'category_list' %}">Categorías</a>
|
||||
<a href="{% url 'tag_list' %}">Etiquetas</a>
|
||||
<a href="{% url 'goal_list' %}">Objetivos</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<span class="spacer"></span>
|
||||
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
{% extends "expenses/base.html" %}
|
||||
|
||||
{% load l10n %}
|
||||
|
||||
{% block title %}Dashboard{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
@ -147,8 +149,10 @@
|
||||
<div class="card card-chart">
|
||||
<h3>{{ acc.name }}</h3>
|
||||
<p><strong>Saldo actual:</strong> {{ acc.current_balance|floatformat:2 }} €</p>
|
||||
|
||||
<canvas id="accountChart{{ acc.id }}"></canvas>
|
||||
|
||||
<div class="canvas-wrapper">
|
||||
<canvas id="accountChart{{ acc.id }}"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
@ -214,6 +218,39 @@
|
||||
</section>
|
||||
{% endif %}
|
||||
|
||||
<!-- ========================= -->
|
||||
<!-- Goals -->
|
||||
<!-- ========================= -->
|
||||
|
||||
<h3>Objetivos</h3>
|
||||
{% if goals %}
|
||||
<div class="goals-widget">
|
||||
{% for goal in goals %}
|
||||
<div class="goal-card">
|
||||
<strong>{{ goal.name }}</strong>
|
||||
|
||||
<div class="progress-bar">
|
||||
<div class="progress-fill
|
||||
{% if goal.percentage < 50 %} low
|
||||
{% elif goal.percentage < 80 %} medium
|
||||
{% else %} high
|
||||
{% endif %}"
|
||||
style="width: {{ goal.percentage|unlocalize }}%"></div>
|
||||
</div>
|
||||
|
||||
<span class="progress-label">
|
||||
{{ goal.progress|floatformat:1 }}€ / {{ goal.target_amount|floatformat:1 }}€ ({{ goal.percentage|floatformat:1 }}%)
|
||||
</span>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<p>No hay objetivos.</p>
|
||||
{% endif %}
|
||||
|
||||
<!-- ========================= -->
|
||||
<!-- Categories -->
|
||||
<!-- ========================= -->
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
{% extends "expenses/base.html" %}
|
||||
|
||||
{% load l10n %}
|
||||
|
||||
{% block title %}Home{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
@ -60,4 +62,30 @@
|
||||
|
||||
</section>
|
||||
|
||||
<h3>Objetivos</h3>
|
||||
{% if goals %}
|
||||
<div class="goals-widget">
|
||||
{% for goal in goals %}
|
||||
<div class="goal-card">
|
||||
<strong>{{ goal.name }}</strong>
|
||||
|
||||
<div class="progress-bar">
|
||||
<div class="progress-fill
|
||||
{% if goal.percentage < 50 %} low
|
||||
{% elif goal.percentage < 80 %} medium
|
||||
{% else %} high
|
||||
{% endif %}"
|
||||
style="width: {{ goal.percentage|unlocalize }}%"></div>
|
||||
</div>
|
||||
|
||||
<span class="progress-label">
|
||||
{{ goal.progress|floatformat:1 }}€ / {{ goal.target_amount|floatformat:1 }}€ ({{ goal.percentage|floatformat:1 }}%)
|
||||
</span>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<p>No tienes objetivos aún.</p>
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
||||
|
||||
20
expenses_manager/expenses/templates/goals/form.html
Normal file
20
expenses_manager/expenses/templates/goals/form.html
Normal file
@ -0,0 +1,20 @@
|
||||
{% extends "expenses/base.html" %}
|
||||
{% block title %}
|
||||
Nuevo objetivo
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h1>
|
||||
Nuevo objetivo
|
||||
</h1>
|
||||
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
{{ form.as_p }}
|
||||
<button type="submit">
|
||||
Guardar
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<a class="btn secondary" href="{% url 'goal_list' %}">Volver</a>
|
||||
{% endblock %}
|
||||
51
expenses_manager/expenses/templates/goals/list.html
Normal file
51
expenses_manager/expenses/templates/goals/list.html
Normal file
@ -0,0 +1,51 @@
|
||||
{% extends "expenses/base.html" %}
|
||||
|
||||
{% load l10n %}
|
||||
|
||||
{% block title %}
|
||||
Objetivos
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h2>Objetivos</h2>
|
||||
|
||||
<a class=btn href="{% url 'goal_create' %}">➕ Nuevo objetivo</a>
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<th>Nombre</th>
|
||||
<th>Progreso</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
|
||||
{% for goal in goals %}
|
||||
<tr>
|
||||
<td>{{ goal.name }}</td>
|
||||
<td>
|
||||
<div class="progress-container">
|
||||
<span class="progress-label">
|
||||
{{ goal.progress|floatformat:1 }}€ / {{ goal.target_amount|floatformat:1 }}€
|
||||
</span>
|
||||
<div class="progress-bar">
|
||||
<div class="progress-fill
|
||||
{% if goal.percentage < 50 %} low
|
||||
{% elif goal.percentage < 80 %} medium
|
||||
{% else %} high
|
||||
{% endif %}"
|
||||
style="width: {{ goal.percentage|unlocalize }}%"></div>
|
||||
</div>
|
||||
<span class="progress-label">
|
||||
{{ goal.percentage|floatformat:1}}%
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<a href="{% url 'goal_edit' goal.id %}">Editar</a>
|
||||
<a href="{% url 'goal_delete' goal.id %}" class="danger">Eliminar</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
|
||||
{% endblock %}
|
||||
@ -24,4 +24,9 @@ urlpatterns = [
|
||||
path('fuel/create/', views.fuel_create, name='fuel_create'),
|
||||
path('fuel/<int:pk>/edit/', views.fuel_edit, name='fuel_edit'),
|
||||
path('categories/', views.category_list, name='category_list'),
|
||||
path('settings/', views.settings_index, name='settings_index'),
|
||||
path('goals/', views.goal_list, name='goal_list'),
|
||||
path('goals/new/', views.goal_create, name='goal_create'),
|
||||
path('goals/<int:pk>/edit/', views.goal_edit, name='goal_edit'),
|
||||
path('goals/<int:pk>/delete/', views.goal_delete, name='goal_delete'),
|
||||
]
|
||||
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user