├── users ├── __init__.py ├── migrations │ ├── __init__.py │ ├── __pycache__ │ │ ├── __init__.cpython-311.pyc │ │ ├── __init__.cpython-312.pyc │ │ ├── 0001_initial.cpython-311.pyc │ │ ├── 0001_initial.cpython-312.pyc │ │ ├── 0005_expense.cpython-311.pyc │ │ ├── 0005_expense.cpython-312.pyc │ │ ├── 0006_delete_expense.cpython-311.pyc │ │ ├── 0006_delete_expense.cpython-312.pyc │ │ ├── 0003_alter_profile_bio.cpython-311.pyc │ │ ├── 0003_alter_profile_bio.cpython-312.pyc │ │ ├── 0002_profile_profile_picture.cpython-311.pyc │ │ ├── 0002_profile_profile_picture.cpython-312.pyc │ │ ├── 0004_alter_profile_profile_picture.cpython-311.pyc │ │ └── 0004_alter_profile_profile_picture.cpython-312.pyc │ ├── 0006_delete_expense.py │ ├── 0003_alter_profile_bio.py │ ├── 0002_profile_profile_picture.py │ ├── 0004_alter_profile_profile_picture.py │ ├── 0005_expense.py │ └── 0001_initial.py ├── tests.py ├── admin.py ├── __pycache__ │ ├── admin.cpython-311.pyc │ ├── admin.cpython-312.pyc │ ├── apps.cpython-311.pyc │ ├── apps.cpython-312.pyc │ ├── forms.cpython-311.pyc │ ├── forms.cpython-312.pyc │ ├── models.cpython-311.pyc │ ├── models.cpython-312.pyc │ ├── urls.cpython-311.pyc │ ├── urls.cpython-312.pyc │ ├── views.cpython-311.pyc │ ├── views.cpython-312.pyc │ ├── __init__.cpython-311.pyc │ └── __init__.cpython-312.pyc ├── apps.py ├── urls.py ├── models.py ├── views.py └── forms.py ├── expenses ├── __init__.py ├── migrations │ ├── __init__.py │ ├── __pycache__ │ │ ├── __init__.cpython-311.pyc │ │ ├── __init__.cpython-312.pyc │ │ ├── 0001_initial.cpython-311.pyc │ │ └── 0001_initial.cpython-312.pyc │ └── 0001_initial.py ├── admin.py ├── tests.py ├── __pycache__ │ ├── admin.cpython-311.pyc │ ├── admin.cpython-312.pyc │ ├── apps.cpython-311.pyc │ ├── apps.cpython-312.pyc │ ├── forms.cpython-311.pyc │ ├── forms.cpython-312.pyc │ ├── urls.cpython-311.pyc │ ├── urls.cpython-312.pyc │ ├── views.cpython-311.pyc │ ├── views.cpython-312.pyc │ ├── __init__.cpython-311.pyc │ ├── __init__.cpython-312.pyc │ ├── models.cpython-311.pyc │ └── models.cpython-312.pyc ├── apps.py ├── models.py ├── urls.py ├── forms.py └── views.py ├── payment ├── __init__.py ├── migrations │ ├── __init__.py │ ├── __pycache__ │ │ ├── __init__.cpython-311.pyc │ │ ├── __init__.cpython-312.pyc │ │ ├── 0001_initial.cpython-311.pyc │ │ └── 0001_initial.cpython-312.pyc │ └── 0001_initial.py ├── tests.py ├── admin.py ├── __pycache__ │ ├── apps.cpython-311.pyc │ ├── apps.cpython-312.pyc │ ├── urls.cpython-311.pyc │ ├── urls.cpython-312.pyc │ ├── admin.cpython-311.pyc │ ├── admin.cpython-312.pyc │ ├── models.cpython-311.pyc │ ├── models.cpython-312.pyc │ ├── views.cpython-311.pyc │ ├── views.cpython-312.pyc │ ├── __init__.cpython-311.pyc │ └── __init__.cpython-312.pyc ├── apps.py ├── urls.py ├── models.py └── views.py ├── visuals ├── __init__.py ├── migrations │ ├── __init__.py │ ├── __pycache__ │ │ ├── __init__.cpython-312.pyc │ │ └── 0001_initial.cpython-312.pyc │ └── 0001_initial.py ├── tests.py ├── admin.py ├── __pycache__ │ ├── apps.cpython-312.pyc │ ├── urls.cpython-312.pyc │ ├── admin.cpython-312.pyc │ ├── models.cpython-312.pyc │ ├── views.cpython-312.pyc │ └── __init__.cpython-312.pyc ├── apps.py ├── urls.py ├── models.py └── views.py ├── ExpenseTracker ├── __init__.py ├── __pycache__ │ ├── urls.cpython-311.pyc │ ├── urls.cpython-312.pyc │ ├── wsgi.cpython-311.pyc │ ├── wsgi.cpython-312.pyc │ ├── __init__.cpython-311.pyc │ ├── __init__.cpython-312.pyc │ ├── settings.cpython-311.pyc │ └── settings.cpython-312.pyc ├── asgi.py ├── wsgi.py ├── urls.py └── settings.py ├── .gitignore ├── requirements.txt ├── static ├── img │ ├── favicon.ico │ └── profile-background.png └── css │ └── style.css ├── media ├── default-profile.png └── profile_pics │ ├── Ghost.jpeg │ ├── python.jpeg │ ├── Antitled.jpeg │ ├── basic-package.jpeg │ ├── leonardoanatomist2.webp │ ├── pixy-result-2032347.jpg │ ├── iphone-series6-watch.jpeg │ └── photo_2024-01-30_11-48-54.jpg ├── templates ├── expenses │ ├── object_confirm_delete.html │ ├── object_detail.html │ ├── object_list.html │ ├── object_form.html │ └── dashboard.html ├── users │ ├── login.html │ ├── register.html │ └── profile.html ├── mpesa │ └── mpesa_sms.html ├── visuals │ ├── expenses_by_category.html │ └── budget_vs_expenses.html └── base.html ├── manage.py ├── LICENSE └── README.md /users/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /expenses/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /payment/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /visuals/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ExpenseTracker/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /users/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /expenses/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /payment/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /visuals/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | db.sqlite3 2 | media/__pycache__/ 3 | venv/ 4 | .env -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | django 2 | djangorestframework 3 | python-dateutil -------------------------------------------------------------------------------- /payment/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /users/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /visuals/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /expenses/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | -------------------------------------------------------------------------------- /expenses/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /payment/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | -------------------------------------------------------------------------------- /users/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | -------------------------------------------------------------------------------- /visuals/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | -------------------------------------------------------------------------------- /static/img/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phantom-kali/ExpenseTracker/HEAD/static/img/favicon.ico -------------------------------------------------------------------------------- /media/default-profile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phantom-kali/ExpenseTracker/HEAD/media/default-profile.png -------------------------------------------------------------------------------- /media/profile_pics/Ghost.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phantom-kali/ExpenseTracker/HEAD/media/profile_pics/Ghost.jpeg -------------------------------------------------------------------------------- /media/profile_pics/python.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phantom-kali/ExpenseTracker/HEAD/media/profile_pics/python.jpeg -------------------------------------------------------------------------------- /media/profile_pics/Antitled.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phantom-kali/ExpenseTracker/HEAD/media/profile_pics/Antitled.jpeg -------------------------------------------------------------------------------- /static/img/profile-background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phantom-kali/ExpenseTracker/HEAD/static/img/profile-background.png -------------------------------------------------------------------------------- /media/profile_pics/basic-package.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phantom-kali/ExpenseTracker/HEAD/media/profile_pics/basic-package.jpeg -------------------------------------------------------------------------------- /payment/__pycache__/apps.cpython-311.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phantom-kali/ExpenseTracker/HEAD/payment/__pycache__/apps.cpython-311.pyc -------------------------------------------------------------------------------- /payment/__pycache__/apps.cpython-312.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phantom-kali/ExpenseTracker/HEAD/payment/__pycache__/apps.cpython-312.pyc -------------------------------------------------------------------------------- /payment/__pycache__/urls.cpython-311.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phantom-kali/ExpenseTracker/HEAD/payment/__pycache__/urls.cpython-311.pyc -------------------------------------------------------------------------------- /payment/__pycache__/urls.cpython-312.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phantom-kali/ExpenseTracker/HEAD/payment/__pycache__/urls.cpython-312.pyc -------------------------------------------------------------------------------- /users/__pycache__/admin.cpython-311.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phantom-kali/ExpenseTracker/HEAD/users/__pycache__/admin.cpython-311.pyc -------------------------------------------------------------------------------- /users/__pycache__/admin.cpython-312.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phantom-kali/ExpenseTracker/HEAD/users/__pycache__/admin.cpython-312.pyc -------------------------------------------------------------------------------- /users/__pycache__/apps.cpython-311.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phantom-kali/ExpenseTracker/HEAD/users/__pycache__/apps.cpython-311.pyc -------------------------------------------------------------------------------- /users/__pycache__/apps.cpython-312.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phantom-kali/ExpenseTracker/HEAD/users/__pycache__/apps.cpython-312.pyc -------------------------------------------------------------------------------- /users/__pycache__/forms.cpython-311.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phantom-kali/ExpenseTracker/HEAD/users/__pycache__/forms.cpython-311.pyc -------------------------------------------------------------------------------- /users/__pycache__/forms.cpython-312.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phantom-kali/ExpenseTracker/HEAD/users/__pycache__/forms.cpython-312.pyc -------------------------------------------------------------------------------- /users/__pycache__/models.cpython-311.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phantom-kali/ExpenseTracker/HEAD/users/__pycache__/models.cpython-311.pyc -------------------------------------------------------------------------------- /users/__pycache__/models.cpython-312.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phantom-kali/ExpenseTracker/HEAD/users/__pycache__/models.cpython-312.pyc -------------------------------------------------------------------------------- /users/__pycache__/urls.cpython-311.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phantom-kali/ExpenseTracker/HEAD/users/__pycache__/urls.cpython-311.pyc -------------------------------------------------------------------------------- /users/__pycache__/urls.cpython-312.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phantom-kali/ExpenseTracker/HEAD/users/__pycache__/urls.cpython-312.pyc -------------------------------------------------------------------------------- /users/__pycache__/views.cpython-311.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phantom-kali/ExpenseTracker/HEAD/users/__pycache__/views.cpython-311.pyc -------------------------------------------------------------------------------- /users/__pycache__/views.cpython-312.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phantom-kali/ExpenseTracker/HEAD/users/__pycache__/views.cpython-312.pyc -------------------------------------------------------------------------------- /visuals/__pycache__/apps.cpython-312.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phantom-kali/ExpenseTracker/HEAD/visuals/__pycache__/apps.cpython-312.pyc -------------------------------------------------------------------------------- /visuals/__pycache__/urls.cpython-312.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phantom-kali/ExpenseTracker/HEAD/visuals/__pycache__/urls.cpython-312.pyc -------------------------------------------------------------------------------- /expenses/__pycache__/admin.cpython-311.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phantom-kali/ExpenseTracker/HEAD/expenses/__pycache__/admin.cpython-311.pyc -------------------------------------------------------------------------------- /expenses/__pycache__/admin.cpython-312.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phantom-kali/ExpenseTracker/HEAD/expenses/__pycache__/admin.cpython-312.pyc -------------------------------------------------------------------------------- /expenses/__pycache__/apps.cpython-311.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phantom-kali/ExpenseTracker/HEAD/expenses/__pycache__/apps.cpython-311.pyc -------------------------------------------------------------------------------- /expenses/__pycache__/apps.cpython-312.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phantom-kali/ExpenseTracker/HEAD/expenses/__pycache__/apps.cpython-312.pyc -------------------------------------------------------------------------------- /expenses/__pycache__/forms.cpython-311.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phantom-kali/ExpenseTracker/HEAD/expenses/__pycache__/forms.cpython-311.pyc -------------------------------------------------------------------------------- /expenses/__pycache__/forms.cpython-312.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phantom-kali/ExpenseTracker/HEAD/expenses/__pycache__/forms.cpython-312.pyc -------------------------------------------------------------------------------- /expenses/__pycache__/urls.cpython-311.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phantom-kali/ExpenseTracker/HEAD/expenses/__pycache__/urls.cpython-311.pyc -------------------------------------------------------------------------------- /expenses/__pycache__/urls.cpython-312.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phantom-kali/ExpenseTracker/HEAD/expenses/__pycache__/urls.cpython-312.pyc -------------------------------------------------------------------------------- /expenses/__pycache__/views.cpython-311.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phantom-kali/ExpenseTracker/HEAD/expenses/__pycache__/views.cpython-311.pyc -------------------------------------------------------------------------------- /expenses/__pycache__/views.cpython-312.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phantom-kali/ExpenseTracker/HEAD/expenses/__pycache__/views.cpython-312.pyc -------------------------------------------------------------------------------- /media/profile_pics/leonardoanatomist2.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phantom-kali/ExpenseTracker/HEAD/media/profile_pics/leonardoanatomist2.webp -------------------------------------------------------------------------------- /media/profile_pics/pixy-result-2032347.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phantom-kali/ExpenseTracker/HEAD/media/profile_pics/pixy-result-2032347.jpg -------------------------------------------------------------------------------- /payment/__pycache__/admin.cpython-311.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phantom-kali/ExpenseTracker/HEAD/payment/__pycache__/admin.cpython-311.pyc -------------------------------------------------------------------------------- /payment/__pycache__/admin.cpython-312.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phantom-kali/ExpenseTracker/HEAD/payment/__pycache__/admin.cpython-312.pyc -------------------------------------------------------------------------------- /payment/__pycache__/models.cpython-311.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phantom-kali/ExpenseTracker/HEAD/payment/__pycache__/models.cpython-311.pyc -------------------------------------------------------------------------------- /payment/__pycache__/models.cpython-312.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phantom-kali/ExpenseTracker/HEAD/payment/__pycache__/models.cpython-312.pyc -------------------------------------------------------------------------------- /payment/__pycache__/views.cpython-311.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phantom-kali/ExpenseTracker/HEAD/payment/__pycache__/views.cpython-311.pyc -------------------------------------------------------------------------------- /payment/__pycache__/views.cpython-312.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phantom-kali/ExpenseTracker/HEAD/payment/__pycache__/views.cpython-312.pyc -------------------------------------------------------------------------------- /users/__pycache__/__init__.cpython-311.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phantom-kali/ExpenseTracker/HEAD/users/__pycache__/__init__.cpython-311.pyc -------------------------------------------------------------------------------- /users/__pycache__/__init__.cpython-312.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phantom-kali/ExpenseTracker/HEAD/users/__pycache__/__init__.cpython-312.pyc -------------------------------------------------------------------------------- /visuals/__pycache__/admin.cpython-312.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phantom-kali/ExpenseTracker/HEAD/visuals/__pycache__/admin.cpython-312.pyc -------------------------------------------------------------------------------- /visuals/__pycache__/models.cpython-312.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phantom-kali/ExpenseTracker/HEAD/visuals/__pycache__/models.cpython-312.pyc -------------------------------------------------------------------------------- /visuals/__pycache__/views.cpython-312.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phantom-kali/ExpenseTracker/HEAD/visuals/__pycache__/views.cpython-312.pyc -------------------------------------------------------------------------------- /expenses/__pycache__/__init__.cpython-311.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phantom-kali/ExpenseTracker/HEAD/expenses/__pycache__/__init__.cpython-311.pyc -------------------------------------------------------------------------------- /expenses/__pycache__/__init__.cpython-312.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phantom-kali/ExpenseTracker/HEAD/expenses/__pycache__/__init__.cpython-312.pyc -------------------------------------------------------------------------------- /expenses/__pycache__/models.cpython-311.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phantom-kali/ExpenseTracker/HEAD/expenses/__pycache__/models.cpython-311.pyc -------------------------------------------------------------------------------- /expenses/__pycache__/models.cpython-312.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phantom-kali/ExpenseTracker/HEAD/expenses/__pycache__/models.cpython-312.pyc -------------------------------------------------------------------------------- /media/profile_pics/iphone-series6-watch.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phantom-kali/ExpenseTracker/HEAD/media/profile_pics/iphone-series6-watch.jpeg -------------------------------------------------------------------------------- /payment/__pycache__/__init__.cpython-311.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phantom-kali/ExpenseTracker/HEAD/payment/__pycache__/__init__.cpython-311.pyc -------------------------------------------------------------------------------- /payment/__pycache__/__init__.cpython-312.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phantom-kali/ExpenseTracker/HEAD/payment/__pycache__/__init__.cpython-312.pyc -------------------------------------------------------------------------------- /visuals/__pycache__/__init__.cpython-312.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phantom-kali/ExpenseTracker/HEAD/visuals/__pycache__/__init__.cpython-312.pyc -------------------------------------------------------------------------------- /ExpenseTracker/__pycache__/urls.cpython-311.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phantom-kali/ExpenseTracker/HEAD/ExpenseTracker/__pycache__/urls.cpython-311.pyc -------------------------------------------------------------------------------- /ExpenseTracker/__pycache__/urls.cpython-312.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phantom-kali/ExpenseTracker/HEAD/ExpenseTracker/__pycache__/urls.cpython-312.pyc -------------------------------------------------------------------------------- /ExpenseTracker/__pycache__/wsgi.cpython-311.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phantom-kali/ExpenseTracker/HEAD/ExpenseTracker/__pycache__/wsgi.cpython-311.pyc -------------------------------------------------------------------------------- /ExpenseTracker/__pycache__/wsgi.cpython-312.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phantom-kali/ExpenseTracker/HEAD/ExpenseTracker/__pycache__/wsgi.cpython-312.pyc -------------------------------------------------------------------------------- /media/profile_pics/photo_2024-01-30_11-48-54.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phantom-kali/ExpenseTracker/HEAD/media/profile_pics/photo_2024-01-30_11-48-54.jpg -------------------------------------------------------------------------------- /ExpenseTracker/__pycache__/__init__.cpython-311.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phantom-kali/ExpenseTracker/HEAD/ExpenseTracker/__pycache__/__init__.cpython-311.pyc -------------------------------------------------------------------------------- /ExpenseTracker/__pycache__/__init__.cpython-312.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phantom-kali/ExpenseTracker/HEAD/ExpenseTracker/__pycache__/__init__.cpython-312.pyc -------------------------------------------------------------------------------- /ExpenseTracker/__pycache__/settings.cpython-311.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phantom-kali/ExpenseTracker/HEAD/ExpenseTracker/__pycache__/settings.cpython-311.pyc -------------------------------------------------------------------------------- /ExpenseTracker/__pycache__/settings.cpython-312.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phantom-kali/ExpenseTracker/HEAD/ExpenseTracker/__pycache__/settings.cpython-312.pyc -------------------------------------------------------------------------------- /payment/migrations/__pycache__/__init__.cpython-311.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phantom-kali/ExpenseTracker/HEAD/payment/migrations/__pycache__/__init__.cpython-311.pyc -------------------------------------------------------------------------------- /payment/migrations/__pycache__/__init__.cpython-312.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phantom-kali/ExpenseTracker/HEAD/payment/migrations/__pycache__/__init__.cpython-312.pyc -------------------------------------------------------------------------------- /users/migrations/__pycache__/__init__.cpython-311.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phantom-kali/ExpenseTracker/HEAD/users/migrations/__pycache__/__init__.cpython-311.pyc -------------------------------------------------------------------------------- /users/migrations/__pycache__/__init__.cpython-312.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phantom-kali/ExpenseTracker/HEAD/users/migrations/__pycache__/__init__.cpython-312.pyc -------------------------------------------------------------------------------- /visuals/migrations/__pycache__/__init__.cpython-312.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phantom-kali/ExpenseTracker/HEAD/visuals/migrations/__pycache__/__init__.cpython-312.pyc -------------------------------------------------------------------------------- /expenses/migrations/__pycache__/__init__.cpython-311.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phantom-kali/ExpenseTracker/HEAD/expenses/migrations/__pycache__/__init__.cpython-311.pyc -------------------------------------------------------------------------------- /expenses/migrations/__pycache__/__init__.cpython-312.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phantom-kali/ExpenseTracker/HEAD/expenses/migrations/__pycache__/__init__.cpython-312.pyc -------------------------------------------------------------------------------- /users/migrations/__pycache__/0001_initial.cpython-311.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phantom-kali/ExpenseTracker/HEAD/users/migrations/__pycache__/0001_initial.cpython-311.pyc -------------------------------------------------------------------------------- /users/migrations/__pycache__/0001_initial.cpython-312.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phantom-kali/ExpenseTracker/HEAD/users/migrations/__pycache__/0001_initial.cpython-312.pyc -------------------------------------------------------------------------------- /users/migrations/__pycache__/0005_expense.cpython-311.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phantom-kali/ExpenseTracker/HEAD/users/migrations/__pycache__/0005_expense.cpython-311.pyc -------------------------------------------------------------------------------- /users/migrations/__pycache__/0005_expense.cpython-312.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phantom-kali/ExpenseTracker/HEAD/users/migrations/__pycache__/0005_expense.cpython-312.pyc -------------------------------------------------------------------------------- /expenses/migrations/__pycache__/0001_initial.cpython-311.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phantom-kali/ExpenseTracker/HEAD/expenses/migrations/__pycache__/0001_initial.cpython-311.pyc -------------------------------------------------------------------------------- /expenses/migrations/__pycache__/0001_initial.cpython-312.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phantom-kali/ExpenseTracker/HEAD/expenses/migrations/__pycache__/0001_initial.cpython-312.pyc -------------------------------------------------------------------------------- /payment/migrations/__pycache__/0001_initial.cpython-311.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phantom-kali/ExpenseTracker/HEAD/payment/migrations/__pycache__/0001_initial.cpython-311.pyc -------------------------------------------------------------------------------- /payment/migrations/__pycache__/0001_initial.cpython-312.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phantom-kali/ExpenseTracker/HEAD/payment/migrations/__pycache__/0001_initial.cpython-312.pyc -------------------------------------------------------------------------------- /users/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class UsersConfig(AppConfig): 5 | default_auto_field = "django.db.models.BigAutoField" 6 | name = "users" 7 | -------------------------------------------------------------------------------- /visuals/migrations/__pycache__/0001_initial.cpython-312.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phantom-kali/ExpenseTracker/HEAD/visuals/migrations/__pycache__/0001_initial.cpython-312.pyc -------------------------------------------------------------------------------- /expenses/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class ExpensesConfig(AppConfig): 5 | default_auto_field = "django.db.models.BigAutoField" 6 | name = "expenses" 7 | -------------------------------------------------------------------------------- /payment/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class PaymentConfig(AppConfig): 5 | default_auto_field = 'django.db.models.BigAutoField' 6 | name = 'payment' 7 | -------------------------------------------------------------------------------- /visuals/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class VisualsConfig(AppConfig): 5 | default_auto_field = 'django.db.models.BigAutoField' 6 | name = 'visuals' 7 | -------------------------------------------------------------------------------- /users/migrations/__pycache__/0006_delete_expense.cpython-311.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phantom-kali/ExpenseTracker/HEAD/users/migrations/__pycache__/0006_delete_expense.cpython-311.pyc -------------------------------------------------------------------------------- /users/migrations/__pycache__/0006_delete_expense.cpython-312.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phantom-kali/ExpenseTracker/HEAD/users/migrations/__pycache__/0006_delete_expense.cpython-312.pyc -------------------------------------------------------------------------------- /users/migrations/__pycache__/0003_alter_profile_bio.cpython-311.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phantom-kali/ExpenseTracker/HEAD/users/migrations/__pycache__/0003_alter_profile_bio.cpython-311.pyc -------------------------------------------------------------------------------- /users/migrations/__pycache__/0003_alter_profile_bio.cpython-312.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phantom-kali/ExpenseTracker/HEAD/users/migrations/__pycache__/0003_alter_profile_bio.cpython-312.pyc -------------------------------------------------------------------------------- /users/migrations/__pycache__/0002_profile_profile_picture.cpython-311.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phantom-kali/ExpenseTracker/HEAD/users/migrations/__pycache__/0002_profile_profile_picture.cpython-311.pyc -------------------------------------------------------------------------------- /users/migrations/__pycache__/0002_profile_profile_picture.cpython-312.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phantom-kali/ExpenseTracker/HEAD/users/migrations/__pycache__/0002_profile_profile_picture.cpython-312.pyc -------------------------------------------------------------------------------- /users/migrations/__pycache__/0004_alter_profile_profile_picture.cpython-311.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phantom-kali/ExpenseTracker/HEAD/users/migrations/__pycache__/0004_alter_profile_profile_picture.cpython-311.pyc -------------------------------------------------------------------------------- /users/migrations/__pycache__/0004_alter_profile_profile_picture.cpython-312.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phantom-kali/ExpenseTracker/HEAD/users/migrations/__pycache__/0004_alter_profile_profile_picture.cpython-312.pyc -------------------------------------------------------------------------------- /payment/urls.py: -------------------------------------------------------------------------------- 1 | 2 | from django.urls import path 3 | from . import views 4 | 5 | urlpatterns = [ 6 | path('api/process-mpesa-sms/', views.process_mpesa_sms, name='process_mpesa_sms'), 7 | path('submit_mpesa_sms/', views.submit_mpesa_sms, name='submit_mpesa_sms') 8 | ] -------------------------------------------------------------------------------- /users/migrations/0006_delete_expense.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.0.2 on 2024-09-22 21:04 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("users", "0005_expense"), 10 | ] 11 | 12 | operations = [ 13 | migrations.DeleteModel( 14 | name="Expense", 15 | ), 16 | ] 17 | -------------------------------------------------------------------------------- /visuals/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | from . import views 3 | 4 | app_name = 'visuals' 5 | 6 | urlpatterns = [ 7 | path('statistics/', views.statistics_view, name='statistics'), 8 | path('budget-vs-expenses/', views.budget_vs_expenses_view, name='budget_vs_expenses'), 9 | path('expenses-by-category/', views.expenses_by_category_view, name='expenses_by_category'), 10 | 11 | ] 12 | -------------------------------------------------------------------------------- /users/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | from . import views 3 | from django.contrib.auth import views as auth_views 4 | 5 | urlpatterns = [ 6 | path('register/', views.register, name='register'), 7 | path('profile/', views.profile, name='profile'), 8 | path('login/', auth_views.LoginView.as_view(template_name='users/login.html'), name='login'), 9 | path('logout/', auth_views.LogoutView.as_view(next_page='login'), name='logout'), 10 | ] -------------------------------------------------------------------------------- /ExpenseTracker/asgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | ASGI config for ExpenseTracker project. 3 | 4 | It exposes the ASGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/5.0/howto/deployment/asgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.asgi import get_asgi_application 13 | 14 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "ExpenseTracker.settings") 15 | 16 | application = get_asgi_application() 17 | -------------------------------------------------------------------------------- /ExpenseTracker/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for ExpenseTracker project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/5.0/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "ExpenseTracker.settings") 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /users/migrations/0003_alter_profile_bio.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.0.2 on 2024-09-21 16:03 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("users", "0002_profile_profile_picture"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name="profile", 15 | name="bio", 16 | field=models.CharField(blank=True, max_length=500), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /users/migrations/0002_profile_profile_picture.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.0.2 on 2024-09-21 12:54 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("users", "0001_initial"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name="profile", 15 | name="profile_picture", 16 | field=models.ImageField(default="default.png", upload_to="profile_pics"), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /users/migrations/0004_alter_profile_profile_picture.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.0.6 on 2024-09-21 19:31 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('users', '0003_alter_profile_bio'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='profile', 15 | name='profile_picture', 16 | field=models.ImageField(default='default-profile.png', upload_to='profile_pics'), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /payment/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | 4 | class MpesaTransaction(models.Model): 5 | transaction_id = models.CharField(max_length=20, unique=True) 6 | amount = models.DecimalField(max_digits=10, decimal_places=2) 7 | recipient = models.CharField(max_length=100) 8 | phone = models.CharField(max_length=15) 9 | transaction_date = models.DateTimeField() 10 | created_at = models.DateTimeField(auto_now_add=True) 11 | 12 | def __str__(self): 13 | return f"{self.transaction_id} - Ksh{self.amount} to {self.recipient}" 14 | -------------------------------------------------------------------------------- /templates/expenses/object_confirm_delete.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% block content %} 3 |
4 |
5 |
6 |

Confirm Delete

7 |

Are you sure you want to delete this {{ model_name }}?

8 |
9 | {% csrf_token %} 10 | 11 | Cancel 12 |
13 |
14 |
15 |
16 | {% endblock %} -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Django's command-line utility for administrative tasks.""" 3 | import os 4 | import sys 5 | 6 | 7 | def main(): 8 | """Run administrative tasks.""" 9 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "ExpenseTracker.settings") 10 | try: 11 | from django.core.management import execute_from_command_line 12 | except ImportError as exc: 13 | raise ImportError( 14 | "Couldn't import Django. Are you sure it's installed and " 15 | "available on your PYTHONPATH environment variable? Did you " 16 | "forget to activate a virtual environment?" 17 | ) from exc 18 | execute_from_command_line(sys.argv) 19 | 20 | 21 | if __name__ == "__main__": 22 | main() 23 | -------------------------------------------------------------------------------- /templates/expenses/object_detail.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% block content %} 3 |
4 |
5 |
6 |

{{ model_name|title }} Detail

7 |

{{ object }}

8 | 19 |
20 |
21 |
22 | {% endblock %} -------------------------------------------------------------------------------- /ExpenseTracker/urls.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from django.conf import settings 3 | from django.urls import path, include 4 | from django.urls import re_path 5 | from django.views.static import serve 6 | 7 | urlpatterns = [ 8 | path("admin/", admin.site.urls), 9 | path("", include("expenses.urls")), 10 | path('users/', include('users.urls')), 11 | path('visuals/', include(('visuals.urls', 'visuals'), namespace='visuals')), 12 | ] 13 | 14 | 15 | if not settings.DEBUG: 16 | urlpatterns += [ 17 | re_path(r"^media/(?P.*)$", serve, {"document_root": settings.MEDIA_ROOT}), 18 | re_path( 19 | r"^static/(?P.*)$", serve, {"document_root": settings.STATIC_ROOT} 20 | ), 21 | ] 22 | else: 23 | from django.conf.urls.static import static 24 | 25 | urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) 26 | urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) 27 | -------------------------------------------------------------------------------- /payment/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.0.6 on 2024-09-27 09:53 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | initial = True 9 | 10 | dependencies = [ 11 | ] 12 | 13 | operations = [ 14 | migrations.CreateModel( 15 | name='MpesaTransaction', 16 | fields=[ 17 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 18 | ('transaction_id', models.CharField(max_length=20, unique=True)), 19 | ('amount', models.DecimalField(decimal_places=2, max_digits=10)), 20 | ('recipient', models.CharField(max_length=100)), 21 | ('phone', models.CharField(max_length=15)), 22 | ('transaction_date', models.DateTimeField()), 23 | ('created_at', models.DateTimeField(auto_now_add=True)), 24 | ], 25 | ), 26 | ] 27 | -------------------------------------------------------------------------------- /users/migrations/0005_expense.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.0.6 on 2024-09-22 10:26 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('users', '0004_alter_profile_profile_picture'), 10 | ] 11 | 12 | operations = [ 13 | migrations.CreateModel( 14 | name='Expense', 15 | fields=[ 16 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 17 | ('amount', models.DecimalField(decimal_places=2, max_digits=10)), 18 | ('date', models.DateField()), 19 | ('description', models.TextField()), 20 | ('category', models.CharField(choices=[('food', 'Food'), ('transport', 'Transport'), ('entertainment', 'Entertainment'), ('utilities', 'Utilities'), ('healthcare', 'Healthcare'), ('education', 'Education'), ('other', 'Other')], max_length=50)), 21 | ], 22 | ), 23 | ] 24 | -------------------------------------------------------------------------------- /expenses/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django.contrib.auth.models import User 3 | 4 | class Category(models.Model): 5 | name = models.CharField(max_length=100) 6 | user = models.ForeignKey(User, on_delete=models.CASCADE) 7 | 8 | def __str__(self): 9 | return self.name 10 | 11 | class Budget(models.Model): 12 | amount = models.DecimalField(max_digits=10, decimal_places=2) 13 | start_date = models.DateField() 14 | end_date = models.DateField() 15 | user = models.ForeignKey(User, on_delete=models.CASCADE) 16 | 17 | def __str__(self): 18 | return f"Budget for {self.user.username}: {self.amount}" 19 | 20 | class Expense(models.Model): 21 | amount = models.DecimalField(max_digits=10, decimal_places=2) 22 | date = models.DateField() 23 | description = models.CharField(max_length=255) 24 | category = models.ForeignKey(Category, on_delete=models.SET_NULL, null=True) 25 | user = models.ForeignKey(User, on_delete=models.CASCADE) 26 | 27 | def __str__(self): 28 | return f"{self.description} - {self.amount}" -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Fidel 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /users/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django.contrib.auth.models import User 3 | from django.db.models.signals import post_save 4 | from django.dispatch import receiver 5 | 6 | class Profile(models.Model): 7 | user = models.OneToOneField(User, on_delete=models.CASCADE) 8 | bio = models.CharField(max_length=500, blank=True) 9 | location = models.CharField(max_length=30, blank=True) 10 | birth_date = models.DateField(null=True, blank=True) 11 | profile_picture = models.ImageField(upload_to='profile_pics', default='default-profile.png') 12 | 13 | @property 14 | def profile_picture_url(self): 15 | if self.profile_picture and hasattr(self.profile_picture, 'url'): 16 | return self.profile_picture.url 17 | return '/static/img/default-profile.png' 18 | 19 | @receiver(post_save, sender=User) 20 | def create_or_update_user_profile(sender, instance, created, **kwargs): 21 | if created: 22 | Profile.objects.create(user=instance) 23 | else: 24 | instance.profile.save() 25 | @receiver(post_save, sender=User) 26 | def save_user_profile(sender, instance, **kwargs): 27 | instance.profile.save() 28 | 29 | -------------------------------------------------------------------------------- /users/views.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render, redirect 2 | from django.contrib import messages 3 | from django.contrib.auth.decorators import login_required 4 | from .forms import UserRegisterForm, UserUpdateForm, ProfileUpdateForm 5 | 6 | def register(request): 7 | if request.method == 'POST': 8 | form = UserRegisterForm(request.POST) 9 | if form.is_valid(): 10 | form.save() 11 | return redirect('dashboard') 12 | else: 13 | form = UserRegisterForm() 14 | return render(request, 'users/register.html', {'form': form}) 15 | 16 | @login_required 17 | def profile(request): 18 | if request.method == 'POST': 19 | u_form = UserUpdateForm(request.POST, instance=request.user) 20 | p_form = ProfileUpdateForm(request.POST, request.FILES, instance=request.user.profile) 21 | if u_form.is_valid() and p_form.is_valid(): 22 | u_form.save() 23 | p_form.save() 24 | messages.success(request, 'Your account has been updated!') 25 | return redirect('profile') 26 | else: 27 | u_form = UserUpdateForm(instance=request.user) 28 | p_form = ProfileUpdateForm(instance=request.user.profile) 29 | 30 | context = { 31 | 'u_form': u_form, 32 | 'p_form': p_form 33 | } 34 | return render(request, 'users/profile.html', context) 35 | -------------------------------------------------------------------------------- /users/forms.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | from django.contrib.auth.forms import UserCreationForm 3 | from django.contrib.auth.models import User 4 | from .models import Profile 5 | 6 | 7 | class UserRegisterForm(UserCreationForm): 8 | email = forms.EmailField() 9 | 10 | class Meta: 11 | model = User 12 | fields = ['username', 'email', 'password1', 'password2'] 13 | 14 | class UserUpdateForm(forms.ModelForm): 15 | email = forms.EmailField() 16 | 17 | class Meta: 18 | model = User 19 | fields = ['username', 'email',] 20 | 21 | class ProfileUpdateForm(forms.ModelForm): 22 | class Meta: 23 | model = Profile 24 | fields = ['bio', 'location', 'birth_date', 'profile_picture'] 25 | widgets = { 26 | 'birth_date': forms.DateInput(attrs={ 27 | 'type': 'date', 28 | 'class': 'form-control date-input-container' 29 | }), 30 | 'profile_picture': forms.ClearableFileInput(attrs={ 31 | 'class': 'file-input-container' 32 | }), 33 | } 34 | 35 | def __init__(self, *args, **kwargs): 36 | super().__init__(*args, **kwargs) 37 | self.fields['birth_date'].widget.attrs['class'] = 'form-control date-input-container' 38 | self.fields['profile_picture'].widget.attrs['class'] = 'file-input-container' 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /users/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.0.2 on 2024-09-21 12:39 2 | 3 | import django.db.models.deletion 4 | from django.conf import settings 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | initial = True 11 | 12 | dependencies = [ 13 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 14 | ] 15 | 16 | operations = [ 17 | migrations.CreateModel( 18 | name="Profile", 19 | fields=[ 20 | ( 21 | "id", 22 | models.BigAutoField( 23 | auto_created=True, 24 | primary_key=True, 25 | serialize=False, 26 | verbose_name="ID", 27 | ), 28 | ), 29 | ("bio", models.TextField(blank=True, max_length=500)), 30 | ("location", models.CharField(blank=True, max_length=30)), 31 | ("birth_date", models.DateField(blank=True, null=True)), 32 | ( 33 | "user", 34 | models.OneToOneField( 35 | on_delete=django.db.models.deletion.CASCADE, 36 | to=settings.AUTH_USER_MODEL, 37 | ), 38 | ), 39 | ], 40 | ), 41 | ] 42 | -------------------------------------------------------------------------------- /templates/expenses/object_list.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% block content %} 3 |
4 |
5 |
6 |

{{ model_name|title }} List

7 | {% if objects %} 8 | 23 | {% else %} 24 |

No {{ model_name }}s found.

25 | {% endif %} 26 | 31 |
32 |
33 |
34 | {% endblock %} -------------------------------------------------------------------------------- /expenses/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path, include 2 | from . import views 3 | 4 | urlpatterns = [ 5 | path('', views.dashboard, name='dashboard'), 6 | path('categories/', views.category_list, name='category_list'), 7 | path('categories//', views.category_detail, name='category_detail'), 8 | path('categories/new/', views.category_create_or_update, name='category_create'), 9 | path('categories//edit/', views.category_create_or_update, name='category_update'), 10 | path('categories//delete/', views.category_delete, name='category_delete'), 11 | 12 | path('budgets/', views.budget_list, name='budget_list'), 13 | path('budgets//', views.budget_detail, name='budget_detail'), 14 | path('budgets/new/', views.budget_create_or_update, name='budget_create'), 15 | path('budgets//edit/', views.budget_create_or_update, name='budget_update'), 16 | path('budgets//delete/', views.budget_delete, name='budget_delete'), 17 | 18 | path('expenses/', views.expense_list, name='expense_list'), 19 | path('expenses//', views.expense_detail, name='expense_detail'), 20 | path('expenses/new/', views.expense_create_or_update, name='expense_create'), 21 | path('expenses//edit/', views.expense_create_or_update, name='expense_update'), 22 | path('expenses//delete/', views.expense_delete, name='expense_delete'), 23 | 24 | path('payment/', include('payment.urls')), 25 | ] 26 | 27 | -------------------------------------------------------------------------------- /visuals/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django.contrib.auth.models import User 3 | 4 | class Category(models.Model): 5 | name = models.CharField(max_length=100) 6 | user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='expenses_categories') 7 | 8 | def __str__(self): 9 | return self.name 10 | 11 | class Budget(models.Model): 12 | amount = models.DecimalField(max_digits=10, decimal_places=2) 13 | start_date = models.DateField() 14 | end_date = models.DateField() 15 | user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='expenses_budgets') 16 | 17 | def __str__(self): 18 | return f"Budget for {self.user.username}: {self.amount}" 19 | 20 | class Expense(models.Model): 21 | PAYMENT_METHOD_CHOICES = [ 22 | ('cash', 'Cash'), 23 | ('credit', 'Credit Card'), 24 | ('debit', 'Debit Card'), 25 | ('paypal', 'PayPal'), 26 | ] 27 | 28 | amount = models.DecimalField(max_digits=10, decimal_places=2) 29 | date = models.DateField() 30 | description = models.CharField(max_length=255) 31 | category = models.ForeignKey(Category, on_delete=models.SET_NULL, null=True) 32 | user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='expenses_expenses') 33 | currency = models.CharField(max_length=10, default='USD') 34 | payment_method = models.CharField(max_length=10, choices=PAYMENT_METHOD_CHOICES, default='cash') 35 | tags = models.CharField(max_length=255, blank=True, null=True) 36 | 37 | def __str__(self): 38 | return f"{self.description} - {self.amount} {self.currency} ({self.payment_method})" 39 | 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /expenses/forms.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | from .models import Budget 3 | from django import forms 4 | from .models import Category, Budget, Expense 5 | 6 | class CategoryForm(forms.ModelForm): 7 | class Meta: 8 | model = Category 9 | fields = ['name'] 10 | 11 | 12 | class BudgetForm(forms.ModelForm): 13 | class Meta: 14 | model = Budget 15 | fields = ['amount', 'start_date', 'end_date'] 16 | widgets = { 17 | 'amount': forms.TextInput(attrs={'class': 'date-input-container'}), 18 | 'start_date': forms.DateInput(attrs={ 19 | 'type': 'date', 20 | 'class': 'date-input-container' 21 | }), 22 | 'end_date': forms.DateInput(attrs={ 23 | 'type': 'date', 24 | 'class': 'date-input-container' 25 | }), 26 | } 27 | 28 | def __init__(self, *args, **kwargs): 29 | super().__init__(*args, **kwargs) 30 | self.fields['start_date'].widget.attrs['class'] = 'date-input-container' 31 | self.fields['end_date'].widget.attrs['class'] = 'date-input-container' 32 | 33 | class ExpenseForm(forms.ModelForm): 34 | class Meta: 35 | model = Expense 36 | fields = ['amount', 'date', 'description', 'category'] 37 | widgets = { 38 | 'date': forms.DateInput(attrs={ 39 | 'type': 'date', 40 | 'class': 'date-input-container' 41 | }), 42 | 'category': forms.RadioSelect(attrs={'class': 'category-bubbles'}), 43 | } 44 | 45 | def __init__(self, *args, **kwargs): 46 | super().__init__(*args, **kwargs) 47 | self.fields['date'].widget.attrs['class'] = 'date-input-container' -------------------------------------------------------------------------------- /templates/users/login.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block title %}Login - ExpenseTracker{% endblock %} 4 | 5 | {% block content %} 6 |
7 |
8 |
9 |
10 |

Log In

11 | 12 | {% if form.non_field_errors %} 13 |
14 | {{ form.non_field_errors }} 15 |
16 | {% endif %} 17 | 18 |
19 | {% csrf_token %} 20 | {% for field in form %} 21 |
22 | {{ field }} 23 | 24 | {% if field.errors %} 25 |
26 | {{ field.errors }} 27 |
28 | {% endif %} 29 |
30 | {% endfor %} 31 | 32 |
33 | 34 |
35 | 36 | Need An Account? Sign Up Now 37 | 38 |
39 |
40 |
41 |
42 |
43 | {% endblock %} 44 | -------------------------------------------------------------------------------- /templates/users/register.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block title %}Register - ExpenseTracker{% endblock %} 4 | 5 | {% block content %} 6 |
7 |
8 |
9 |
10 |

Register

11 | 12 | {% if form.non_field_errors %} 13 |
14 | {{ form.non_field_errors }} 15 |
16 | {% endif %} 17 | 18 |
19 | {% csrf_token %} 20 | {% for field in form %} 21 |
22 | {{ field }} 23 | 24 | {% if field.errors %} 25 |
26 | {{ field.errors }} 27 |
28 | {% endif %} 29 |
30 | {% endfor %} 31 | 32 |
33 | 34 |
35 | 36 | Already Have An Account? Sign In 37 | 38 |
39 |
40 |
41 |
42 |
43 | {% endblock %} 44 | -------------------------------------------------------------------------------- /templates/expenses/object_form.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% block content %} 3 |
4 |
5 |
6 |

{% if is_update %}Edit{% else %}Create{% endif %} {{ model_name|title }}

7 |
8 | {% csrf_token %} 9 | {% for field in form %} 10 | {% if field.name == 'date' or field.name == 'start_date' or field.name == 'end_date' or field.name == 'birth_date' %} 11 |
12 | {{ field.label }} 13 |
14 | {{ field }} 15 |
16 |
17 | {% elif field.name == 'category' %} 18 |
19 | 20 |
21 | {% for radio in field %} 22 | {{ radio.tag }} 23 | 24 | {% endfor %} 25 |
26 |
27 | {% else %} 28 |
29 | {{ field }} 30 | 31 |
32 | {% endif %} 33 | {% endfor %} 34 |
35 | 36 | Cancel 37 |
38 |
39 |
40 |
41 |
42 | {% endblock %} 43 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Expense Tracker README 2 | 3 | ### Introduction 4 | This Django web application is designed to help you effectively track and manage your expenses. It provides features for creating and managing categories, budgets, and individual expenses. 5 | 6 | ### Features 7 | * **Categories:** 8 | * Create, edit, and delete categories to organize your expenses. 9 | * View a list of all categories. 10 | * **Budgets:** 11 | * Set budgets for specific categories to track spending limits. 12 | * View budget details and track progress towards goals. 13 | * **Expenses:** 14 | * Record expenses, including the amount, date, category, and description. 15 | * View a list of all expenses, filtered by category or date range. 16 | * Edit or delete existing expenses. 17 | 18 | ### Usage 19 | 1. **Installation:** 20 | ```bash 21 | # Clone the repository 22 | git clone https://github.com/phantom-kali/ExpenseTracker.git 23 | 24 | # Navigate to the project directory 25 | cd ExpenseTracker 26 | 27 | # Create a virtual environment 28 | python -m venv venv 29 | 30 | # Activate the virtual environment 31 | # For Windows: 32 | venv\Scripts\activate 33 | # For Linux/macOS: 34 | source venv/bin/activate 35 | 36 | # Install dependencies 37 | pip install -r requirements.txt 38 | ``` 39 | 40 | 2. **Database Setup:** 41 | ```bash 42 | # Create a database according to your Django settings 43 | 44 | # Run migrations 45 | python manage.py migrate 46 | ``` 47 | 48 | 3. **Run the Server:** 49 | ```bash 50 | # Start the development server 51 | python manage.py runserver 52 | ``` 53 | 54 | 4. **Access the Application:** 55 | * Open a web browser and navigate to: 56 | `http://127.0.0.1:8000/` 57 | 58 | ### Contributing 59 | Contributions are welcome! Please feel free to submit pull requests or issues. 60 | 61 | ### License 62 | This project is licensed under the [MIT License](https://opensource.org/licenses/MIT). -------------------------------------------------------------------------------- /templates/mpesa/mpesa_sms.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block extra_css %} 4 | 45 | {% endblock %} 46 | 47 | {% block content %} 48 |
49 |
50 |
51 |

Submit M-Pesa SMS

52 |
53 | {% csrf_token %} 54 |
55 | 56 |
57 | 58 | 59 | 60 | 61 |
62 |
63 | 66 |
67 |
68 |
69 |
70 | {% endblock %} 71 | -------------------------------------------------------------------------------- /visuals/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.1.1 on 2024-09-30 16:58 2 | 3 | import django.db.models.deletion 4 | from django.conf import settings 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | initial = True 11 | 12 | dependencies = [ 13 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 14 | ] 15 | 16 | operations = [ 17 | migrations.CreateModel( 18 | name='Budget', 19 | fields=[ 20 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 21 | ('amount', models.DecimalField(decimal_places=2, max_digits=10)), 22 | ('start_date', models.DateField()), 23 | ('end_date', models.DateField()), 24 | ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='expenses_budgets', to=settings.AUTH_USER_MODEL)), 25 | ], 26 | ), 27 | migrations.CreateModel( 28 | name='Category', 29 | fields=[ 30 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 31 | ('name', models.CharField(max_length=100)), 32 | ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='expenses_categories', to=settings.AUTH_USER_MODEL)), 33 | ], 34 | ), 35 | migrations.CreateModel( 36 | name='Expense', 37 | fields=[ 38 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 39 | ('amount', models.DecimalField(decimal_places=2, max_digits=10)), 40 | ('date', models.DateField()), 41 | ('description', models.CharField(max_length=255)), 42 | ('currency', models.CharField(default='USD', max_length=10)), 43 | ('payment_method', models.CharField(choices=[('cash', 'Cash'), ('credit', 'Credit Card'), ('debit', 'Debit Card'), ('paypal', 'PayPal')], default='cash', max_length=10)), 44 | ('tags', models.CharField(blank=True, max_length=255, null=True)), 45 | ('category', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='visuals.category')), 46 | ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='expenses_expenses', to=settings.AUTH_USER_MODEL)), 47 | ], 48 | ), 49 | ] 50 | -------------------------------------------------------------------------------- /expenses/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.0.2 on 2024-09-21 12:39 2 | 3 | import django.db.models.deletion 4 | from django.conf import settings 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | initial = True 11 | 12 | dependencies = [ 13 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 14 | ] 15 | 16 | operations = [ 17 | migrations.CreateModel( 18 | name="Budget", 19 | fields=[ 20 | ( 21 | "id", 22 | models.BigAutoField( 23 | auto_created=True, 24 | primary_key=True, 25 | serialize=False, 26 | verbose_name="ID", 27 | ), 28 | ), 29 | ("amount", models.DecimalField(decimal_places=2, max_digits=10)), 30 | ("start_date", models.DateField()), 31 | ("end_date", models.DateField()), 32 | ( 33 | "user", 34 | models.ForeignKey( 35 | on_delete=django.db.models.deletion.CASCADE, 36 | to=settings.AUTH_USER_MODEL, 37 | ), 38 | ), 39 | ], 40 | ), 41 | migrations.CreateModel( 42 | name="Category", 43 | fields=[ 44 | ( 45 | "id", 46 | models.BigAutoField( 47 | auto_created=True, 48 | primary_key=True, 49 | serialize=False, 50 | verbose_name="ID", 51 | ), 52 | ), 53 | ("name", models.CharField(max_length=100)), 54 | ( 55 | "user", 56 | models.ForeignKey( 57 | on_delete=django.db.models.deletion.CASCADE, 58 | to=settings.AUTH_USER_MODEL, 59 | ), 60 | ), 61 | ], 62 | ), 63 | migrations.CreateModel( 64 | name="Expense", 65 | fields=[ 66 | ( 67 | "id", 68 | models.BigAutoField( 69 | auto_created=True, 70 | primary_key=True, 71 | serialize=False, 72 | verbose_name="ID", 73 | ), 74 | ), 75 | ("amount", models.DecimalField(decimal_places=2, max_digits=10)), 76 | ("date", models.DateField()), 77 | ("description", models.CharField(max_length=255)), 78 | ( 79 | "category", 80 | models.ForeignKey( 81 | null=True, 82 | on_delete=django.db.models.deletion.SET_NULL, 83 | to="expenses.category", 84 | ), 85 | ), 86 | ( 87 | "user", 88 | models.ForeignKey( 89 | on_delete=django.db.models.deletion.CASCADE, 90 | to=settings.AUTH_USER_MODEL, 91 | ), 92 | ), 93 | ], 94 | ), 95 | ] 96 | -------------------------------------------------------------------------------- /visuals/views.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render 2 | from django.db.models import Sum, Count 3 | from expenses.models import Expense, Category, Budget 4 | from django.utils.timezone import now 5 | from django.contrib.auth.decorators import login_required 6 | from django.db.models.functions import TruncDate, TruncMonth 7 | from datetime import datetime, timedelta 8 | from django.http import JsonResponse 9 | from dateutil import parser 10 | import logging 11 | 12 | # Logger setup 13 | logger = logging.getLogger(__name__) 14 | 15 | @login_required 16 | def statistics_view(request): 17 | total_expenses = Expense.objects.filter(user=request.user).aggregate(Sum('amount'))['amount__sum'] or 0 18 | total_categories = Category.objects.filter(user=request.user).count() 19 | total_budgets = Budget.objects.filter(user=request.user).count() 20 | 21 | context = { 22 | 'total_expenses': total_expenses, 23 | 'total_categories': total_categories, 24 | 'total_budgets': total_budgets, 25 | } 26 | return render(request, 'visuals/statistics.html', context) 27 | 28 | 29 | @login_required 30 | def expenses_by_category_view(request): 31 | # Get all categories for the logged-in user 32 | categories = Category.objects.filter(user=request.user) 33 | expense_data = [] 34 | 35 | # Loop through each category and calculate total expenses and expense count 36 | for category in categories: 37 | total_expenses = ( 38 | Expense.objects.filter(user=request.user, category=category) 39 | .aggregate(total_amount=Sum('amount'))['total_amount'] or 0 40 | ) 41 | expense_count = ( 42 | Expense.objects.filter(user=request.user, category=category) 43 | .count() 44 | ) 45 | expense_data.append({ 46 | 'category': category.name, # Category name 47 | 'total_expenses': float(total_expenses), # Ensure it's a float for JSON compatibility 48 | 'expense_count': expense_count, 49 | }) 50 | 51 | # Calculate totals 52 | total_expenses_sum = sum(item['total_expenses'] for item in expense_data) 53 | total_expense_count = sum(item['expense_count'] for item in expense_data) 54 | 55 | # Pass data to template 56 | context = { 57 | 'expense_data': expense_data, # Pass the list of dictionaries 58 | 'total_expenses': total_expenses_sum, 59 | 'total_expense_count': total_expense_count, 60 | 'base_currency': 'USD', # Define as appropriate 61 | } 62 | return render(request, 'visuals/expenses_by_category.html', context) 63 | 64 | @login_required 65 | def budget_vs_expenses_view(request): 66 | budgets = Budget.objects.filter(user=request.user) 67 | budget_expense_data = [] 68 | 69 | for budget in budgets: 70 | total_expenses = ( 71 | Expense.objects.filter(user=request.user, date__range=[budget.start_date, budget.end_date]) 72 | .aggregate(total_amount=Sum('amount'))['total_amount'] or 0 73 | ) 74 | budget_expense_data.append({ 75 | 'budget': float(budget.amount), 76 | 'expenses': float(total_expenses), 77 | 'period': f"{budget.start_date} to {budget.end_date}", 78 | }) 79 | 80 | 81 | context = { 82 | 'budget_expense_data': budget_expense_data, 83 | } 84 | return render(request, 'visuals/budget_vs_expenses.html', context) 85 | 86 | -------------------------------------------------------------------------------- /ExpenseTracker/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for ExpenseTracker project. 3 | 4 | Generated by 'django-admin startproject' using Django 5.0.2. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/5.0/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/5.0/ref/settings/ 11 | """ 12 | 13 | import os 14 | from pathlib import Path 15 | 16 | # Build paths inside the project like this: BASE_DIR / 'subdir'. 17 | BASE_DIR = Path(__file__).resolve().parent.parent 18 | 19 | 20 | # Quick-start development settings - unsuitable for production 21 | # See https://docs.djangoproject.com/en/5.0/howto/deployment/checklist/ 22 | 23 | # SECURITY WARNING: keep the secret key used in production secret! 24 | SECRET_KEY = "django-insecure-@f#)%3vh0rs34%ztzxkgop-+@2umd#o^143#d2$l*wtxko7(x=" 25 | 26 | # SECURITY WARNING: don't run with debug turned on in production! 27 | DEBUG = True 28 | 29 | ALLOWED_HOSTS = [ '0.0.0.0', '127.0.0.1'] 30 | 31 | 32 | # Application definition 33 | 34 | INSTALLED_APPS = [ 35 | "django.contrib.admin", 36 | "django.contrib.auth", 37 | "django.contrib.contenttypes", 38 | "django.contrib.sessions", 39 | "django.contrib.messages", 40 | "django.contrib.staticfiles", 41 | "rest_framework", 42 | "expenses", 43 | "users", 44 | 'payment', 45 | 'visuals', 46 | ] 47 | 48 | MIDDLEWARE = [ 49 | "django.middleware.security.SecurityMiddleware", 50 | "django.contrib.sessions.middleware.SessionMiddleware", 51 | "django.middleware.common.CommonMiddleware", 52 | "django.middleware.csrf.CsrfViewMiddleware", 53 | "django.contrib.auth.middleware.AuthenticationMiddleware", 54 | "django.contrib.messages.middleware.MessageMiddleware", 55 | "django.middleware.clickjacking.XFrameOptionsMiddleware", 56 | ] 57 | 58 | ROOT_URLCONF = "ExpenseTracker.urls" 59 | 60 | TEMPLATES = [ 61 | { 62 | "BACKEND": "django.template.backends.django.DjangoTemplates", 63 | "DIRS": [os.path.join(BASE_DIR, "templates")], 64 | "APP_DIRS": True, 65 | "OPTIONS": { 66 | "context_processors": [ 67 | "django.template.context_processors.debug", 68 | "django.template.context_processors.request", 69 | "django.contrib.auth.context_processors.auth", 70 | "django.contrib.messages.context_processors.messages", 71 | ], 72 | }, 73 | }, 74 | ] 75 | 76 | WSGI_APPLICATION = "ExpenseTracker.wsgi.application" 77 | 78 | 79 | # Database 80 | # https://docs.djangoproject.com/en/5.0/ref/settings/#databases 81 | 82 | DATABASES = { 83 | "default": { 84 | "ENGINE": "django.db.backends.sqlite3", 85 | "NAME": BASE_DIR / "db.sqlite3", 86 | } 87 | } 88 | 89 | 90 | # Password validation 91 | # https://docs.djangoproject.com/en/5.0/ref/settings/#auth-password-validators 92 | 93 | AUTH_PASSWORD_VALIDATORS = [ 94 | { 95 | "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", 96 | }, 97 | { 98 | "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", 99 | }, 100 | { 101 | "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator", 102 | }, 103 | { 104 | "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator", 105 | }, 106 | ] 107 | 108 | 109 | # Internationalization 110 | # https://docs.djangoproject.com/en/5.0/topics/i18n/ 111 | 112 | LANGUAGE_CODE = "en-us" 113 | 114 | TIME_ZONE = "UTC" 115 | 116 | USE_I18N = True 117 | 118 | USE_TZ = True 119 | 120 | 121 | # Static files (CSS, JavaScript, Images) 122 | # https://docs.djangoproject.com/en/5.0/howto/static-files/ 123 | 124 | STATIC_URL = "/static/" 125 | STATICFILES_DIRS = [os.path.join(BASE_DIR, 'static')] 126 | STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles') 127 | 128 | MEDIA_ROOT = os.path.join(BASE_DIR, 'media') 129 | MEDIA_URL = '/media/' 130 | 131 | # Default primary key field type 132 | # https://docs.djangoproject.com/en/5.0/ref/settings/#default-auto-field 133 | 134 | DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" 135 | 136 | 137 | LOGIN_REDIRECT_URL = 'dashboard' 138 | LOGIN_URL = 'login' 139 | 140 | 141 | -------------------------------------------------------------------------------- /templates/visuals/expenses_by_category.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block title %}Expenses by Category{% endblock %} 4 | 5 | {% block content %} 6 |
7 |

Expenses by Category

8 | 9 | 10 |
11 | 12 | 17 |
18 | 19 | 20 |
21 | 22 |
23 | 24 |
25 | 26 | 27 |
28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | {% for expense in expense_data %} 38 | 39 | 40 | 41 | 42 | 43 | {% endfor %} 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 |
Category NameExpense CountAmount ({{ base_currency }})
{{ expense.category }}{{ expense.expense_count }}{{ expense.total_expenses|floatformat:2 }}
Total{{ total_expense_count }}{{ total_expenses|floatformat:2 }}
53 |
54 |
55 |
56 | {% endblock %} 57 | 58 | {% block extra_js %} 59 | 60 | 115 | {% endblock %} 116 | -------------------------------------------------------------------------------- /templates/expenses/dashboard.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block title %}Dashboard - ExpenseTracker{% endblock %} 4 | 5 | {% block content %} 6 |
7 |
8 |
9 |
10 |

Dashboard

11 |
12 |
13 |
14 | 15 |
16 |
17 |
18 |
19 |
20 | 21 |
22 |
Total Expenses
23 |

${{ total_expenses }}

24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 | 32 |
33 |
This Month
34 |

${{ monthly_expenses }}

35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 | 43 |
44 |
Budget Remaining
45 |

${{ budget_remaining }}

46 |
47 |
48 |
49 |
50 | 51 |
52 |
53 |
54 |
55 |
56 |
Recent Expenses
57 | 58 |
59 |
60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | {% for expense in expenses %} 71 | 72 | 73 | 74 | 75 | 76 | 77 | {% endfor %} 78 | 79 |
DateCategoryDescriptionAmount
{{ expense.date }}{{ expense.category.name }}{{ expense.description }}${{ expense.amount }}
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
Categories
88 |
89 | {% for category in categories %} 90 |
91 | {{ category.name }} 92 | {{ category.expense_count }} 93 |
94 | {% endfor %} 95 |
96 |
97 |
98 |
99 |
100 |
101 | {% endblock %} -------------------------------------------------------------------------------- /expenses/views.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render, redirect, get_object_or_404 2 | from django.contrib.auth.decorators import login_required 3 | from django.contrib import messages 4 | from .models import Category, Budget, Expense 5 | from .forms import CategoryForm, BudgetForm, ExpenseForm 6 | from django.db.models import Sum 7 | from django.utils import timezone 8 | 9 | @login_required 10 | def dashboard(request): 11 | expenses = Expense.objects.filter(user=request.user).order_by("-date")[:5] 12 | categories = Category.objects.filter(user=request.user) 13 | budget = Budget.objects.filter(user=request.user).last() 14 | 15 | total_expenses = Expense.objects.filter(user=request.user).aggregate(Sum("amount"))["amount__sum"] or 0 16 | monthly_expenses = Expense.objects.filter(user=request.user, date__month=timezone.now().month).aggregate(Sum("amount"))["amount__sum"] or 0 17 | budget_amount = budget.amount if budget else 0 18 | budget_remaining = budget_amount - monthly_expenses 19 | 20 | context = { 21 | "expenses": expenses, 22 | "categories": categories, 23 | "budget": budget, 24 | "total_expenses": total_expenses, 25 | "monthly_expenses": monthly_expenses, 26 | "budget_remaining": budget_remaining, 27 | } 28 | return render(request, "expenses/dashboard.html", context) 29 | 30 | @login_required 31 | def object_list(request, model, template_name): 32 | objects = model.objects.filter(user=request.user) 33 | context = { 34 | 'objects': objects, 35 | 'model_name': model.__name__.lower() 36 | } 37 | return render(request, template_name, context) 38 | 39 | @login_required 40 | def object_detail(request, model, pk, template_name): 41 | obj = get_object_or_404(model, pk=pk, user=request.user) 42 | context = { 43 | 'object': obj, 44 | 'model_name': model.__name__.lower() 45 | } 46 | return render(request, template_name, context) 47 | 48 | @login_required 49 | def object_create_or_update(request, model, form_class, pk=None, template_name='expenses/object_form.html'): 50 | if pk: 51 | obj = get_object_or_404(model, pk=pk, user=request.user) 52 | form = form_class(request.POST or None, instance=obj) 53 | else: 54 | form = form_class(request.POST or None) 55 | 56 | if request.method == 'POST' and form.is_valid(): 57 | instance = form.save(commit=False) 58 | instance.user = request.user 59 | instance.save() 60 | messages.success(request, f'{model.__name__} {"updated" if pk else "created"} successfully.') 61 | return redirect('dashboard') 62 | 63 | context = { 64 | 'form': form, 65 | 'model_name': model.__name__.lower(), 66 | 'is_update': pk is not None 67 | } 68 | return render(request, template_name, context) 69 | 70 | @login_required 71 | def object_delete(request, model, pk, template_name='expenses/object_confirm_delete.html'): 72 | obj = get_object_or_404(model, pk=pk, user=request.user) 73 | if request.method == 'POST': 74 | obj.delete() 75 | messages.success(request, f'{model.__name__} deleted successfully.') 76 | return redirect('dashboard') 77 | context = { 78 | 'object': obj, 79 | 'model_name': model.__name__.lower() 80 | } 81 | return render(request, template_name, context) 82 | 83 | # Use these generic views for each model 84 | @login_required 85 | def category_list(request): 86 | return object_list(request, Category, 'expenses/object_list.html') 87 | 88 | @login_required 89 | def category_detail(request, pk): 90 | return object_detail(request, Category, pk, 'expenses/object_detail.html') 91 | 92 | @login_required 93 | def category_create_or_update(request, pk=None): 94 | return object_create_or_update(request, Category, CategoryForm, pk) 95 | 96 | @login_required 97 | def category_delete(request, pk): 98 | return object_delete(request, Category, pk) 99 | 100 | # Repeat similar patterns for Budget and Expense models 101 | @login_required 102 | def budget_list(request): 103 | return object_list(request, Budget, 'expenses/object_list.html') 104 | 105 | @login_required 106 | def budget_detail(request, pk): 107 | return object_detail(request, Budget, pk, 'expenses/object_detail.html') 108 | 109 | @login_required 110 | def budget_create_or_update(request, pk=None): 111 | return object_create_or_update(request, Budget, BudgetForm, pk) 112 | 113 | @login_required 114 | def budget_delete(request, pk): 115 | return object_delete(request, Budget, pk) 116 | 117 | @login_required 118 | def expense_list(request): 119 | return object_list(request, Expense, 'expenses/object_list.html') 120 | 121 | @login_required 122 | def expense_detail(request, pk): 123 | return object_detail(request, Expense, pk, 'expenses/object_detail.html') 124 | 125 | @login_required 126 | def expense_create_or_update(request, pk=None): 127 | return object_create_or_update(request, Expense, ExpenseForm, pk) 128 | 129 | @login_required 130 | def expense_delete(request, pk): 131 | return object_delete(request, Expense, pk) 132 | -------------------------------------------------------------------------------- /payment/views.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render, redirect 2 | from django.http import JsonResponse 3 | from django.views.decorators.csrf import csrf_exempt, csrf_protect 4 | from django.views.decorators.http import require_POST 5 | from django.db import transaction, IntegrityError 6 | from django.contrib import messages 7 | from django.contrib.auth.decorators import login_required 8 | from .models import MpesaTransaction 9 | from expenses.models import Category, Expense 10 | import json 11 | import logging 12 | import re 13 | from datetime import datetime 14 | from django.utils import timezone 15 | 16 | logger = logging.getLogger(__name__) 17 | 18 | def create_mpesa_transaction(transaction_data, user): 19 | try: 20 | mpesa_transaction = MpesaTransaction.objects.create( 21 | transaction_id=transaction_data['transaction_id'], 22 | amount=transaction_data['amount'], 23 | recipient=transaction_data['recipient'], 24 | phone=transaction_data.get('phone'), 25 | transaction_date=transaction_data['datetime'] 26 | ) 27 | 28 | if transaction_data.get('phone'): 29 | category_name = "M-Pesa Personal Payment" 30 | else: 31 | category_name = "M-Pesa Business Payment" 32 | 33 | category, _ = Category.objects.get_or_create(name=category_name, user=user) 34 | 35 | Expense.objects.create( 36 | user=user, 37 | amount=transaction_data['amount'], 38 | description=f"M-Pesa payment to {transaction_data['recipient']}", 39 | date=transaction_data['datetime'].date(), 40 | category=category 41 | ) 42 | 43 | except IntegrityError as e: 44 | logger.error(f"IntegrityError for transaction ID {transaction_data['transaction_id']}: {str(e)}") 45 | 46 | @csrf_exempt 47 | @require_POST 48 | def process_mpesa_sms(request): 49 | try: 50 | data = json.loads(request.body) 51 | sms_text = data.get('sms_text') 52 | 53 | if not sms_text: 54 | return JsonResponse({'error': 'No SMS text provided'}, status=400) 55 | 56 | transaction_data = parse_mpesa_sms(sms_text) 57 | 58 | if transaction_data: 59 | try: 60 | create_mpesa_transaction(transaction_data, request.user) 61 | return JsonResponse({'message': 'Transaction processed and expense added successfully'}, status=201) 62 | except IntegrityError as e: 63 | return JsonResponse({'error': 'Transaction already processed.'}, status=400) 64 | else: 65 | return JsonResponse({'error': 'Invalid M-Pesa SMS format'}, status=400) 66 | 67 | except json.JSONDecodeError: 68 | return JsonResponse({'error': 'Invalid JSON'}, status=400) 69 | except Exception as e: 70 | logger.exception("Unexpected error in process_mpesa_sms") 71 | return JsonResponse({'error': str(e)}, status=500) 72 | 73 | def parse_mpesa_sms(sms_text: str) -> dict: 74 | pattern = ( 75 | r"([A-Z0-9]+) Confirmed\.\s+Ksh([\d,]+\.?\d*)\s+(?:sent to|paid to)\s+([\w\s]+)(?:\s*for account\s+([\d\-]+))?\s+on\s+(\d{1,2}/\d{1,2}/\d{2})\s+at\s+(\d{1,2}:\d{2}\s*[AP]M)\." 76 | ) 77 | 78 | match = re.search(pattern, sms_text) 79 | 80 | if match: 81 | transaction_id, amount, recipient, account_number, date, time = match.groups() 82 | 83 | amount = float(amount.replace(',', '')) 84 | date_obj = timezone.make_aware(datetime.strptime(f"{date} {time}", "%d/%m/%y %I:%M %p")) 85 | 86 | phone = re.search(r'\b\d{10}\b', sms_text) 87 | phone_number = phone.group(0) if phone else None 88 | 89 | return { 90 | 'transaction_id': transaction_id, 91 | 'amount': amount, 92 | 'recipient': recipient.strip(), 93 | 'phone': phone_number, 94 | 'account_number': account_number.strip() if account_number else None, 95 | 'datetime': date_obj 96 | } 97 | 98 | return None 99 | 100 | @csrf_protect 101 | @login_required 102 | def submit_mpesa_sms(request): 103 | if request.method == 'POST': 104 | sms_text = request.POST.get('sms_text', '') 105 | if sms_text: 106 | transaction_data = parse_mpesa_sms(sms_text) 107 | if transaction_data: 108 | try: 109 | create_mpesa_transaction(transaction_data, request.user) 110 | messages.success(request, 'Transaction processed and expense added successfully.') 111 | return redirect('dashboard') 112 | except IntegrityError as e: 113 | messages.error(request, 'This transaction has already been processed.') 114 | except Exception as e: 115 | messages.error(request, 'An unexpected error occurred. Please try again later.') 116 | else: 117 | messages.error(request, 'Invalid M-Pesa SMS format. Add only deductions message!') 118 | else: 119 | messages.error(request, 'No SMS text provided.') 120 | return render(request, 'mpesa/mpesa_sms.html') 121 | -------------------------------------------------------------------------------- /templates/users/profile.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block title %}Profile - ExpenseTracker{% endblock %} 4 | 5 | {% block extra_css %} 6 | 88 | {% endblock %} 89 | 90 | {% block content %} 91 |
92 | {{ user.username }} 95 |

{{ user.username }}

96 |

{{ user.profile.location|default:"Location not set" }}

97 |
98 | 99 |
100 |
101 |
102 |
103 |
104 |
About Me
105 |

{{ user.profile.bio|default:"No bio available" }}

106 |
107 |
108 |
109 |
110 |
111 |
112 |
Edit Profile
113 |
114 | {% csrf_token %} 115 |
116 |
117 |
118 | {{ u_form.username }} 119 | 120 |
121 |
122 |
123 |
124 | {{ u_form.email }} 125 | 126 |
127 |
128 |
129 |
130 | {{ p_form.bio }} 131 | 132 |
133 |
134 |
135 |
136 | {{ p_form.location }} 137 | 138 |
139 |
140 |
141 |
142 | {{ p_form.birth_date }} 143 | 144 |
145 |
146 |
147 |
148 | 149 |
150 | 151 |

Drag & Drop your image here or click to select

152 | {{ p_form.profile_picture }} 153 |
154 |
155 | 158 |
159 |
160 |
161 |
162 |
163 |
164 | {% endblock %} 165 | 166 | {% block extra_js %} 167 | 206 | {% endblock %} 207 | -------------------------------------------------------------------------------- /templates/base.html: -------------------------------------------------------------------------------- 1 | {% load static %} 2 | 3 | 4 | 5 | 6 | 7 | 8 | {% block title %}ExpenseTracker{% endblock %} 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 106 |
107 | 108 | 109 |
110 |
111 | {% if messages %} 112 | {% for message in messages %} 113 | 117 | {% endfor %} 118 | {% endif %} 119 |
120 | {% block content %} 121 | {% endblock %} 122 |
123 | 124 | 125 | 126 | 145 | {% block extra_js %}{% endblock %} 146 | 147 | 148 | 149 | 150 | -------------------------------------------------------------------------------- /templates/visuals/budget_vs_expenses.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% block content %} 3 |

Budget vs Expenses & Budget Overview

4 | 5 |
6 | 7 |
8 |
9 |
Total Budget: {{ budget_total }}
10 |
11 |
12 |
Total Expenses: {{ expenses_total }}
13 |
14 |
15 | 16 | 17 |
18 |
19 | 20 | 30 |
31 |
32 | 33 | 34 |
35 |
36 |
37 | 38 |
39 |
40 |
41 |
42 | 43 | 44 | 190 | {% endblock %} 191 | -------------------------------------------------------------------------------- /static/css/style.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --primary-color: #28a745; 3 | --secondary-color: #6c757d; 4 | --success-color: #28a745; 5 | --danger-color: #dc3545; 6 | --light-bg: #f8f9fa; 7 | --dark-text: #343a40; 8 | --card-shadow: 0 4px 15px rgba(0, 0, 0, 0.1); 9 | } 10 | 11 | body { 12 | font-family: 'Roboto', sans-serif; 13 | background-color: var(--light-bg); 14 | color: var(--dark-text); 15 | line-height: 1.6; 16 | } 17 | 18 | .container { 19 | max-width: 1200px; 20 | margin: 0 auto; 21 | padding: 20px; 22 | } 23 | 24 | .card { 25 | background-color: #ffffff; 26 | border-radius: 15px; 27 | box-shadow: var(--card-shadow); 28 | transition: transform 0.3s ease, box-shadow 0.3s ease; 29 | overflow: hidden; 30 | } 31 | 32 | .card:hover { 33 | transform: translateY(-5px); 34 | box-shadow: 0 6px 20px rgba(0, 0, 0, 0.15); 35 | } 36 | 37 | .card-body { 38 | padding: 25px; 39 | } 40 | 41 | .card-title { 42 | font-size: 22px; 43 | font-weight: 600; 44 | color: var(--primary-color); 45 | margin-bottom: 20px; 46 | border-bottom: 2px solid var(--light-bg); 47 | padding-bottom: 10px; 48 | } 49 | 50 | .card-text { 51 | font-size: 36px; 52 | font-weight: bold; 53 | color: var(--secondary-color); 54 | } 55 | 56 | .table { 57 | width: 100%; 58 | border-collapse: separate; 59 | border-spacing: 0 10px; 60 | margin-top: 20px; 61 | } 62 | 63 | .table th, 64 | .table td { 65 | padding: 15px; 66 | text-align: left; 67 | } 68 | 69 | .table th { 70 | background-color: var(--primary-color); 71 | color: #ffffff; 72 | font-weight: 600; 73 | text-transform: uppercase; 74 | border: 2px solid #ffffff; 75 | } 76 | 77 | .table tbody tr { 78 | background-color: #ffffff; 79 | box-shadow: var(--card-shadow); 80 | transition: transform 0.2s ease; 81 | } 82 | 83 | .table tbody tr:hover { 84 | transform: scale(1.02); 85 | } 86 | .chart-container { 87 | margin-bottom: 40px; 88 | } 89 | .expense-amount { 90 | font-weight: bold; 91 | color: var(--danger-color); 92 | } 93 | 94 | .income-amount { 95 | font-weight: bold; 96 | color: var(--success-color); 97 | } 98 | 99 | .list-group-item { 100 | background-color: var(--light-bg); 101 | color: var(--dark-text); 102 | border: none; 103 | padding: 12px 15px; 104 | margin-bottom: 5px; 105 | border-radius: 8px; 106 | transition: background-color 0.3s ease; 107 | } 108 | 109 | .list-group-item:hover { 110 | background-color: #e9ecef; 111 | cursor: pointer; 112 | } 113 | 114 | h2 { 115 | font-size: 32px; 116 | font-weight: 700; 117 | margin-bottom: 25px; 118 | color: var(--primary-color); 119 | } 120 | 121 | .row { 122 | margin-bottom: 40px; 123 | } 124 | 125 | .form-control { 126 | border: 2px solid #ced4da; 127 | border-radius: 8px; 128 | padding: 12px; 129 | font-size: 16px; 130 | transition: border-color 0.3s ease; 131 | } 132 | 133 | .form-control:focus { 134 | border-color: var(--primary-color); 135 | box-shadow: none; 136 | } 137 | 138 | .btn { 139 | padding: 12px 20px; 140 | font-size: 16px; 141 | font-weight: 600; 142 | border-radius: 8px; 143 | transition: all 0.3s ease; 144 | cursor: pointer; 145 | } 146 | 147 | .btn-primary { 148 | background-color: var(--primary-color); 149 | border: none; 150 | } 151 | 152 | .btn-primary:hover { 153 | background-color: #218838; 154 | transform: translateY(-2px); 155 | } 156 | 157 | .chart-container { 158 | background-color: whitesmoke; 159 | border-radius: 15px; 160 | padding: 20px; 161 | box-shadow: var(--card-shadow); 162 | } 163 | 164 | .gradient-custom { 165 | background: linear-gradient(135deg, #28a745, #20c997); 166 | } 167 | 168 | .navbar { 169 | z-index: 1030; 170 | } 171 | 172 | .navbar .navbar-toggler { 173 | z-index: 1051; 174 | } 175 | 176 | .navbar-collapse { 177 | z-index: 1049; 178 | } 179 | 180 | .navbar-nav .nav-link { 181 | color: #ffffff !important; 182 | transition: color 0.3s ease; 183 | } 184 | 185 | .navbar-nav .nav-link:hover { 186 | color: #e9ecef !important; 187 | } 188 | 189 | .navbar-nav .nav-link.active { 190 | color: #ffffff !important; 191 | font-weight: bold; 192 | } 193 | 194 | .dropdown-menu { 195 | background-color: rgba(40, 167, 69, 0.9); 196 | z-index: 1052; 197 | } 198 | 199 | .dropdown-item { 200 | color: #ffffff !important; 201 | } 202 | 203 | .dropdown-item:hover { 204 | background-color: rgba(32, 201, 151, 0.7); 205 | color: #ffffff !important; 206 | } 207 | 208 | .expense-summary { 209 | display: flex; 210 | justify-content: space-between; 211 | margin-bottom: 30px; 212 | } 213 | 214 | .summary-item { 215 | text-align: center; 216 | padding: 20px; 217 | background-color: #ffffff; 218 | border-radius: 10px; 219 | box-shadow: var(--card-shadow); 220 | flex: 1; 221 | margin: 0 10px; 222 | transition: transform 0.3s ease; 223 | } 224 | 225 | .summary-item:hover { 226 | transform: translateY(-5px); 227 | } 228 | 229 | .summary-item h3 { 230 | font-size: 18px; 231 | color: var(--secondary-color); 232 | margin-bottom: 10px; 233 | } 234 | 235 | .summary-item p { 236 | font-size: 24px; 237 | font-weight: bold; 238 | margin: 0; 239 | } 240 | 241 | .summary-item.total-income p { 242 | color: var(--success-color); 243 | } 244 | 245 | .summary-item.total-expenses p { 246 | color: var(--danger-color); 247 | } 248 | 249 | .summary-item.balance p { 250 | color: var(--primary-color); 251 | } 252 | 253 | @media (max-width: 768px) { 254 | .card-text { 255 | font-size: 28px; 256 | } 257 | 258 | h2 { 259 | font-size: 28px; 260 | } 261 | 262 | .table th, 263 | .table td { 264 | padding: 10px; 265 | } 266 | 267 | .expense-summary { 268 | flex-direction: column; 269 | } 270 | 271 | .summary-item { 272 | margin: 10px 0; 273 | } 274 | } 275 | 276 | .profile-header { 277 | background: linear-gradient(rgba(0, 0, 0, 0.6), rgba(0, 0, 0, 0.6)), url('/static/img/profile-background.png'); 278 | background-size: cover; 279 | background-position: center; 280 | color: white; 281 | padding: 60px 0; 282 | margin-bottom: 40px; 283 | text-align: center; 284 | border-radius: 0 0 15px 15px; 285 | } 286 | 287 | .profile-img { 288 | width: 150px; 289 | height: 150px; 290 | object-fit: cover; 291 | border: 5px solid var(--primary-color); 292 | box-shadow: 0 4px 10px rgba(0, 0, 0, 0.2); 293 | border-radius: 50%; 294 | margin-bottom: 20px; 295 | } 296 | 297 | .profile-header h1 { 298 | font-size: 2.5rem; 299 | margin-bottom: 10px; 300 | } 301 | 302 | .profile-header p { 303 | font-size: 1.1rem; 304 | opacity: 0.9; 305 | } 306 | 307 | .drag-drop-area { 308 | border: 3px dashed var(--primary-color); 309 | border-radius: 10px; 310 | padding: 30px; 311 | text-align: center; 312 | cursor: pointer; 313 | transition: all 0.3s ease; 314 | } 315 | 316 | .drag-drop-area:hover, 317 | .drag-drop-area.dragover { 318 | background-color: rgba(40, 167, 69, 0.1); 319 | } 320 | 321 | .drag-drop-area i { 322 | font-size: 3rem; 323 | color: var(--primary-color); 324 | margin-bottom: 15px; 325 | } 326 | 327 | .drag-drop-area p { 328 | color: var(--secondary-color); 329 | margin-bottom: 0; 330 | } 331 | 332 | .form-outline input, 333 | .form-outline textarea { 334 | border-color: #ced4da; 335 | background-color: whitesmoke; 336 | } 337 | 338 | .form-outline input:focus, 339 | .form-outline textarea:focus { 340 | border-color: var(--primary-color); 341 | } 342 | 343 | .form-label { 344 | color: green; 345 | font-weight: bold; 346 | } 347 | 348 | .btn-primary { 349 | background-color: var(--primary-color); 350 | border-color: var(--primary-color); 351 | } 352 | 353 | .btn-primary:hover { 354 | background-color: #218838; 355 | border-color: #218838; 356 | } 357 | .date-input-container { 358 | position: relative; 359 | width: 60%; 360 | margin-bottom: 1.5rem; 361 | } 362 | 363 | .date-input-wrapper { 364 | height: 33px; 365 | width: 100%; 366 | margin: 0 auto; 367 | background-color: whitesmoke; 368 | border: 1px solid grey; 369 | border-radius: 0.25rem; 370 | overflow: hidden; 371 | } 372 | 373 | .date-input-container input[type="date"] { 374 | width: 100%; 375 | padding: 0.375rem 0.75rem; 376 | font-size: 1rem; 377 | line-height: 1.5; 378 | color: green; 379 | background-color: transparent; 380 | border: none; 381 | -webkit-appearance: none; 382 | -moz-appearance: none; 383 | appearance: none; 384 | background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='%2328a745' viewBox='0 0 16 16'%3E%3Cpath d='M3.5 0a.5.5 0 0 1 .5.5V1h8V.5a.5.5 0 0 1 1 0V1h1a2 2 0 0 1 2 2v11a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V3a2 2 0 0 1 2-2h1V.5a.5.5 0 0 1 .5-.5zM1 4v10a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1V4H1z'/%3E%3C/svg%3E"); 385 | background-repeat: no-repeat; 386 | background-position: right 1rem center; 387 | background-size: 16px 16px; 388 | cursor: pointer; 389 | } 390 | .date-input-container input[type="date"]::-webkit-calendar-picker-indicator { 391 | opacity: 1; 392 | background: none; 393 | } 394 | .date-input-container input[type="date"]:focus { 395 | outline: none; 396 | box-shadow: 0 0 0 0.2rem rgba(40, 167, 69, 0.25); 397 | } 398 | 399 | .date-input-container label { 400 | position: absolute; 401 | top: -0.75rem; 402 | left: 5%; 403 | padding: 0 0.25rem; 404 | background-color: #fff; 405 | color: var(--primary-color); 406 | font-size: 0.875rem; 407 | transition: all 0.2s ease-in-out; 408 | font-weight: bold; 409 | } 410 | .logout-btn .btn { 411 | padding: 0; 412 | font-size: inherit; 413 | font-weight: bold; 414 | border-radius: 0; 415 | background-color: transparent; 416 | align-items: center; 417 | height: 100%; 418 | } 419 | 420 | .logout-btn .btn:hover { 421 | background-color: transparent; 422 | color: #000; 423 | } 424 | 425 | .category-bubbles { 426 | display: flex; 427 | flex-wrap: wrap; 428 | gap: 10px; 429 | } 430 | 431 | .category-bubbles input[type="radio"] { 432 | display: none; 433 | } 434 | 435 | .category-bubbles label { 436 | display: inline-block; 437 | padding: 10px 20px; 438 | background-color: #f0f0f0; 439 | border: 2px solid #d0d0d0; 440 | border-radius: 25px; 441 | cursor: pointer; 442 | transition: all 0.3s ease; 443 | font-size: 14px; 444 | box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1); 445 | } 446 | 447 | .category-bubbles label:hover { 448 | background-color: #e0e0e0; 449 | transform: translateY(-2px); 450 | box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15); 451 | } 452 | 453 | .category-bubbles input[type="radio"]:checked + label { 454 | background-color: #007bff; 455 | border-color: #0056b3; 456 | color: white; 457 | } 458 | 459 | .category-bubbles input[type="radio"]:checked + label:hover { 460 | background-color: #0056b3; 461 | } 462 | 463 | 464 | .alert-container { 465 | position: fixed; 466 | top: 50%; 467 | left: 50%; 468 | transform: translate(-50%, -50%); 469 | z-index: 1050; 470 | transition: opacity 0.5s ease-in-out; 471 | opacity: 0; 472 | pointer-events: none; 473 | } 474 | 475 | 476 | .alert { 477 | backdrop-filter: blur(5px); 478 | border-radius: 8px; 479 | margin: 0; 480 | width: 600px; 481 | height: 150px; 482 | display: flex; 483 | justify-content: center; 484 | align-items: center; 485 | text-align: center; 486 | pointer-events: auto; 487 | } 488 | 489 | --------------------------------------------------------------------------------