├── .dockerignore
├── .env-example
├── .gitignore
├── .vscode
├── launch.json
└── settings.json
├── LICENSE.txt
├── README.md
├── SparkyBudget-demo.db
├── SparkyBudget.png
├── SparkyBudget
├── SparkyBudget.py
├── py_db
│ ├── __init__.py
│ ├── db_scripts
│ │ ├── SparkyBudget_DDL.sql
│ │ ├── SparkyBudget_DML.sql
│ │ ├── SparkyBudget_Demo.sql
│ │ └── upgrade
│ │ │ └── SparkyBudget_Upgrade_v0.19.sql
│ └── init_db.py
├── py_routes
│ ├── __init__.py
│ ├── budget_summary.py
│ ├── historical_trend.py
│ ├── home.py
│ └── manage_categories.py
├── py_utils
│ ├── FileToDB.py
│ ├── SimpleFinToDB.py
│ ├── __init__.py
│ ├── auth.py
│ ├── currency_utils.py
│ ├── daily_balance_history.py
│ ├── monthly_budget_insert.py
│ ├── scheduler.py
│ └── subcategory_update.py
├── static
│ ├── Spinner-0.4s-57px.gif
│ ├── Spinner-1s-200px.gif
│ ├── css
│ │ ├── SparkyBudget.css
│ │ ├── balance_details.css
│ │ ├── balance_summary.css
│ │ ├── budget.css
│ │ ├── budget_summary.css
│ │ ├── budget_transaction_details.css
│ │ ├── category.css
│ │ ├── login.css
│ │ ├── navigation_bar.css
│ │ ├── style.css
│ │ └── trend.css
│ ├── images
│ │ ├── Sparky.jpg
│ │ ├── SparkyBudget.png
│ │ ├── budget_icons
│ │ │ ├── ICE Skating.png
│ │ │ ├── Auto Gas.png
│ │ │ ├── Auto Insurance.png
│ │ │ ├── Auto Payment.png
│ │ │ ├── BAP.png
│ │ │ ├── Bank Fee.png
│ │ │ ├── Books.png
│ │ │ ├── CC Rewards.png
│ │ │ ├── Cash Withdrawal.png
│ │ │ ├── Clothing.png
│ │ │ ├── Dance Class.png
│ │ │ ├── Doctor.png
│ │ │ ├── EZ Pass.png
│ │ │ ├── Eating Outside.png
│ │ │ ├── Electronics and Software.png
│ │ │ ├── Entertainment.png
│ │ │ ├── Food and Dining.png
│ │ │ ├── Gas and Electric.png
│ │ │ ├── Groceries.png
│ │ │ ├── Hair.png
│ │ │ ├── Home Applicances.png
│ │ │ ├── Home Improvement.png
│ │ │ ├── Home Insurance.png
│ │ │ ├── Hula Hoop Class.png
│ │ │ ├── ICE Skating.png
│ │ │ ├── India Ticket.png
│ │ │ ├── Interest Income.png
│ │ │ ├── Internet.png
│ │ │ ├── Jewellery.png
│ │ │ ├── Mobile Phone.png
│ │ │ ├── Parking.png
│ │ │ ├── Paycheck.png
│ │ │ ├── Pharmacy.png
│ │ │ ├── Rent.png
│ │ │ ├── Service Fee.png
│ │ │ ├── Shopping.png
│ │ │ ├── Subscription.png
│ │ │ ├── Swimming Class.png
│ │ │ ├── Tamil School.png
│ │ │ ├── Taxi.png
│ │ │ ├── Train.png
│ │ │ └── USMLE.png
│ │ ├── favicon.ico
│ │ ├── homepage.webp
│ │ ├── logout.svg
│ │ ├── money-bill-trend-up-solid.svg
│ │ └── refresh.svg
│ ├── js
│ │ ├── balance_details.js
│ │ ├── balance_summary.js
│ │ ├── budget.js
│ │ ├── budget_summary.js
│ │ ├── budget_transaction_details.js
│ │ ├── category.js
│ │ ├── index.js
│ │ ├── navigation_bar.js
│ │ ├── script.js
│ │ ├── theme.js
│ │ └── trend.js
│ └── manifest.json
└── templates
│ ├── balance_details.html.jinja
│ ├── balance_summary.html.jinja
│ ├── budget.html.jinja
│ ├── budget_summary_chart.html.jinja
│ ├── budget_transaction_details.html.jinja
│ ├── category.html.jinja
│ ├── components
│ └── navigation_bar.html.jinja
│ ├── index.html.jinja
│ ├── login.html.jinja
│ └── trend.html.jinja
├── demo
├── SparkyBudget-Desktop.mp4
└── SparkyBudget-Mobile.mp4
├── docker-compose.yaml
├── dockerfile
├── entrypoint.sh
├── mypy.ini
├── pdm.lock
├── pyproject.toml
├── requirements.txt
└── sparkybudget_unraid.xml
/.dockerignore:
--------------------------------------------------------------------------------
1 | # Ignore Python cache files
2 | __pycache__/
3 | *.py[cod]
4 | *$py.class
5 |
6 | # Ignore virtual environments
7 | .venv/
8 | env/
9 | venv/
10 | ENV/
11 | env.bak/
12 | venv.bak/
13 |
14 | # Ignore build and distribution directories
15 | build/
16 | dist/
17 | .eggs/
18 | *.egg-info/
19 | .installed.cfg
20 | *.egg
21 | wheels/
22 |
23 | # Ignore logs and temporary files
24 | *.log
25 | *.tmp
26 | *.bak
27 | *.old
28 | *.swp
29 |
30 | # Ignore IDE/editor config files
31 | .vscode/
32 | .idea/
33 | .spyderproject/
34 | .spyproject/
35 |
36 | # Ignore OS-generated files
37 | .DS_Store
38 | Thumbs.db
39 |
40 | # Ignore test and coverage files
41 | htmlcov/
42 | .tox/
43 | .nox/
44 | .coverage
45 | .coverage.*
46 | .cache
47 | .pytest_cache/
48 | *.cover
49 | *.py,cover
50 | .hypothesis/
51 |
52 | # Ignore Flask-specific files
53 | instance/
54 | .webassets-cache
55 |
56 | # Ignore database and secrets
57 | **/SparkyBudget.db
58 | containers/data/
59 | **/token.txt
60 | **/access_url.txt
61 | **/*.csv
62 | **/.env
63 | private/
64 |
65 | # Ignore static files that are not needed
66 | static/css/*.map
67 | static/js/*.map
68 |
69 | # Ignore Git files
70 | .git/
71 | .gitignore
72 |
73 | # Ignore Docker-related files
74 | .dockerignore
75 | docker-compose.override.yml
--------------------------------------------------------------------------------
/.env-example:
--------------------------------------------------------------------------------
1 | USE_INTERNAL_HTTPS=0
2 | USE_SECURE_SESSION_COOKIE=0
3 | SPARKY_USER=Sparky
4 | SPARKY_PASS=Sparky
5 | SIMPLEFIN_TOKEN=Your_SimpleFin_Token
6 | LOG_LEVEL=INFO
7 | TZ=America/New_York
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 | *$py.class
5 |
6 | # C extensions
7 | *.so
8 |
9 | # Distribution / packaging
10 | .Python
11 | build/
12 | develop-eggs/
13 | dist/
14 | downloads/
15 | eggs/
16 | .eggs/
17 | lib/
18 | lib64/
19 | parts/
20 | sdist/
21 | var/
22 | wheels/
23 | share/python-wheels/
24 | *.egg-info/
25 | .installed.cfg
26 | *.egg
27 | MANIFEST
28 |
29 | # PyInstaller
30 | # Usually these files are written by a python script from a template
31 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
32 | *.manifest
33 | *.spec
34 |
35 | # Installer logs
36 | pip-log.txt
37 | pip-delete-this-directory.txt
38 |
39 | # Unit test / coverage reports
40 | htmlcov/
41 | .tox/
42 | .nox/
43 | .coverage
44 | .coverage.*
45 | .cache
46 | nosetests.xml
47 | coverage.xml
48 | *.cover
49 | *.py,cover
50 | .hypothesis/
51 | .pytest_cache/
52 | cover/
53 |
54 | # Translations
55 | *.mo
56 | *.pot
57 |
58 | # Django stuff:
59 | *.log
60 | local_settings.py
61 | db.sqlite3
62 | db.sqlite3-journal
63 |
64 | # Flask stuff:
65 | instance/
66 | .webassets-cache
67 |
68 | # Scrapy stuff:
69 | .scrapy
70 |
71 | # Sphinx documentation
72 | docs/_build/
73 |
74 | # PyBuilder
75 | .pybuilder/
76 | target/
77 |
78 | # Jupyter Notebook
79 | .ipynb_checkpoints
80 |
81 | # IPython
82 | profile_default/
83 | ipython_config.py
84 |
85 | # pyenv
86 | # For a library or package, you might want to ignore these files since the code is
87 | # intended to run in multiple environments; otherwise, check them in:
88 | # .python-version
89 |
90 | # pipenv
91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies
93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not
94 | # install all needed dependencies.
95 | #Pipfile.lock
96 |
97 | # poetry
98 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
99 | # This is especially recommended for binary packages to ensure reproducibility, and is more
100 | # commonly ignored for libraries.
101 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
102 | #poetry.lock
103 |
104 | # pdm
105 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
106 | #pdm.lock
107 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
108 | # in version control.
109 | # https://pdm.fming.dev/#use-with-ide
110 | .pdm.toml
111 | .pdm-python
112 |
113 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
114 | __pypackages__/
115 |
116 | # Celery stuff
117 | celerybeat-schedule
118 | celerybeat.pid
119 |
120 | # SageMath parsed files
121 | *.sage.py
122 |
123 | # Environments
124 | **/.env
125 | .venv
126 | env/
127 | venv/
128 | ENV/
129 | env.bak/
130 | venv.bak/
131 |
132 | # Spyder project settings
133 | .spyderproject
134 | .spyproject
135 |
136 | # Rope project settings
137 | .ropeproject
138 |
139 | # mkdocs documentation
140 | /site
141 |
142 | # mypy
143 | .mypy_cache/
144 | .dmypy.json
145 | dmypy.json
146 |
147 | # Pyre type checker
148 | .pyre/
149 |
150 | # pytype static type analyzer
151 | .pytype/
152 |
153 | # Cython debug symbols
154 | cython_debug/
155 |
156 | # PyCharm
157 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
158 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
159 | # and can be added to the global gitignore or merged into this file. For a more nuclear
160 | # option (not recommended) you can uncomment the following to ignore the entire idea folder.
161 | #.idea/
162 |
163 | # dev database
164 | containers/data/
165 |
166 | # secrets for SparkyBudget
167 | **/token.txt
168 | **/access_url.txt
169 | **/*.db
170 | output/
171 | certs/
172 | **/*.csv
173 | .cursor/mcp.json
174 | private/
175 |
--------------------------------------------------------------------------------
/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | // Use IntelliSense to learn about possible attributes.
3 | // Hover to view descriptions of existing attributes.
4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
5 | "version": "0.2.0",
6 | "configurations": [
7 | {
8 | "name": "(HTTP) Python: SparkyBudget app",
9 | "type": "python",
10 | "request": "launch",
11 | "module": "flask",
12 | "cwd": "${workspaceFolder}/SparkyBudget",
13 | "env": {
14 | "FLASK_APP": "app.py",
15 | "FLASK_ENV": "development",
16 | "FLASK_DEBUG": "1" // make sure it is not "0"
17 | },
18 | "args": [
19 | "run",
20 | // "--no-debugger", Comment out this line
21 | // "--no-reload" Comment out this line
22 | ],
23 | "jinja": true,
24 | "justMyCode": true,
25 | "envFile": "${workspaceFolder}/.env"
26 | },
27 | {
28 | "name": "(HTTPS) Python: SparkyBudget app",
29 | "type": "python",
30 | "request": "launch",
31 | "module": "flask",
32 | "cwd": "${workspaceFolder}/SparkyBudget",
33 | "env": {
34 | "FLASK_APP": "app.py",
35 | "FLASK_ENV": "development",
36 | "FLASK_DEBUG": "1" // make sure it is not "0"
37 | },
38 | "args": [
39 | "run",
40 | "--cert=certs/cert.pem",
41 | "--key=certs/key.pem"
42 | // "--no-debugger", Comment out this line
43 | // "--no-reload" Comment out this line
44 | ],
45 | "jinja": true,
46 | "justMyCode": true,
47 | "envFile": "${workspaceFolder}/.env"
48 | }
49 | ]
50 | }
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "python.analysis.autoImportCompletions": true,
3 | "python.languageServer": "Pylance",
4 | "python.missingPackage.severity": "Warning",
5 | "python.terminal.activateEnvInCurrentTerminal": false,
6 | "mypy.dmypyExecutable": "${workspaceFolder}/.venv/bin/dmypy",
7 | "black-formatter.args": [
8 | "--line-length",
9 | "119"
10 | ],
11 | "isort.args": ["--profile", "black"],
12 | "[python]": {
13 | "editor.tabSize": 4,
14 | "editor.insertSpaces": true,
15 | "editor.formatOnSave": true,
16 | "editor.codeActionsOnSave": {
17 | "source.organizeImports": "explicit",
18 | "source.unusedImports": "explicit"
19 | }
20 | },
21 | "python.analysis.diagnosticSeverityOverrides": {
22 | "reportMissingParameterType": "information",
23 | "reportMissingTypeStubs": "information",
24 | "reportUnknownArgumentType": "information",
25 | "reportUnknownMemberType": "information",
26 | "reportUnknownParameterType": "information",
27 | "reportUnknownVariableType": "information"
28 | }
29 | }
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
1 | SparkyBudget: An application for setting up budgets for expenses.
2 | Copyright (C) 2024 CodeWithCJ
3 |
4 | This program is free software: you can redistribute it and/or modify
5 | it under the terms of the GNU Affero General Public License as
6 | published by the Free Software Foundation, either version 3 of the
7 | License, or (at your option) any later version.
8 |
9 | This program is distributed in the hope that it will be useful,
10 | but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | GNU Affero General Public License for more details.
13 |
14 | You should have received a copy of the GNU Affero General Public License
15 | along with this program. If not, see .
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # SparkyBudget
2 |
3 |
4 | **SparkyBudge** is a personal finance management app that helps users track accounts like Checking, Credit Card, and Loan, manage budgets, and analyze spending trends. Its dark-themed interface offers tools for monitoring net cash, setting recurring budgets, and viewing historical financial data. Ideal for anyone seeking to organize their finances with ease.
5 |
6 | 
7 |
8 |
9 |
10 | # 🛠 How to Install?
11 | 1. Create a new directory:
12 | ```
13 | mkdir sparkybudget
14 | cd sparkybudget
15 | ```
16 | 2. Download Docker Compose and .env-example.
17 | ```
18 | wget https://raw.githubusercontent.com/CodeWithCJ/SparkyBudget/refs/heads/main/docker-compose.yaml
19 | wget https://raw.githubusercontent.com/CodeWithCJ/SparkyBudget/refs/heads/main/.env-example
20 | ```
21 | 3. Rename and update DB & the environment file:
22 | ```
23 | mv .env-example .env
24 | nano .env
25 | ```
26 | 4. Pull and start the Docker containers:
27 | ```
28 | docker compose pull && docker compose up -d
29 | ```
30 |
31 |
32 | # 🌍 How to Access?
33 | 📍 Open your browser and go to:
34 | 👉 http://localhost:5050
35 |
36 |
37 | # 📂 Demo Files
38 | 📌 Pass below env variable to "Yes" to auto generate demo accounts & transactions. Backup your DB file if you are existing user.
39 | This will generate dummy accounts & transactions in the DB file.
40 | ```
41 | SPARKY_DEMO=Yes
42 | ```
43 |
44 | # 🔄 How to Reset the Token?
45 | If you need to reset your SimpleFin Token, delete the access_url.txt.
46 | Follow these steps:
47 |
48 | ```
49 | docker exec -it sparkybudget sh
50 | rm /SparkyBudget/access_url.txt
51 | docker-compose down && docker-compose up
52 | ```
53 |
54 |
55 | ⚠️ Important:
56 |
57 | The token can only be used once. You will need to generate a new token from SimpleFin and update it in .env before retrying.
58 |
59 |
60 | # 💬 Need Help?
61 | Refer detailed instrusctions and documentation in Wiki.
62 |
63 | Join our Discord Community for installation support, configuration help, and contributions:
64 | 👉 https://discord.gg/vGjn4b6CVB
65 |
66 |
67 |
68 |
--------------------------------------------------------------------------------
/SparkyBudget-demo.db:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodeWithCJ/SparkyBudget/c4c550580c72e7bcfdb171542351fa2d3a7a73eb/SparkyBudget-demo.db
--------------------------------------------------------------------------------
/SparkyBudget.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodeWithCJ/SparkyBudget/c4c550580c72e7bcfdb171542351fa2d3a7a73eb/SparkyBudget.png
--------------------------------------------------------------------------------
/SparkyBudget/SparkyBudget.py:
--------------------------------------------------------------------------------
1 | import locale, os, logging
2 | from datetime import timedelta
3 | from datetime import datetime
4 | from threading import Lock
5 | from flask import Flask, jsonify
6 | from flask_login import LoginManager, login_required
7 | import dotenv
8 | import os
9 |
10 | # Load from private/.env first (lowest precedence)
11 | dotenv_path_private = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'private', '.env')
12 | dotenv.load_dotenv(dotenv_path_private, override=True)
13 |
14 | # Then load from the default location (main folder)
15 | dotenv.load_dotenv(override=True)
16 |
17 | DATABASE_PATH = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'private', 'db', 'SparkyBudget.db') # Revert to original calculation
18 | PRIVATE_FOLDER_PATH = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'private') # Define private folder path
19 |
20 | # Get log level from environment, default to INFO if not set
21 | log_level = os.getenv("LOG_LEVEL", "INFO").upper()
22 | logging.basicConfig(
23 | level=log_level,
24 | format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
25 | handlers=[
26 | logging.StreamHandler() # Logs to the console
27 | ]
28 | )
29 | logger = logging.getLogger(__name__)
30 |
31 | # py_utils
32 | from py_utils.auth import load_user, login, logout, before_request, unauthorized
33 | from py_utils.currency_utils import app as currency_app
34 | from py_utils.SimpleFinToDB import process_accounts_data
35 | from py_utils.subcategory_update import subcategory_update_bp
36 | from py_utils.FileToDB import file_to_db_bp
37 | from py_db.init_db import initialize_database
38 |
39 |
40 | # py_routes
41 | from py_routes.home import home_bp
42 | from py_routes.budget_summary import budget_sumary_bp
43 | from py_routes.historical_trend import historical_trend_bp
44 | from py_routes.manage_categories import manage_categories_bp
45 |
46 |
47 |
48 |
49 | def create_app():
50 | app = Flask(__name__, template_folder='./templates', static_folder='./static')
51 | app.jinja_env.add_extension("jinja2.ext.loopcontrols")
52 | app.config['DATABASE_PATH'] = DATABASE_PATH
53 | app.config['PRIVATE_DATA_PATH'] = PRIVATE_FOLDER_PATH # Set private data path in config
54 | app.config["PERMANENT_SESSION_LIFETIME"] = timedelta(days=1)
55 | app.config["SESSION_COOKIE_SECURE"] = bool(int(os.getenv("USE_INTERNAL_HTTPS", 0))) or bool(
56 | int(os.getenv("USE_SECURE_SESSION_COOKIE", 1))
57 | )
58 | app.config["SESSION_COOKIE_HTTPONLY"] = True
59 | #app.secret_key = secrets.token_hex(16)
60 | app.secret_key = os.getenv("FLASK_SECRET_KEY", "your-very-secure-key")
61 | login_manager = LoginManager(app)
62 | return app, login_manager
63 |
64 | app, login_manager = create_app()
65 |
66 | # Update Jinja2 environment
67 | app.jinja_env.filters.update(currency_app.jinja_env.filters)
68 |
69 | login_manager.user_loader(load_user)
70 | login_manager.unauthorized_handler(unauthorized)
71 |
72 | app.before_request(lambda: before_request(app))
73 |
74 | app.route("/login", methods=["GET", "POST"])(login)
75 | app.route("/logout", methods=["GET", "POST"])(logout)
76 |
77 | # Register blueprints
78 | app.register_blueprint(home_bp)
79 | app.register_blueprint(budget_sumary_bp)
80 | app.register_blueprint(historical_trend_bp)
81 | app.register_blueprint(manage_categories_bp)
82 | app.register_blueprint(subcategory_update_bp)
83 | app.register_blueprint(file_to_db_bp)
84 |
85 | db_lock = Lock()
86 |
87 | locale.setlocale(locale.LC_ALL, "")
88 |
89 | @app.route("/download_data", methods=["POST"])
90 | @login_required
91 | def download_data():
92 | try:
93 | #private_data_path = current_app.config['PRIVATE_DATA_PATH'] # Get path from config
94 | process_accounts_data(PRIVATE_FOLDER_PATH) # Pass path to function
95 | return jsonify({"success": True, "message": "Sync with Bank Successfully."})
96 | except Exception as e:
97 | return jsonify({"success": False, "message": str(e)})
98 |
99 | # Initialize the database within the application context
100 | with app.app_context():
101 | initialize_database() # Call the initialization function
102 |
103 | if __name__ == "__main__":
104 | ssl_context = None
105 | if bool(int(os.getenv("USE_INTERNAL_HTTPS", 0))):
106 | ssl_context = (r"certs/cert.pem", r"certs/key.pem")
107 |
108 | logger.info(f"SparkyBudget started at {datetime.now()}")
109 | #start_scheduler()
110 | # Run Flask app
111 | app.run(host="0.0.0.0", port=5000, ssl_context=ssl_context, debug=True)
--------------------------------------------------------------------------------
/SparkyBudget/py_db/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodeWithCJ/SparkyBudget/c4c550580c72e7bcfdb171542351fa2d3a7a73eb/SparkyBudget/py_db/__init__.py
--------------------------------------------------------------------------------
/SparkyBudget/py_db/db_scripts/SparkyBudget_DDL.sql:
--------------------------------------------------------------------------------
1 | CREATE TABLE "D_AccountTypes" (
2 | "AccountTypeKey" INTEGER NOT NULL,
3 | "AccountType" TEXT NOT NULL,
4 | "SortOrder" INTEGER NOT NULL,
5 | "HideFromBudget" BOOLEAN DEFAULT 0 COLLATE BINARY,
6 | PRIMARY KEY("AccountTypeKey" AUTOINCREMENT)
7 | );
8 |
9 |
10 | CREATE TABLE "D_Budget" (
11 | "SubCategory" TEXT,
12 | "BudgetAmount" REAL,
13 | PRIMARY KEY("SubCategory")
14 | );
15 | CREATE TABLE "D_Category" (
16 | "SubCategoryKey" INTEGER NOT NULL UNIQUE,
17 | "SubCategory" TEXT UNIQUE,
18 | "Category" TEXT,
19 | PRIMARY KEY("SubCategoryKey" AUTOINCREMENT)
20 | );
21 | CREATE TABLE "D_Category_Rule" (
22 | "RuleKey" INTEGER NOT NULL,
23 | "Default_SubCategory" TEXT NOT NULL,
24 | "Rule_Category" TEXT NOT NULL,
25 | "Rule_Pattern" TEXT NOT NULL,
26 | "Match_Word" TEXT NOT NULL,
27 | PRIMARY KEY("RuleKey" AUTOINCREMENT)
28 | );
29 | CREATE TABLE "F_Balance" (
30 | "AccountKey" INTEGER,
31 | "AccountID" TEXT,
32 | "AccountName" TEXT,
33 | "BalanceDate" DATE,
34 | "Balance" REAL,
35 | "AvailableBalance" REAL,
36 | "OrganizationDomain" TEXT,
37 | "OrganizationName" TEXT,
38 | "OrganizationSFInURL" TEXT,
39 | "DisplayAccountName" TEXT,
40 | AccountTypeKey INTEGER,
41 | PRIMARY KEY("AccountKey" AUTOINCREMENT)
42 | );
43 | CREATE TABLE "F_Balance_History" (
44 | "Date" DATE,
45 | "AccountID" TEXT,
46 | "AccountName" TEXT,
47 | "BalanceDate" DATE,
48 | "Balance" REAL,
49 | "AvailableBalance" REAL,
50 | "OrganizationDomain" TEXT,
51 | "OrganizationName" TEXT,
52 | "OrganizationSFInURL" TEXT,
53 | "DisplayAccountName" TEXT,
54 | AccountTypeKey INTEGER,
55 | PRIMARY KEY("Date","AccountID")
56 | );
57 | CREATE TABLE "F_Budget" (
58 | "BudgetMonth" DATE,
59 | "SubCategory" TEXT,
60 | "BudgetAmount" REAL,
61 | PRIMARY KEY("BudgetMonth","SubCategory")
62 | );
63 | CREATE TABLE "F_Transaction" (
64 | "TransactionKey" INTEGER,
65 | "AccountID" TEXT,
66 | "AccountName" TEXT,
67 | "TransactionID" TEXT,
68 | "TransactionPosted" DATE,
69 | "TransactionAmount" REAL,
70 | "TransactionDescription" TEXT,
71 | "TransactionPayee" TEXT,
72 | "TransactionMemo" TEXT,
73 | "SubCategory" TEXT,
74 | "TransactionPending" TEXT,
75 | "TransactionAmountNew" REAL,
76 | PRIMARY KEY("TransactionKey" AUTOINCREMENT)
77 | );
78 | CREATE TABLE stg_Balance (
79 | AccountID TEXT,
80 | AccountName TEXT,
81 | BalanceDate DATE,
82 | Balance REAL,
83 | AvailableBalance REAL,
84 | OrganizationDomain TEXT,
85 | OrganizationName TEXT,
86 | OrganizationSFInURL TEXT
87 | );
88 | CREATE TABLE "stg_Transaction" (
89 | "AccountID" TEXT,
90 | "AccountName" TEXT,
91 | "TransactionID" TEXT,
92 | "TransactionPosted" DATE,
93 | "TransactionAmount" REAL,
94 | "TransactionDescription" TEXT,
95 | "TransactionPayee" TEXT,
96 | "TransactionMemo" TEXT,
97 | "TransactionPending" TEXT
98 | );
99 |
100 | CREATE TABLE D_DB (DB_VERSION TEXT);
101 |
102 |
103 | CREATE TRIGGER tr_insert_stg_balance
104 | AFTER INSERT ON stg_balance
105 | FOR EACH ROW
106 | BEGIN
107 | -- Insert or replace record in f_balance
108 | INSERT OR REPLACE INTO f_balance (
109 | AccountKey,
110 | AccountID,
111 | AccountName,
112 | BalanceDate,
113 | Balance,
114 | AvailableBalance,
115 | OrganizationDomain,
116 | OrganizationName,
117 | OrganizationSFInURL,
118 | AccountTypeKey,
119 | DisplayAccountName
120 | ) VALUES (
121 | (SELECT AccountKey FROM f_balance WHERE AccountID = NEW.AccountID), -- Use subquery to get existing AccountKey
122 | NEW.AccountID,
123 | NEW.AccountName,
124 | NEW.BalanceDate,
125 | NEW.Balance,
126 | NEW.AvailableBalance,
127 | NEW.OrganizationDomain,
128 | NEW.OrganizationName,
129 | NEW.OrganizationSFInURL,
130 | (SELECT AccountTypeKey FROM f_balance WHERE AccountID = NEW.AccountID), -- Use the existing value of AccountType
131 | (SELECT DisplayAccountName FROM f_balance WHERE AccountID = NEW.AccountID) -- Use the existing value of DisplayAccountName
132 |
133 | );
134 |
135 | -- Remove the specific record from stg_balance after insert
136 | DELETE FROM stg_balance WHERE AccountID = NEW.AccountID;
137 | END
138 | ;
139 |
140 | CREATE TRIGGER tr_insert_stg_transaction
141 | AFTER INSERT ON stg_transaction
142 | FOR EACH ROW
143 | BEGIN
144 | -- Insert or replace record in f_transaction
145 | INSERT OR REPLACE INTO f_transaction (
146 | TransactionKey,
147 | AccountID,
148 | AccountName,
149 | TransactionID,
150 | TransactionPosted,
151 | TransactionAmount,
152 | TransactionDescription,
153 | TransactionPayee,
154 | TransactionMemo,
155 | TransactionPending,
156 | SubCategory
157 | ) VALUES (
158 | (SELECT TransactionKey FROM f_transaction WHERE TransactionID = NEW.TransactionID),
159 | NEW.AccountID,
160 | NEW.AccountName,
161 | NEW.TransactionID,
162 | NEW.TransactionPosted,
163 | NEW.TransactionAmount,
164 | NEW.TransactionDescription,
165 | NEW.TransactionPayee,
166 | NEW.TransactionMemo,
167 | NEW.TransactionPending,
168 | (SELECT SubCategory FROM f_transaction WHERE TransactionID = NEW.TransactionID)
169 | );
170 |
171 | -- Remove the specific record from stg_transaction after insert
172 | DELETE FROM stg_transaction WHERE TransactionID = NEW.TransactionID;
173 |
174 | UPDATE F_Transaction
175 | SET SubCategory = (
176 | SELECT DISTINCT a12.Default_SubCategory
177 | FROM D_Category_Rule a12
178 | WHERE
179 | (LOWER(F_Transaction.TransactionPayee) LIKE '%' || LOWER(a12.Match_Word) || '%'
180 | OR LOWER(a12.Match_Word) LIKE '%' || LOWER(F_Transaction.TransactionPayee) || '%')
181 | AND a12.Rule_Category = 'Payee'
182 | AND a12.Rule_Pattern = 'Contains'
183 | LIMIT 1
184 | )
185 | WHERE EXISTS (
186 | SELECT 1
187 | FROM D_Category_Rule a12
188 | WHERE
189 | (LOWER(F_Transaction.TransactionPayee) LIKE '%' || LOWER(a12.Match_Word) || '%'
190 | OR LOWER(a12.Match_Word) LIKE '%' || LOWER(F_Transaction.TransactionPayee) || '%')
191 | AND a12.Rule_Category = 'Payee'
192 | AND a12.Rule_Pattern = 'Contains'
193 | )
194 | AND SubCategory IS NULL;
195 |
196 | END
197 | ;
198 |
199 |
200 |
--------------------------------------------------------------------------------
/SparkyBudget/py_db/db_scripts/upgrade/SparkyBudget_Upgrade_v0.19.sql:
--------------------------------------------------------------------------------
1 | DROP TRIGGER IF EXISTS tr_insert_stg_transaction;
2 | CREATE TRIGGER tr_insert_stg_transaction
3 | AFTER INSERT ON stg_transaction
4 | FOR EACH ROW
5 | BEGIN
6 | -- Insert or replace record in f_transaction
7 | INSERT OR REPLACE INTO f_transaction (
8 | TransactionKey,
9 | AccountID,
10 | AccountName,
11 | TransactionID,
12 | TransactionPosted,
13 | TransactionAmount,
14 | TransactionDescription,
15 | TransactionPayee,
16 | TransactionMemo,
17 | TransactionPending,
18 | SubCategory,
19 | TransactionAmountNew -- Add TransactionAmountNew here
20 | ) VALUES (
21 | (SELECT TransactionKey FROM f_transaction WHERE TransactionID = NEW.TransactionID),
22 | NEW.AccountID,
23 | NEW.AccountName,
24 | NEW.TransactionID,
25 | NEW.TransactionPosted,
26 | NEW.TransactionAmount,
27 | NEW.TransactionDescription,
28 | NEW.TransactionPayee,
29 | NEW.TransactionMemo,
30 | NEW.TransactionPending,
31 | (SELECT SubCategory FROM f_transaction WHERE TransactionID = NEW.TransactionID),
32 | (SELECT TransactionAmountNew FROM f_transaction WHERE TransactionID = NEW.TransactionID) -- Select existing TransactionAmountNew
33 | );
34 |
35 | -- Remove the specific record from stg_transaction after insert
36 | DELETE FROM stg_transaction WHERE TransactionID = NEW.TransactionID;
37 |
38 | UPDATE F_Transaction
39 | SET SubCategory = (
40 | SELECT DISTINCT a12.Default_SubCategory
41 | FROM D_Category_Rule a12
42 | WHERE
43 | (LOWER(F_Transaction.TransactionPayee) LIKE '%' || LOWER(a12.Match_Word) || '%'
44 | OR LOWER(a12.Match_Word) LIKE '%' || LOWER(F_Transaction.TransactionPayee) || '%')
45 | AND a12.Rule_Category = 'Payee'
46 | AND a12.Rule_Pattern = 'Contains'
47 | LIMIT 1
48 | )
49 | WHERE EXISTS (
50 | SELECT 1
51 | FROM D_Category_Rule a12
52 | WHERE
53 | (LOWER(F_Transaction.TransactionPayee) LIKE '%' || LOWER(a12.Match_Word) || '%'
54 | OR LOWER(a12.Match_Word) LIKE '%' || LOWER(F_Transaction.TransactionPayee) || '%')
55 | AND a12.Rule_Category = 'Payee'
56 | AND a12.Rule_Pattern = 'Contains'
57 | )
58 | AND SubCategory IS NULL;
59 |
60 | END
61 | ;
62 |
63 |
64 | update D_DB set DB_VERSION = "v0.19";
--------------------------------------------------------------------------------
/SparkyBudget/py_db/init_db.py:
--------------------------------------------------------------------------------
1 | import os
2 | import sqlite3
3 | import logging
4 | from flask import current_app
5 |
6 | logger = logging.getLogger(__name__)
7 |
8 | def initialize_database():
9 | """
10 | Checks if the database exists and initializes/upgrades it if necessary.
11 | """
12 | db_dir = os.path.dirname(current_app.config['DATABASE_PATH'])
13 | if not os.path.exists(db_dir):
14 | os.makedirs(db_dir)
15 |
16 | db_exists = os.path.exists(current_app.config['DATABASE_PATH'])
17 | conn = None
18 | current_version = None # Initialize current_version outside try block
19 |
20 | try:
21 | conn = sqlite3.connect(current_app.config['DATABASE_PATH'])
22 | cursor = conn.cursor()
23 |
24 | if not db_exists:
25 | logger.info("Database file not found. Initializing database...")
26 | # Get the directory of the current file (init_db.py)
27 | current_dir = os.path.dirname(__file__)
28 | db_scripts_dir = os.path.join(current_dir, 'db_scripts')
29 |
30 | # Read and execute DDL script
31 | ddl_script_path = os.path.join(db_scripts_dir, 'SparkyBudget_DDL.sql')
32 | if os.path.exists(ddl_script_path):
33 | with open(ddl_script_path, 'r') as f:
34 | sql_script_ddl = f.read()
35 | cursor.executescript(sql_script_ddl)
36 | logger.info("DDL script executed.")
37 | else:
38 | logger.error(f"DDL script not found at {ddl_script_path}")
39 | raise FileNotFoundError(f"DDL script not found at {ddl_script_path}")
40 |
41 |
42 | # Read and execute DML script
43 | dml_script_path = os.path.join(db_scripts_dir, 'SparkyBudget_DML.sql')
44 | if os.path.exists(dml_script_path):
45 | with open(dml_script_path, 'r') as f:
46 | sql_script_dml = f.read()
47 | cursor.executescript(sql_script_dml)
48 | logger.info("DML script executed.")
49 | else:
50 | logger.warning(f"DML script not found at {dml_script_path}. Skipping DML execution.")
51 |
52 |
53 | # Check for SPARKY_DEMO environment variable and execute demo script if set to 'Yes'
54 | sparky_demo = os.getenv("SPARKY_DEMO")
55 | if sparky_demo and sparky_demo.lower() == 'yes':
56 | logger.info("SPARKY_DEMO is set to 'Yes'. Executing demo data script...")
57 | demo_script_path = os.path.join(db_scripts_dir, 'SparkyBudget_Demo.sql')
58 | if os.path.exists(demo_script_path):
59 | with open(demo_script_path, 'r') as f:
60 | sql_script_demo = f.read()
61 | cursor.executescript(sql_script_demo)
62 | logger.info("Demo data script executed successfully.")
63 | else:
64 | logger.warning(f"Demo data script not found at {demo_script_path}")
65 |
66 | conn.commit()
67 | logger.info("Database initialized successfully.")
68 |
69 | else:
70 | logger.info("Database file found. Checking version...")
71 | try:
72 | # Check if D_DB table exists
73 | cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='D_DB'")
74 | if cursor.fetchone():
75 | # Table exists, get version
76 | cursor.execute("SELECT DB_VERSION FROM D_DB ORDER BY rowid DESC LIMIT 1")
77 | version_row = cursor.fetchone()
78 | if version_row:
79 | current_version = version_row[0]
80 | logger.info(f"Existing database version: {current_version}")
81 | else:
82 | # Table exists but no version found, assume old and set to v0.17
83 | logger.warning("D_DB table found but no version entry. Assuming old version and setting to v0.17")
84 | cursor.execute("INSERT INTO D_DB (DB_VERSION) VALUES (?)", ('v0.17',))
85 | conn.commit()
86 | current_version = 'v0.17'
87 | logger.info(f"Database version set to {current_version}")
88 | else:
89 | # D_DB table does not exist, assume very old and set to v0.17
90 | logger.warning("D_DB table not found. Assuming very old version and setting to v0.17")
91 | cursor.execute("CREATE TABLE D_DB (DB_VERSION TEXT)")
92 | cursor.execute("INSERT INTO D_DB (DB_VERSION) VALUES (?)", ('v0.17',))
93 | conn.commit()
94 | current_version = 'v0.17'
95 | logger.info(f"Database version set to {current_version}")
96 |
97 | except Exception as e:
98 | logger.error(f"Error checking database version: {e}")
99 | # If we can't get the version, we can't upgrade safely.
100 | raise # Stop here if version check fails
101 |
102 | # Now handle upgrades if current_version is determined (moved outside the else block)
103 | if current_version:
104 | logger.info("Checking for database upgrades...")
105 | upgrade_scripts_dir = os.path.join(os.path.dirname(__file__), 'db_scripts', 'upgrade')
106 | if os.path.exists(upgrade_scripts_dir):
107 | upgrade_files = [f for f in os.listdir(upgrade_scripts_dir) if f.endswith('.sql')]
108 |
109 | # Filter and sort upgrade files
110 | def extract_version(filename):
111 | """Extracts the version string (e.g., '0.14', '1') from the filename."""
112 | name_without_ext = filename.replace('.sql', '')
113 | # Find the last occurrence of '_v'
114 | v_index = name_without_ext.rfind('_v')
115 | if v_index != -1:
116 | # Return the part after '_v'
117 | return name_without_ext[v_index + 2:]
118 | return None
119 |
120 | def version_tuple(version_str):
121 | """Converts a version string (e.g., 'v0.14', '0.14', 'v1') to a tuple of integers for comparison."""
122 | if version_str:
123 | # Remove leading 'v' if it exists
124 | if version_str.startswith('v'):
125 | version_str = version_str[1:]
126 | try:
127 | # Split by '.' and convert to integers
128 | return tuple(map(int, version_str.split('.')))
129 | except ValueError:
130 | logger.warning(f"Invalid version number format in string: {version_str}")
131 | return (0,) # Handle invalid version strings
132 | logger.warning(f"Missing version string.")
133 | return (0,) # Handle None or empty strings
134 |
135 | current_version_tuple = version_tuple(current_version)
136 | logger.debug(f"Current version tuple: {current_version_tuple}")
137 |
138 | relevant_upgrade_files = []
139 | for filename in upgrade_files:
140 | logger.debug(f"Processing upgrade file: {filename}")
141 | file_version_str = extract_version(filename)
142 | if file_version_str:
143 | file_version_tuple = version_tuple(file_version_str) # Use the corrected version_tuple
144 | logger.debug(f"File: {filename}, Extracted version string: {file_version_str}, Version tuple: {file_version_tuple}")
145 | if file_version_tuple > current_version_tuple:
146 | logger.debug(f"File version {file_version_tuple} is newer than current version {current_version_tuple}. Adding to relevant files.")
147 | relevant_upgrade_files.append((file_version_tuple, filename))
148 | else:
149 | logger.debug(f"File version {file_version_tuple} is not newer than current version {current_version_tuple}. Skipping.")
150 | else:
151 | logger.warning(f"Could not extract version from filename: {filename}. Skipping.")
152 |
153 | relevant_upgrade_files.sort() # Sorts by version tuple
154 |
155 | if relevant_upgrade_files:
156 | logger.info(f"Found {len(relevant_upgrade_files)} upgrade scripts to execute.")
157 | for version_tuple, filename in relevant_upgrade_files:
158 | script_path = os.path.join(upgrade_scripts_dir, filename)
159 | logger.info(f"Executing upgrade script: {filename}")
160 | try:
161 | with open(script_path, 'r') as f:
162 | sql_script = f.read()
163 | cursor.executescript(sql_script)
164 | conn.commit()
165 |
166 |
167 | except Exception as e:
168 | logger.error(f"Error executing upgrade script {filename}: {e}")
169 | conn.rollback() # Rollback changes from this script
170 | raise # Stop upgrade process on error
171 |
172 | logger.info("Database upgrade process completed.")
173 | else:
174 | logger.info("No database upgrade scripts found or none are newer than the current version.")
175 | else:
176 | logger.warning(f"Upgrade scripts directory not found at {upgrade_scripts_dir}. Skipping upgrade check.")
177 |
178 |
179 | except Exception as e:
180 | logger.error(f"An error occurred during database initialization or upgrade: {e}")
181 | if conn:
182 | conn.rollback() # Ensure any pending transaction is rolled back
183 | # Re-raise the exception to indicate failure
184 | raise
185 | finally:
186 | if conn:
187 | conn.close()
--------------------------------------------------------------------------------
/SparkyBudget/py_routes/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodeWithCJ/SparkyBudget/c4c550580c72e7bcfdb171542351fa2d3a7a73eb/SparkyBudget/py_routes/__init__.py
--------------------------------------------------------------------------------
/SparkyBudget/py_routes/home.py:
--------------------------------------------------------------------------------
1 | #py_routes/home.py
2 |
3 | import sqlite3, os, logging
4 | from flask import Blueprint, render_template, jsonify, request, current_app
5 | from flask_login import login_required
6 | from datetime import datetime
7 |
8 |
9 | log_level = os.getenv("LOG_LEVEL", "INFO").upper()
10 | logger = logging.getLogger(__name__)
11 |
12 | home_bp = Blueprint('home', __name__)
13 |
14 | @home_bp.route('/')
15 | @login_required
16 | def index():
17 | # Connect to the SQLite database
18 | conn = sqlite3.connect(
19 | current_app.config['DATABASE_PATH'], detect_types=sqlite3.PARSE_DECLTYPES | sqlite3.PARSE_COLNAMES
20 | ) # Replace with the actual name of your SQLite database file
21 | cursor = conn.cursor()
22 | # Fetch data for the first table: OrganizationName, sum(Balance), sum(AvailableBalance)
23 | cursor.execute(
24 | """
25 | SELECT AccountType,TotalBalance,TotalAvailableBalance FROM (
26 | SELECT
27 | AccountType,
28 | SortOrder,
29 | ROUND(SUM(Balance), 2) AS TotalBalance,
30 | ROUND(SUM(COALESCE(NULLIF(NULLIF(AvailableBalance, 0), ''), Balance)), 2) AS TotalAvailableBalance
31 | FROM
32 | F_Balance a11
33 | LEFT JOIN D_AccountTypes a12
34 | ON a11.AccountTypeKey = a12.AccountTypeKey
35 | GROUP BY
36 | AccountType,
37 | SortOrder
38 |
39 | UNION ALL
40 |
41 | SELECT
42 | 'Net Cash' AS AccountType,
43 | -1 AS SortOrder,
44 | ROUND(SUM(Balance), 2) AS TotalBalance,
45 | ROUND(SUM(COALESCE(NULLIF(NULLIF(AvailableBalance, 0), ''), Balance)), 2) AS TotalAvailableBalance
46 | FROM
47 | F_Balance a11
48 | LEFT JOIN D_AccountTypes a12
49 | ON a11.AccountTypeKey = a12.AccountTypeKey
50 | WHERE AccountType IN ('Checking', 'Savings', 'Credit Card')
51 |
52 | UNION ALL
53 |
54 | SELECT
55 | 'Net Worth' AS AccountType,
56 | 1000 AS SortOrder,
57 | ROUND(SUM(Balance), 2) AS TotalBalance,
58 | ROUND(SUM(COALESCE(NULLIF(NULLIF(AvailableBalance, 0), ''), Balance)), 2) AS TotalAvailableBalance
59 | FROM
60 | F_Balance a11
61 | LEFT JOIN D_AccountTypes a12
62 | ON a11.AccountTypeKey = a12.AccountTypeKey
63 | )
64 | ORDER BY
65 | SortOrder;
66 | """
67 | )
68 | account_type_data = cursor.fetchall()
69 | labels = list(zip(*account_type_data))[0]
70 | balances = list(zip(*account_type_data))[1]
71 | cursor.execute(
72 | """
73 | SELECT
74 | AccountType,
75 | Coalesce(DisplayAccountName, AccountName) as AccountName,
76 | ROUND(SUM(Balance), 2) AS TotalBalance,
77 | ROUND(SUM(COALESCE(NULLIF(NULLIF(AvailableBalance, 0), ''), Balance)), 2) AS TotalAvailableBalance,
78 | ABS(SUM(Balance)) + ABS(SUM(AvailableBalance)) as Dummy
79 | FROM
80 | F_Balance a11
81 | LEFT JOIN D_AccountTypes a12
82 | ON a11.AccountTypeKey = a12.AccountTypeKey
83 | WHERE AccountType not in ('Hide')
84 | GROUP BY
85 | AccountType,
86 | Coalesce(DisplayAccountName, AccountName)
87 | ORDER BY
88 | SortOrder,
89 | 5 desc
90 | """
91 | )
92 | account_type_banak_data = cursor.fetchall()
93 | # Fetch data for the second table: OrganizationName, AccountName, FormattedBalanceDate, Balance, AvailableBalance
94 | cursor.execute(
95 | """
96 | SELECT AccountKey,
97 | COALESCE(AccountType,'UNKNOWN'),
98 | OrganizationName,
99 | Coalesce(DisplayAccountName,AccountName) as AccountName,
100 | DATE(BalanceDate) AS "[date]",
101 | ROUND(Balance, 2) AS Balance,
102 | ROUND(AvailableBalance, 2) AS AvailableBalance
103 | FROM F_Balance a11
104 | LEFT JOIN D_AccountTypes a12
105 | ON a11.AccountTypeKey = a12.AccountTypeKey
106 | ORDER BY COALESCE(SortOrder,999),OrganizationName, Coalesce(DisplayAccountName, AccountName)
107 | """
108 | )
109 | bank_account_name_balance_details = cursor.fetchall()
110 | # Fetch distinct Transaction Years and Months for filters
111 | cursor.execute(
112 | 'SELECT DISTINCT CAST(strftime("%Y", TransactionPosted) AS INTEGER) AS Year FROM F_Transaction ORDER BY Year DESC'
113 | )
114 | transaction_years = [row[0] for row in cursor.fetchall()]
115 | cursor.execute('SELECT DISTINCT strftime("%m", TransactionPosted) AS Month FROM F_Transaction ORDER BY Month DESC')
116 | transaction_months = [row[0] for row in cursor.fetchall()]
117 | # Query for daily balance data (the one you need to display)
118 | daily_balance_query = """
119 | SELECT
120 | AccountType,
121 | COALESCE(DisplayAccountName, AccountName) AS AccountName,
122 | Date,
123 | ROUND(SUM(CASE WHEN COALESCE(AvailableBalance, 0) <> 0 THEN AvailableBalance ELSE Balance END), 0) AS DailyBalance
124 | FROM
125 | F_Balance_History a11
126 | LEFT JOIN D_AccountTypes a12
127 | ON a11.AccountTypeKey = a12.AccountTypeKey
128 | WHERE
129 | (COALESCE(AvailableBalance, 0) <> 0 OR COALESCE(Balance, 0) <> 0)
130 | GROUP BY
131 | AccountType, COALESCE(DisplayAccountName, AccountName), Date
132 | ORDER BY
133 | Date, AccountType, COALESCE(DisplayAccountName, AccountName) ASC
134 | """
135 | # Execute the daily balance query
136 | cursor.execute(daily_balance_query)
137 | daily_balance_data = cursor.fetchall()
138 | # Close the database connection
139 | conn.close()
140 | # Render the template with the fetched data and filters, including transaction_years
141 | return render_template(
142 | "index.html.jinja",
143 | account_type_data=account_type_data,
144 | account_type_banak_data=account_type_banak_data,
145 | bank_account_name_balance_details=bank_account_name_balance_details,
146 | transaction_years=transaction_years,
147 | transaction_months=transaction_months,
148 | #now=datetime.now(timezone.utc),
149 | now = datetime.now(),
150 | labels=labels,
151 | balances=balances,
152 | daily_balance_data=daily_balance_data # Add the new daily balance data
153 | )
154 |
155 |
156 | # In app.py
157 |
158 | # Route to fetch all AccountTypes for the dropdown (unchanged)
159 | @home_bp.route("/get_account_types", methods=["GET"])
160 | @login_required
161 | def get_account_types():
162 | try:
163 | conn = sqlite3.connect(current_app.config['DATABASE_PATH'], detect_types=sqlite3.PARSE_DECLTYPES | sqlite3.PARSE_COLNAMES)
164 | cursor = conn.cursor()
165 | cursor.execute("SELECT AccountTypeKey, AccountType FROM D_AccountTypes ORDER BY AccountType")
166 | account_types = cursor.fetchall()
167 | conn.close()
168 | return jsonify({"success": True, "account_types": [{"key": row[0], "type": row[1]} for row in account_types]})
169 | except Exception as e:
170 | logger.error(f"Error fetching account types: {str(e)}", exc_info=True)
171 | return jsonify({"success": False, "message": str(e)})
172 |
173 |
174 |
175 | @home_bp.route("/addAccount", methods=["POST"])
176 | @login_required
177 | def add_account():
178 | try:
179 | data = request.get_json()
180 | logger.info(f"Adding new account: {data}")
181 |
182 | query = """
183 | INSERT INTO F_Balance (
184 | AccountName, Balance, AvailableBalance, OrganizationDomain,
185 | OrganizationName, OrganizationSFInURL, BalanceDate, AccountTypeKey
186 | ) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
187 | """
188 | values = (
189 | data['accountName'],
190 | data['balance'],
191 | data['availableBalance'],
192 | data['organizationDomain'],
193 | data['organizationName'],
194 | None, # OrganizationSFInURL is not provided
195 | data['balanceDate'],
196 | data['accountTypeKey']
197 | )
198 |
199 | conn = sqlite3.connect(current_app.config['DATABASE_PATH'])
200 | cursor = conn.cursor()
201 | cursor.execute(query, values)
202 | conn.commit()
203 | conn.close()
204 |
205 | logger.info("Account added successfully.")
206 | return jsonify({"success": True, "message": "Account added successfully."})
207 |
208 | except Exception as e:
209 | logger.error(f"Error adding account: {str(e)}", exc_info=True)
210 | return jsonify({"success": False, "message": str(e)}), 400
211 |
212 |
213 |
214 |
215 | # Consolidated route to update all fields for an account
216 | @home_bp.route("/update_account", methods=["POST"])
217 | @login_required
218 | def update_account():
219 | try:
220 | data = request.get_json()
221 | logger.debug("Received data for update_account:", data)
222 |
223 | account_key = data.get("account_key")
224 | account_type_key = data.get("account_type_key")
225 | display_account_name = data.get("display_account_name")
226 | balance_date = data.get("balance_date")
227 | balance = data.get("balance")
228 | available_balance = data.get("available_balance")
229 |
230 | # Validate required fields
231 | if account_key is None:
232 | raise ValueError("account_key must be provided")
233 | if account_type_key is None:
234 | raise ValueError("account_type_key must be provided")
235 |
236 | # Convert account_key to integer
237 | try:
238 | account_key = int(account_key)
239 | except (ValueError, TypeError):
240 | logger.error("Invalid account_key type:", account_key, exc_info=True)
241 | raise ValueError("account_key must be a valid integer")
242 |
243 | # Handle display_account_name: convert empty string to None
244 | if display_account_name is not None and display_account_name.strip() == '':
245 | display_account_name = None
246 | logger.debug("Converted empty string to None for DisplayAccountName")
247 |
248 | # Validate and convert numeric fields
249 | balance = float(balance) if balance is not None else 0.0
250 | available_balance = float(available_balance) if available_balance is not None else 0.0
251 |
252 | # Validate balance_date format (YYYY-MM-DD) if provided
253 | if balance_date:
254 | try:
255 | datetime.strptime(balance_date, '%Y-%m-%d')
256 | except ValueError:
257 | raise ValueError("balance_date must be in YYYY-MM-DD format")
258 |
259 | conn = sqlite3.connect(current_app.config['DATABASE_PATH'], detect_types=sqlite3.PARSE_DECLTYPES | sqlite3.PARSE_COLNAMES)
260 | cursor = conn.cursor()
261 |
262 | # Update all fields in a single query
263 | cursor.execute(
264 | """
265 | UPDATE F_Balance
266 | SET AccountTypeKey = ?,
267 | DisplayAccountName = ?,
268 | BalanceDate = ?,
269 | Balance = ?,
270 | AvailableBalance = ?
271 | WHERE AccountKey = ?
272 | """,
273 | (account_type_key, display_account_name, balance_date, balance, available_balance, account_key)
274 | )
275 |
276 | logger.debug(f"Rows updated: {cursor.rowcount}")
277 |
278 | # Fetch the updated row to return to the frontend
279 | cursor.execute(
280 | """
281 | SELECT AccountKey,
282 | COALESCE(AccountType,'UNKNOWN'),
283 | OrganizationName,
284 | Coalesce(DisplayAccountName,AccountName) as AccountName,
285 | DATE(BalanceDate) AS "[date]",
286 | ROUND(Balance, 2) AS Balance,
287 | ROUND(AvailableBalance, 2) AS AvailableBalance
288 | FROM F_Balance a11
289 | LEFT JOIN D_AccountTypes a12
290 | ON a11.AccountTypeKey = a12.AccountTypeKey
291 | WHERE AccountKey = ?
292 | """,
293 | (account_key,)
294 | )
295 | updated_row = cursor.fetchone()
296 |
297 | conn.commit()
298 | conn.close()
299 |
300 | if cursor.rowcount == 0:
301 | return jsonify({"success": False, "message": "No rows updated. AccountKey not found."}), 400
302 |
303 | return jsonify({
304 | "success": True,
305 | "message": "Account updated successfully.",
306 | "updated_row": updated_row
307 | })
308 |
309 | except Exception as e:
310 | logger.error(f"Error in update_account: {str(e)}", exc_info=True)
311 | if 'conn' in locals():
312 | conn.close()
313 | return jsonify({"success": False, "message": str(e)}), 400
--------------------------------------------------------------------------------
/SparkyBudget/py_utils/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodeWithCJ/SparkyBudget/c4c550580c72e7bcfdb171542351fa2d3a7a73eb/SparkyBudget/py_utils/__init__.py
--------------------------------------------------------------------------------
/SparkyBudget/py_utils/auth.py:
--------------------------------------------------------------------------------
1 | #py_utils/auth.py
2 | import os
3 | import logging # Ensure logging is imported
4 | from flask import request, redirect, url_for, session, render_template
5 | from flask_login import login_user, logout_user, current_user, UserMixin
6 | from datetime import timedelta # Ensure this line is included
7 |
8 | # Configure logging
9 | logging.basicConfig(level=logging.INFO)
10 | logger = logging.getLogger(__name__)
11 |
12 | # Simple hardcoded user class (replace with your user model if you have one)
13 | class User(UserMixin):
14 | def __init__(self, user_id):
15 | self.id = user_id
16 |
17 | sparky_username = os.getenv("SPARKY_USER", "Sparky")
18 | sparky_password = os.getenv("SPARKY_PASS", "Sparky")
19 |
20 | def load_user(user_id):
21 | return User(user_id)
22 |
23 | def login():
24 | logger.debug("Login route reached.")
25 | if request.method == "POST":
26 | username = request.form["username"]
27 | password = request.form["password"]
28 | if username == sparky_username and password == sparky_password:
29 | user = User(username)
30 | login_user(user)
31 | session["user"] = sparky_username
32 | session.permanent = True # Make the session permanent
33 | logger.debug(f"User {username} successfully logged in.")
34 | return redirect(url_for("home.index")) # Correct the endpoint reference
35 | return render_template("login.html.jinja")
36 |
37 | def logout():
38 | logger.debug("Logging out user.")
39 | logout_user()
40 | return redirect(url_for("login"))
41 |
42 | def before_request(app):
43 | session.permanent = True
44 | app.permanent_session_lifetime = timedelta(days=1) # Ensure this line is included
45 | if current_user.is_authenticated:
46 | session["user"] = current_user.id
47 |
48 | def unauthorized():
49 | logger.warning("Unauthorized access attempt.")
50 | # Redirect the user to the login page when unauthorized
51 | return redirect(url_for("login"))
52 |
--------------------------------------------------------------------------------
/SparkyBudget/py_utils/currency_utils.py:
--------------------------------------------------------------------------------
1 | #py_utils/currency_utils.py
2 |
3 | import locale
4 | from flask import Flask
5 |
6 | app = Flask(__name__)
7 |
8 | def format_money(number):
9 | if number is None:
10 | return "--"
11 | else:
12 | rounded_number = round(number, 2) # Round to two decimal places
13 | # Format the number with a $ sign and handle negative values
14 | if rounded_number < 0:
15 | return f"-${abs(rounded_number):,}" # Add $ after the negative sign
16 | return f"${rounded_number:,}" # Format for positive numbers
17 |
18 | def format_money_whole(number):
19 | if number is None:
20 | return "--"
21 | else:
22 | rounded_number = round(number) # Round to whole number
23 | # Format the number with a $ sign and handle negative values
24 | if rounded_number < 0:
25 | return f"-${abs(rounded_number):,}" # Add $ after the negative sign
26 | return f"${rounded_number:,}" # Format for positive numbers
27 |
28 | def format_currency(value):
29 | # Convert value to float
30 | value = float(value)
31 |
32 | # Check if the value is negative
33 | is_negative = value < 0
34 |
35 | # Format the value as currency without parentheses
36 | formatted_value = locale.currency(abs(value), grouping=True)
37 |
38 | # Add the negative sign if necessary
39 | if is_negative:
40 | formatted_value = "-" + formatted_value
41 |
42 | return formatted_value
43 |
44 | # Register custom filters with the Jinja2 environment
45 | app.jinja_env.filters['tocurrency'] = format_money
46 | app.jinja_env.filters['tocurrency_whole'] = format_money_whole
--------------------------------------------------------------------------------
/SparkyBudget/py_utils/daily_balance_history.py:
--------------------------------------------------------------------------------
1 | #py_utils/daily_balance_history.py
2 |
3 | import sqlite3, os, logging
4 | from datetime import datetime
5 |
6 | log_level = os.getenv("LOG_LEVEL", "INFO").upper()
7 | logging.basicConfig(
8 | level=log_level,
9 | format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
10 | handlers=[
11 | logging.StreamHandler() # Logs to the console
12 | ]
13 | )
14 | logger = logging.getLogger(__name__)
15 |
16 |
17 | def daily_balance_history_insert(db_path):
18 | logger.info(f"daily_balance_history_insert executed at {datetime.now()}")
19 | from SparkyBudget import db_lock # Import the db_lock from your app or shared module
20 | with db_lock: # Synchronize database access
21 | try:
22 | # Connect to the SQLite database with the 'with' statement to auto-close the connection
23 | with sqlite3.connect(db_path) as conn:
24 | cursor = conn.cursor()
25 |
26 | # Insert new records from F_Balance into F_Balance_History
27 | cursor.execute(
28 | """
29 | INSERT INTO F_Balance_History (
30 | Date, AccountID, AccountName, BalanceDate, Balance, AvailableBalance,
31 | OrganizationDomain, OrganizationName, OrganizationSFInURL,
32 | DisplayAccountName, AccountTypeKey
33 | )
34 | SELECT
35 | DATE('now'), -- Current date
36 | AccountID,
37 | AccountName,
38 | BalanceDate,
39 | Balance,
40 | AvailableBalance,
41 | OrganizationDomain,
42 | OrganizationName,
43 | OrganizationSFInURL,
44 | DisplayAccountName,
45 | AccountTypeKey
46 | FROM F_Balance
47 | """
48 | )
49 |
50 | # Commit the changes to the database (with 'with' statement, this is handled automatically)
51 | conn.commit()
52 |
53 | except sqlite3.Error as e:
54 | logger.error(f"An error occurred: {e}")
55 |
56 | # Call the function
57 | #daily_balance_history_insert()
58 |
--------------------------------------------------------------------------------
/SparkyBudget/py_utils/monthly_budget_insert.py:
--------------------------------------------------------------------------------
1 | #py_utils/monthly_budget_insert.py
2 |
3 | import sqlite3, os, logging
4 | from datetime import datetime
5 |
6 |
7 | # Get log level from environment, default to INFO if not set
8 | log_level = os.getenv("LOG_LEVEL", "INFO").upper()
9 | logger = logging.getLogger(__name__)
10 |
11 |
12 |
13 | def month_budget_update_using_template(db_path):
14 | logger.info(f"month_budget_update_using_template executed at {datetime.now()}")
15 | from SparkyBudget import db_lock # Import the db_lock from your app or shared module
16 | # Ensure that only one thread/process can access the DB at a time
17 | with db_lock:
18 | # Use the `with` statement to manage the SQLite connection and cursor
19 | with sqlite3.connect(db_path) as conn:
20 | cursor = conn.cursor()
21 |
22 | # Check if it's the first day of the month
23 | if datetime.now().day == 1:
24 | # Insert new records from D_Budget into F_Budget
25 | cursor.execute(
26 | """
27 | INSERT OR IGNORE INTO F_Budget ("BudgetMonth", "SubCategory", "BudgetAmount")
28 | SELECT DATE('now', 'start of month') AS "BudgetMonth", "SubCategory", "BudgetAmount"
29 | FROM D_Budget
30 | """
31 | )
32 |
33 | # Uncomment and update this section if needed
34 | # Update existing records in F_Budget if SubCategory already exists for the current month
35 | # cursor.execute("""
36 | # UPDATE F_Budget
37 | # SET "BudgetAmount" = (SELECT "BudgetAmount" FROM D_Budget WHERE F_Budget."SubCategory" = D_Budget."SubCategory"),
38 | # "BudgetMonth" = DATE('now', 'start of month')
39 | # WHERE EXISTS (
40 | # SELECT 1
41 | # FROM D_Budget
42 | # WHERE F_Budget."SubCategory" = D_Budget."SubCategory"
43 | # ) AND "BudgetMonth" = DATE('now', 'start of month')
44 | # """)
45 |
46 | # Commit the changes to the database
47 | conn.commit()
48 |
49 | # The connection and cursor are automatically closed at the end of the `with` block
50 |
--------------------------------------------------------------------------------
/SparkyBudget/py_utils/scheduler.py:
--------------------------------------------------------------------------------
1 | import sys
2 | import os
3 | import schedule
4 | import time
5 | import threading
6 | from datetime import datetime
7 | import logging
8 |
9 | log_level = os.getenv("LOG_LEVEL", "INFO").upper()
10 | logging.basicConfig(
11 | level=log_level,
12 | format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
13 | handlers=[
14 | logging.StreamHandler() # Logs to the console
15 | ]
16 | )
17 | logger = logging.getLogger(__name__)
18 |
19 | current_dir = os.path.dirname(os.path.abspath(__file__))
20 | parent_dir = os.path.abspath(os.path.join(current_dir, ".."))
21 | sys.path.insert(0, parent_dir)
22 |
23 | from py_utils.monthly_budget_insert import month_budget_update_using_template
24 | from py_utils.daily_balance_history import daily_balance_history_insert
25 | from py_utils.SimpleFinToDB import process_accounts_data
26 |
27 | # Global variable for private data path, defined directly
28 | PRIVATE_DATA_PATH_SCHEDULER = '/private'
29 |
30 | # Global variable for database path, defined directly
31 | DATABASE_PATH_SCHEDULER = '/private/db/SparkyBudget.db'
32 |
33 | def setup_scheduler():
34 | logger.info(f"Setting up scheduler at {datetime.now()} with timezone {time.tzname}")
35 | schedule.every().day.at("01:00").do(
36 | lambda: run_with_error_handling("Monthly Budget Update", lambda: month_budget_update_using_template(DATABASE_PATH_SCHEDULER))
37 | )
38 | schedule.every().day.at("23:55").do(
39 | lambda: run_with_error_handling("Daily Balance History", lambda: daily_balance_history_insert(DATABASE_PATH_SCHEDULER))
40 | )
41 | schedule.every(4).hours.do(
42 | lambda: run_with_error_handling("Account Data Processing", lambda: process_accounts_data(PRIVATE_DATA_PATH_SCHEDULER))
43 | )
44 | # Test job for debugging
45 | #schedule.every(10).seconds.do(
46 | # lambda: run_with_error_handling("Test Job", lambda: logger.info("Test job ran!"))
47 | #)
48 | #schedule.every(30).seconds.do(
49 | # lambda: run_with_error_handling("Account Data Processing", process_accounts_data)
50 | #)
51 |
52 | def run_with_error_handling(task_name, func):
53 | try:
54 | logger.info(f"Running {task_name} at {datetime.now()}")
55 | func()
56 | logger.info(f"Completed {task_name} successfully")
57 | except Exception as e:
58 | logger.error(f"Error in {task_name}: {str(e)}", exc_info=True)
59 |
60 | def run_scheduler():
61 | setup_scheduler()
62 | logger.info(f"Scheduler started with {len(schedule.jobs)} jobs")
63 | while True:
64 | try:
65 | next_run = schedule.next_run()
66 | logger.debug(f"Next scheduled task: {next_run}")
67 | schedule.run_pending()
68 | time.sleep(10)
69 | except Exception as e:
70 | logger.error(f"Scheduler loop error: {str(e)}", exc_info=True)
71 | time.sleep(60)
72 | if not schedule.jobs:
73 | logger.warning("Scheduler jobs lost, reinitializing...")
74 | setup_scheduler()
75 |
76 | def start_scheduler():
77 | scheduler_thread = threading.Thread(target=run_scheduler, name="SchedulerThread")
78 | scheduler_thread.daemon = True
79 | scheduler_thread.start()
80 | logger.info(f"Scheduler thread started - ID: {scheduler_thread.ident}")
81 | return scheduler_thread
82 |
83 | if __name__ == "__main__":
84 | logger.info("Starting the scheduler as standalone...")
85 | start_scheduler()
86 | while True:
87 | time.sleep(60) # Keep the main thread alive
--------------------------------------------------------------------------------
/SparkyBudget/py_utils/subcategory_update.py:
--------------------------------------------------------------------------------
1 | # py_utils/subcategory_mgmt.py
2 |
3 | import sqlite3, os, logging
4 | from flask import Blueprint, jsonify, request, current_app
5 | from flask_login import login_required
6 |
7 |
8 |
9 | log_level = os.getenv("LOG_LEVEL", "INFO").upper()
10 | logger = logging.getLogger(__name__)
11 |
12 |
13 |
14 | subcategory_update_bp = Blueprint('subcategory_update', __name__)
15 |
16 | @subcategory_update_bp.route("/getDistinctSubcategories", methods=["GET"])
17 | @login_required
18 | def get_distinct_subcategories():
19 | try:
20 | # Connect to the SQLite database
21 | conn = sqlite3.connect(current_app.config['DATABASE_PATH']) # Replace with the actual name of your SQLite database file
22 | cursor = conn.cursor()
23 |
24 | # Fetch distinct Subcategories
25 | cursor.execute("SELECT DISTINCT SubCategory FROM D_Category ORDER BY SubCategory")
26 | distinct_subcategories = [row[0] for row in cursor.fetchall()]
27 |
28 | # Close the database connection
29 | conn.close()
30 |
31 | return jsonify(distinct_subcategories)
32 |
33 | except Exception as e:
34 | # Print the exception details
35 | logger.error(f"An error occurred while fetching distinct subcategories: {str(e)}")
36 |
37 | return jsonify({"error": "Failed to fetch distinct subcategories"}), 500
38 |
39 | @subcategory_update_bp.route("/updateSubcategory", methods=["POST"])
40 | @login_required
41 | def update_subcategory():
42 | transaction_key = request.form.get("transactionId")
43 | new_subcategory = request.form.get("updatedSubcategory")
44 |
45 | # Connect to the SQLite database
46 | conn = sqlite3.connect(current_app.config['DATABASE_PATH']) # Replace with the actual name of your SQLite database file
47 | cursor = conn.cursor()
48 |
49 | # Update the Subcategory in the database
50 | # cursor.execute('UPDATE F_Transaction SET SubCategory = ? WHERE TransactionKey = ?', (new_subcategory, transaction_key))
51 | subcategory_update_query = "UPDATE F_Transaction SET SubCategory = ? WHERE TransactionKey = ?"
52 | subcategory_update_parameters = (new_subcategory, transaction_key)
53 |
54 | logger.debug("SQL Query of updateSubcategory:", subcategory_update_query, " Paramters:", subcategory_update_parameters)
55 |
56 | cursor.execute(subcategory_update_query, subcategory_update_parameters)
57 |
58 | # Commit the changes
59 | conn.commit()
60 |
61 | # Close the database connection
62 | conn.close()
63 |
64 | logger.debug("Subcategory updated successfully")
65 |
66 | return jsonify({"status": "success"})
--------------------------------------------------------------------------------
/SparkyBudget/static/Spinner-0.4s-57px.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodeWithCJ/SparkyBudget/c4c550580c72e7bcfdb171542351fa2d3a7a73eb/SparkyBudget/static/Spinner-0.4s-57px.gif
--------------------------------------------------------------------------------
/SparkyBudget/static/Spinner-1s-200px.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodeWithCJ/SparkyBudget/c4c550580c72e7bcfdb171542351fa2d3a7a73eb/SparkyBudget/static/Spinner-1s-200px.gif
--------------------------------------------------------------------------------
/SparkyBudget/static/css/SparkyBudget.css:
--------------------------------------------------------------------------------
1 | /* Master CSS file for SparkyBudget common styles and theming */
2 |
3 | /* CSS Variables for Theming */
4 | :root {
5 | /* Dark Theme (Default) */
6 | --background-color: #282c35;
7 | --text-color: #e0e7ff;
8 | --container-background: linear-gradient(145deg, #2E3440, #353b48);
9 | --table-header-background: linear-gradient(90deg, #00bcd4, #0288d1);
10 | --table-header-text-color: #ffffff;
11 | --table-border-color: rgba(95, 101, 102, 0.3);
12 | --table-row-hover-background: rgba(0, 188, 212, 0.1);
13 | --heading-color: #61dafb;
14 | --heading-text-shadow: 0 0 10px rgba(0, 188, 212, 0.8);
15 | --delete-button-color: #ff6b6b;
16 | --delete-button-hover-color: #ff4c4c;
17 | --mobile-row-background: #2E3440;
18 | --button-background: linear-gradient(to top, #00154c, #12376e, #23487f);
19 | --button-hover-background: linear-gradient(0deg,#A47CF3,#683FEA);
20 | }
21 |
22 | .light-theme {
23 | /* Light Theme Overrides */
24 | --background-color: #FAFAFA; /* Soft off-white */
25 | --text-color: #444444; /* Dark gray */
26 | --container-background: linear-gradient(145deg, #e0e0e0, #ffffff);
27 | --table-header-background: #c0b283;
28 | --table-header-text-color: #000; /* Black text color for table headers in light theme */
29 | --table-border-color: rgba(0, 0, 0, 0.1);
30 | --table-row-hover-background: rgba(2, 136, 209, 0.1);
31 | --heading-color: #0288d1;
32 | --heading-text-shadow: none;
33 | --delete-button-color: #d32f2f;
34 | --delete-button-hover-color: #c62828;
35 | --mobile-row-background: #ffffff;
36 | --button-background: linear-gradient(to top, #00154c, #12376e, #23487f);
37 | --button-hover-background: linear-gradient(0deg,#A47CF3,#683FEA); /* Slightly darker cream for hover */
38 | }
39 |
40 | :root {
41 | --even-row-background: #2e3440;
42 | --odd-row-background: #353b48;
43 | }
44 |
45 | .light-theme {
46 | --even-row-background: #f0f0f0; /* Light theme even color */
47 | --odd-row-background: #ffffff; /* Light theme odd color */
48 | }
49 |
50 | /* Basic body styles using variables */
51 | body {
52 | background: var(--background-color);
53 | color: var(--text-color);
54 | font-family: 'Arial', sans-serif;
55 | margin: 0;
56 | padding: 0;
57 | transition: background 0.3s ease, color 0.3s ease; /* Smooth transition */
58 | }
59 |
60 | /* Glowing animation keyframes (remains the same) */
61 | @keyframes glow {
62 | 0% {
63 | box-shadow: 0 0 10px rgba(0, 188, 212, 0.3), 0 0 40px rgba(0, 188, 212, 0.2);
64 | }
65 | 100% {
66 | box-shadow: 0 0 15px rgba(0, 188, 212, 0.5), 0 0 60px rgba(0, 188, 212, 0.3);
67 | }
68 | }
69 |
70 | /* Container for styled tables with glowing effect */
71 | .sb-table-container {
72 | background: var(--container-background);
73 | padding: 20px;
74 | border-radius: 16px;
75 | margin: 20px auto;
76 |
77 | position: relative;
78 | box-shadow: 0 0 20px rgba(0, 188, 212, 0.3);
79 | animation: glow 2s ease-in-out infinite alternate;
80 | }
81 |
82 | /* Table styles */
83 | .sb-table-container table {
84 | width: 100%;
85 | border-collapse: separate;
86 | border-spacing: 0;
87 | background: var(--container-background);
88 | border-radius: 12px;
89 | overflow: hidden;
90 | margin-top: 20px;
91 | border: 0.25px solid var(--table-border-color) !important; /* Add border around the table */
92 | }
93 |
94 | .sb-table-container thead {
95 | color: var(--table-header-text-color);
96 | text-transform: uppercase;
97 | letter-spacing: 1.2px;
98 | }
99 |
100 | .sb-table-container th {
101 | padding: 16px 20px;
102 | text-align: left;
103 | border: none;
104 | font-weight: 600;
105 | color: var(--table-header-text-color) !important;
106 | position: relative;
107 | background: var(--background-color);
108 | }
109 |
110 | .sb-table-container th::after {
111 | content: '';
112 | position: absolute;
113 | right: 0;
114 | top: 50%;
115 | transform: translateY(-50%);
116 | height: 60%;
117 | width: 1px;
118 | background: rgba(255, 255, 255, 0.3); /* This might need a variable */
119 | }
120 |
121 | .sb-table-container th:last-child::after {
122 | display: none;
123 | }
124 |
125 | .sb-table-container tbody {
126 | background: transparent;
127 | }
128 |
129 | .sb-table-container tr {
130 | transition: all 0.3s ease;
131 | }
132 |
133 | .sb-table-container tr:hover {
134 | background: var(--table-row-hover-background) !important; /* Subtle blue background */
135 | transform: translateY(-2px);
136 | box-shadow: 0 0 10px rgba(0, 188, 212, 0.5), 0 0 20px rgba(0, 188, 212, 0.3); /* Glowing effect */
137 | }
138 |
139 | .sb-table-container tbody tr:nth-child(even) {
140 | background-color: var(--even-row-background, #2e3440); /* Default to dark theme even color */
141 | }
142 |
143 | .sb-table-container tbody tr:nth-child(odd) {
144 | background-color: var(--odd-row-background, #353b48); /* Default to dark theme odd color */
145 | }
146 |
147 | .sb-table-container td {
148 | padding: 15px 20px;
149 | text-align: left;
150 | border: 1px solid var(--table-border-color); /* Add border to body cells */
151 | color: var(--text-color);
152 | font-weight: 400;
153 | }
154 |
155 | .sb-table-container td:last-child {
156 | border-right: none;
157 | }
158 |
159 | /* Center the "No data" message */
160 | .sb-table-container .text-center {
161 | text-align: center;
162 | font-style: italic;
163 | color: #888; /* This might need a variable */
164 | }
165 |
166 | /* Header styles within the container */
167 | .sb-table-container h1 {
168 | text-align: center;
169 | color: var(--heading-color);
170 | font-size: 28px;
171 | margin-bottom: 20px;
172 | text-shadow: var(--heading-text-shadow);
173 | }
174 |
175 | /* Styles for the delete button column */
176 | .sb-delete-column {
177 | text-align: center;
178 | width: 50px;
179 | }
180 |
181 | /* Styles for the delete button */
182 | .sb-delete-btn {
183 | background: none;
184 | border: none;
185 | color: var(--delete-button-color);
186 | cursor: pointer;
187 | font-size: 18px;
188 | transition: transform 0.2s ease, color 0.2s ease;
189 | }
190 |
191 | .sb-delete-btn:hover {
192 | color: var(--delete-button-hover-color);
193 | transform: scale(1.2);
194 | }
195 |
196 | .sb-delete-btn:focus {
197 | outline: none;
198 | }
199 |
200 | /* Responsive styles for mobile devices */
201 | @media (max-width: 768px) {
202 | /* Adjust the table styles */
203 | .sb-table-container table {
204 | width: 100%;
205 | font-size: 14px;
206 | border-collapse: collapse;
207 | }
208 |
209 | .sb-table-container table thead {
210 | display: none;
211 | }
212 |
213 | .sb-table-container table tbody tr {
214 | display: flex;
215 | flex-direction: row;
216 | align-items: center;
217 | justify-content: space-between;
218 | margin-bottom: 10px;
219 | margin-top: 10px;
220 | border: 1px solid var(--table-border-color);
221 | border-radius: 8px;
222 | padding: 8px;
223 | background: var(--mobile-row-background);
224 | box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
225 | margin-left: 5px;
226 | margin-right: 5px;
227 | width: calc(100% - 10px);
228 | box-sizing: border-box;
229 | }
230 |
231 | .sb-table-container table tbody tr td {
232 | padding: 5px 8px;
233 | border: none !important;
234 | box-sizing: border-box;
235 | }
236 |
237 | /* Subcategory and amount in a single container */
238 | .sb-table-container table tbody tr .sb-subcategory-amount {
239 | display: flex;
240 | flex-direction: column;
241 | flex-grow: 1;
242 | margin-left: 10px;
243 | }
244 |
245 | .sb-table-container table tbody tr .sb-subcategory {
246 | font-weight: bold;
247 | color: var(--heading-color); /* Using heading color for subcategory */
248 | margin-bottom: 3px;
249 | }
250 |
251 | .sb-table-container table tbody tr .sb-amount {
252 | font-size: 16px;
253 | color: var(--text-color);
254 | }
255 |
256 | /* Delete button styling */
257 | .sb-table-container table tbody tr .sb-delete-column {
258 | display: flex;
259 | align-items: center;
260 | justify-content: center;
261 | width: 40px;
262 | margin-left: 10px;
263 | }
264 |
265 | .sb-table-container table tbody tr .sb-delete-btn {
266 | background: none;
267 | border: none;
268 | color: var(--delete-button-color);
269 | cursor: pointer;
270 | font-size: 18px;
271 | transition: transform 0.2s ease, color 0.2s ease;
272 | }
273 |
274 | .sb-table-container table tbody tr .sb-delete-btn:hover {
275 | color: var(--delete-button-hover-color);
276 | transform: scale(1.2);
277 | }
278 |
279 | .sb-delete-btn:focus {
280 | outline: none;
281 | }
282 | }
283 |
284 | /* Styles for the form wrapper */
285 | .sb-form-wrapper {
286 | margin-top: 10px;
287 | width: 35% !important; /* Keep the form width */
288 | height: auto;
289 | padding: 15px;
290 | border: 1px solid var(--table-border-color); /* Use theme variable for border */
291 | background: var(--container-background);
292 | border-radius: 8px;
293 | display: flex;
294 | flex-direction: column;
295 | gap: 10px; /* Space between form groups and button */
296 | align-items: center;
297 | position: relative;
298 | }
299 |
300 | .sb-form-wrapper .form-group {
301 | display: flex;
302 | align-items: center; /* Vertically center the label and input/select */
303 | justify-content: space-between; /* Distribute space between label and input/select */
304 | width: 100%; /* Ensure the group takes full width of the wrapper */
305 |
306 | }
307 |
308 | .sb-form-wrapper .budget-form-group label {
309 | flex: 0 0 30%; /* Reduce label width for better balance */
310 | text-align: left; /* Align text to the left */
311 | color: var(--text-color); /* Use theme variable for text color */
312 | font-size: 14px;
313 | }
314 |
315 | .sb-form-wrapper .budget-form-group select{
316 | flex: 0 0 65% !important; /* Consistent width for both select and input */
317 | width: 100% !important; /* Ensure they fill their allocated space */
318 | box-sizing: border-box; /* Include padding and border in the width */
319 |
320 | }
321 |
322 | .sb-form-wrapper .budget-form-group input {
323 | flex: 0 0 65% !important; /* Consistent width for both select and input */
324 | width: 100% !important; /* Ensure they fill their allocated space */
325 | box-sizing: border-box; /* Include padding and border in the width */
326 |
327 | }
328 |
329 | /* Styles for buttons within sb-form-wrapper */
330 | .sb-form-wrapper button {
331 | border: none;
332 | width: 10em; /* Reduced width */
333 | height: 3em; /* Reduced height */
334 | border-radius: 2em; /* Adjusted border-radius */
335 | display: flex;
336 | justify-content: center;
337 | align-items: center;
338 | gap: 8px; /* Reduced gap */
339 | background: var(--button-background);
340 | cursor: pointer;
341 | transition: all 450ms ease-in-out;
342 | margin: 0 auto; /* Keep the button centered */
343 |
344 | font-weight: 600;
345 | color: white; /* Assuming white text for both themes for now */
346 | font-size: small;
347 |
348 | }
349 |
350 | .sb-form-wrapper button:hover {
351 | background: var(--button-hover-background);
352 | /* The box-shadow might need adjustment for light theme */
353 | box-shadow: inset 0px 1px 0px 0px rgba(255, 255, 255, 0.4),
354 | inset 0px -4px 0px 0px rgba(0, 0, 0, 0.2),
355 | 0px 0px 0px 4px rgba(255, 255, 255, 0.2),
356 | 0px 0px 180px 0px #9917FF;
357 | transform: translateY(-2px);
358 | color: white; /* Assuming white text for both themes for now */
359 | }
360 |
361 | @media (max-width: 768px) {
362 | .sb-form-wrapper {
363 | width: 90% !important; /* Adjust width for mobile */
364 | margin: 0 auto; /* Center the form on mobile */
365 | }
366 | }
367 |
368 | @media (max-width: 768px) {
369 | .mobile-header-logo .networth,
370 | .mobile-header-logo .account {
371 | display: none;
372 | }
373 | }
374 | /* --- Generic Select2 Styles --- */
375 |
376 | /* Container for the Select2 dropdown */
377 | .sp-select2-dropdown .select2-container--default .select2-selection--single {
378 | background-color: var(--background-color) !important; /* Use theme background */
379 | color: var(--text-color) !important; /* Use theme text color */
380 | border: 1px solid var(--table-border-color) !important; /* Use theme border color */
381 | height: 38px !important; /* Adjust height if needed */
382 | padding: 6px 12px !important; /* Adjust padding */
383 | }
384 |
385 | /* Style for the selected value */
386 | .sp-select2-dropdown .select2-container--default .select2-selection--single .select2-selection__rendered {
387 | color: var(--text-color) !important; /* Use theme text color for selected value */
388 | line-height: 26px; /* Vertically align text */
389 | }
390 |
391 | /* Style for the dropdown arrow */
392 | .sp-select2-dropdown .select2-container--default .select2-selection--single .select2-selection__arrow {
393 | height: 36px !important; /* Match container height */
394 | }
395 |
396 | /* Style for the dropdown panel */
397 | .sp-select2-dropdown .select2-container--default .select2-dropdown {
398 | background-color: var(--background-color) !important; /* Use theme background for dropdown */
399 | border: 1px solid var(--table-border-color) !important; /* Use theme border color */
400 | }
401 |
402 | /* Style for dropdown options */
403 | .sp-select2-dropdown .select2-container--default .select2-results__option {
404 | color: var(--text-color) !important; /* Use theme text color for options */
405 | background-color: var(--background-color) !important; /* Use theme background for options */
406 | }
407 |
408 | /* Style for highlighted (hovered) option */
409 | .sp-select2-dropdown .select2-container--default .select2-results__option--highlighted[aria-selected] {
410 | background-color: var(--table-row-hover-background) !important; /* Use theme hover background */
411 | color: var(--text-color) !important; /* Use theme text color on hover */
412 | }
413 |
414 | /* Style for selected option */
415 | .sp-select2-dropdown .select2-container--default .select2-results__option[aria-selected="true"] {
416 | background-color: var(--odd-row-background) !important; /* Use theme odd row background for selected */
417 | color: var(--text-color) !important; /* Use theme text color for selected */
418 | }
419 |
420 | /* Style for the search input within the dropdown */
421 | .sp-select2-dropdown .select2-search input {
422 | background-color: var(--even-row-background) !important; /* Use theme even row background for search input */
423 | color: var(--text-color) !important; /* Use theme text color for search input */
424 | border: 1px solid var(--table-border-color) !important; /* Use theme border color */
425 | }
--------------------------------------------------------------------------------
/SparkyBudget/static/css/balance_details.css:
--------------------------------------------------------------------------------
1 | /* Container with glowing effect */
2 | .balance-details-wrapper {
3 | background: linear-gradient(145deg, #2E3440, #353b48);
4 | padding: 20px;
5 | border-radius: 16px;
6 | margin: 20px auto;
7 | width: 80%;
8 | position: relative;
9 | }
10 |
11 | /* Glowing animation keyframes */
12 |
13 | /* Desktop Table Styles */
14 | .balance-details-table {
15 | }
16 |
17 | .balance-details-table thead {
18 | }
19 |
20 | .balance-details-table th {
21 | border-right: 1px solid #ddd; /* Add vertical border */
22 | }
23 |
24 |
25 |
26 | .balance-details-table tbody {
27 | }
28 |
29 | .balance-details-table tr {
30 | }
31 |
32 | .balance-details-table tr:hover {
33 | box-shadow: none; /* Remove box-shadow on hover */
34 | }
35 |
36 | .balance-details-table td {
37 | border-bottom: 1px solid #ddd; /* Add horizontal border */
38 | border-right: 1px solid #ddd; /* Add vertical border */
39 | }
40 |
41 |
42 | /* Editable Cell Styles */
43 | .editable-display-name, .editable-account-type, .editable-last-sync, .editable-balance, .editable-available-balance {
44 | cursor: pointer;
45 | }
46 |
47 | .editable-display-name:hover, .editable-account-type:hover, .editable-last-sync:hover, .editable-balance:hover, .editable-available-balance:hover {
48 | background-color: rgba(0, 188, 212, 0.2);
49 | }
50 |
51 | .display-name-input, .account-type-select, .last-sync-input, .balance-input, .available-balance-input {
52 | width: 100%;
53 | background-color: #444444;
54 | color: #ffffff;
55 |
56 | padding: 5px;
57 | border-radius: 6px;
58 | }
59 |
60 | /* DataTables Overrides */
61 | table.dataTable thead .sorting,
62 | table.dataTable thead .sorting_asc,
63 | table.dataTable thead .sorting_desc {
64 | background-color: transparent;
65 | color: #ffffff !important;
66 | }
67 |
68 | /* Hide Mobile View on Desktop */
69 | .balance-details-mobile-container {
70 | display: none;
71 | }
72 |
73 | /* Mobile Styles */
74 | @media (max-width: 768px) {
75 | .balance-details-wrapper {
76 | width: 95%; /* Set to 95% for mobile view */
77 | margin: 10px auto; /* Reduce margin to minimize extra space */
78 | padding: 10px; /* Reduce padding if needed */
79 | }
80 | .balance-details-container {
81 | display: none;
82 | }
83 |
84 | .balance-details-mobile-container {
85 | display: block;
86 | width: 100%; /* Ensure it takes the full width of the wrapper */
87 | max-width: none; /* Remove any max-width restrictions */
88 | margin: 0; /* Remove any extra margins */
89 | padding: 0; /* Remove padding to avoid extra space */
90 | }
91 |
92 | .bank-group {
93 | width: 100%; /* Ensure the bank group takes full width */
94 | margin-bottom: 20px;
95 | padding: 0; /* Remove padding if any */
96 | }
97 |
98 | .bank-title {
99 | color: #ffffff;
100 | font-size: 20px;
101 | font-weight: 600;
102 | margin-bottom: 10px;
103 | padding-left: 10px;
104 | text-transform: uppercase;
105 | letter-spacing: 1px;
106 | }
107 |
108 | .balance_details_card {
109 | background: linear-gradient(145deg, #2E3440, #353b48);
110 | border-radius: 8px;
111 | margin: 10px 0;
112 | padding: 15px;
113 | color: #fff;
114 | box-shadow: 0 2px 5px rgba(0, 188, 212, 0.2);
115 | width: 100% !important; /* Or adjust percentage as needed */
116 |
117 |
118 | margin-bottom: 15px; /* Keep or adjust vertical spacing */
119 | }
120 |
121 |
122 | .balance_details_header {
123 | display: flex;
124 | justify-content: space-between;
125 | align-items: center;
126 | }
127 |
128 | .balance_details_info {
129 | flex-grow: 1;
130 | }
131 |
132 | .balance_details_account_name {
133 | font-size: 16px;
134 | font-weight: bold;
135 | display: block;
136 | }
137 |
138 | .balance_details_last_refreshed {
139 | font-size: 12px;
140 | color: #aaa;
141 | display: block;
142 | }
143 |
144 | .balance_details_balance {
145 | font-size: 16px;
146 | font-weight: bold;
147 | display: block;
148 | }
149 |
150 | .balance_details_status {
151 | font-size: 12px;
152 | color: #00cc00;
153 | cursor: pointer;
154 | }
155 |
156 | .balance_details_body {
157 | margin-top: 10px;
158 | }
159 |
160 | .balance_details_field {
161 | margin: 5px 0;
162 | }
163 |
164 | .balance_details_label {
165 | font-weight: bold;
166 | color: #aaa;
167 | }
168 | }
169 |
170 |
171 |
172 |
173 | /* AddAccount Popup Overlay */
174 | #addAccountPopup {
175 | position: fixed;
176 | top: 0;
177 | left: 0;
178 | width: 100%;
179 | height: 100%;
180 | background: rgba(0, 0, 0, 0.7);
181 | display: flex;
182 | justify-content: center;
183 | align-items: center;
184 | z-index: 1000;
185 | }
186 |
187 | /* Popup Content */
188 | #addAccountPopup .popup-content {
189 | background: #1e1e2f;
190 | padding: 20px;
191 | border-radius: 8px;
192 | width: 400px;
193 | box-shadow: 0 0 15px rgba(0, 0, 0, 0.5);
194 | color: #e0e7ff;
195 | }
196 |
197 | /* Form Styles */
198 | #addAccountForm .form-group {
199 | margin-bottom: 15px;
200 | }
201 |
202 | #addAccountForm .form-group label {
203 | display: block;
204 | margin-bottom: 5px;
205 | font-size: 14px;
206 | color: #ffffff;
207 | }
208 |
209 | #addAccountForm .form-group input,
210 | #addAccountForm .form-group select {
211 | width: 100%;
212 | padding: 8px;
213 | border: 1px solid #444;
214 | border-radius: 4px;
215 | background: #2a2a3d;
216 | color: #e0e7ff;
217 | font-size: 14px;
218 | }
219 |
220 | #addAccountForm .form-actions {
221 | display: flex;
222 | justify-content: space-between;
223 | }
224 |
225 | #addAccountForm .btn {
226 | padding: 8px 15px;
227 | border: none;
228 | border-radius: 4px;
229 | cursor: pointer;
230 | }
231 |
232 | #addAccountForm .btn-primary {
233 | background: #61dafb;
234 | color: #1e1e2f;
235 | }
236 |
237 | #addAccountForm .btn-secondary {
238 | background: #444;
239 | color: #e0e7ff;
240 | }
241 |
242 |
243 |
244 | /* Upload Icon Styles */
245 | .upload-icon {
246 | color: #61dafb;
247 | font-size: 16px;
248 | transition: color 0.3s ease;
249 | }
250 |
251 | .upload-icon:hover {
252 | color: #00bcd4;
253 | }
254 |
255 | /* Ensure upload column is centered */
256 | .balance-details-table td:last-child {
257 | text-align: center;
258 | }
259 |
260 | /* Mobile Upload Icon */
261 | .balance-details-mobile-container .upload-icon {
262 | margin-left: 10px;
263 | font-size: 14px;
264 | }
265 |
266 | .edit-indicator {
267 | margin-left: 5px;
268 | font-size: 0.8em; /* Make icon slightly smaller */
269 | color: #777; /* Give it a subtle color */
270 | cursor: default; /* Indicate it's not clickable itself */
271 | }
272 | .upload-icon i { /* Style the upload icon if needed */
273 | cursor: pointer;
274 | }
--------------------------------------------------------------------------------
/SparkyBudget/static/css/balance_summary.css:
--------------------------------------------------------------------------------
1 | .balance-summary-list {
2 | font-family: Arial, sans-serif;
3 | color: #d6dadf;
4 | background: linear-gradient(145deg, #2E3440, #353b48);
5 | border-radius: 8px;
6 | }
7 |
8 | .account-type {
9 | margin-bottom: 5px;
10 | }
11 |
12 | .account-type-header {
13 | display: flex;
14 | justify-content: space-between;
15 | align-items: flex-start;
16 | padding: 15px 10px;
17 | cursor: pointer;
18 | background-color: #252a33;
19 | border-radius: 4px;
20 | }
21 |
22 | /* Remove cursor: pointer for headers without children */
23 | .account-type-header:not([data-toggle="collapse"]) {
24 | cursor: default;
25 | }
26 |
27 |
28 | .account-type-left {
29 | display: flex;
30 | align-items: center;
31 | }
32 |
33 | .expand-icon {
34 | font-size: 14px;
35 | margin-right: 10px;
36 | }
37 |
38 | .expand-placeholder {
39 | width: 24px; /* Match the width of the expand-icon (14px font-size + 10px margin-right) */
40 | display: inline-block;
41 | }
42 |
43 | .account-name {
44 | flex-grow: 1;
45 | font-size: 16px;
46 | }
47 |
48 | .account-type-right {
49 | display: flex;
50 | flex-direction: column;
51 | align-items: flex-end;
52 | text-align: right;
53 | }
54 |
55 | .balance {
56 | font-weight: bold;
57 | font-size: 16px;
58 | }
59 |
60 | .available-balance {
61 | font-size: 12px;
62 | margin-top: 2px;
63 | }
64 |
65 | .collapse {
66 | display: none;
67 | }
68 |
69 | .collapse.show {
70 | display: block;
71 | }
72 |
73 | .account-sub-item {
74 | display: flex;
75 | justify-content: space-between;
76 | padding: 10px 20px;
77 | background-color: #474e5d;
78 | border-top: 1px solid #2a5a5a;
79 | }
80 |
81 | .account-sub-item-right {
82 | display: flex;
83 | flex-direction: column;
84 | align-items: flex-end;
85 | text-align: right;
86 | }
87 |
88 | .account-sub-item .account-name {
89 | text-align: left;
90 | font-size: 14px;
91 | }
92 |
93 | .account-sub-item .balance {
94 | font-weight: normal;
95 | font-size: 14px;
96 | }
97 |
98 | .account-sub-item .available-balance {
99 | font-size: 11px;
100 | margin-top: 2px;
101 | }
102 |
103 |
104 |
105 | /* Ensure the balance summary is visible on larger screens by default */
106 | @media (min-width: 769px) {
107 | .balance-summary-container {
108 | display: block;
109 | }
110 | }
111 |
112 |
113 | /* Light theme styles */
114 | .light-theme .balance-summary-list {
115 | color: #333; /* Darker text for light theme */
116 | background: linear-gradient(145deg, #e0e0e0, #f0f0f0); /* Lighter background */
117 | }
118 |
119 | .light-theme .account-type-header {
120 | background-color: #cccccc; /* Lighter header background */
121 | }
122 |
123 | .light-theme .account-sub-item {
124 | background-color: #dddddd; /* Lighter sub-item background */
125 | border-top: 1px solid #bbbbbb; /* Lighter border */
126 | }
127 |
128 | /* Light theme styles for the main container */
129 | .light-theme .balance-summary-container {
130 | color: var(--text-color); /* Use text color from SparkyBudget.css */
131 | background: var(--container-background); /* Use container background from SparkyBudget.css */
132 | box-shadow: 0 0 20px rgba(0, 123, 255, 0.3); /* Adjust glow for light theme */
133 | }
134 |
--------------------------------------------------------------------------------
/SparkyBudget/static/css/budget.css:
--------------------------------------------------------------------------------
1 | /* Dark theme background for the entire page */
2 | body {
3 | background: #282c35;
4 | color: #e0e7ff;
5 | font-family: 'Arial', sans-serif;
6 | margin: 0;
7 | padding: 0;
8 | }
9 |
10 |
11 | .sb-table-container {
12 | margin: 20px auto;
13 | width: 40%; /* Increased width to prevent overflow */
14 | }
15 | /* Styles for the delete button */
16 | .delete-column {
17 | text-align: center;
18 | width: 50px;
19 | }
20 |
21 | .delete-btn {
22 | background: none;
23 | border: none;
24 | color: #ff6b6b;
25 | cursor: pointer;
26 | font-size: 18px;
27 | transition: transform 0.2s ease, color 0.2s ease;
28 | }
29 |
30 | .delete-btn:hover {
31 | color: #ff4c4c;
32 | transform: scale(1.2);
33 | }
34 |
35 | .delete-btn:focus {
36 | outline: none;
37 | }
38 |
39 | .select2-container--default .select2-results__option {
40 | background-color: #3c444f !important; /* Set the background color to match the category page */
41 | /* Set the text color to white for better visibility */
42 | color: white;
43 | }
44 |
45 | /* Limit the width of the select2 dropdown to 40% */
46 |
47 | /* Responsive styles for mobile devices */
48 | @media (max-width: 768px) {
49 | /* Adjust the table styles */
50 | table {
51 | width: 100%;
52 | font-size: 14px;
53 | border-collapse: collapse;
54 | }
55 |
56 | table thead {
57 | display: none; /* Already hidden, which is good */
58 | }
59 |
60 | table tbody tr {
61 | display: flex;
62 | flex-direction: row; /* Use row to align items horizontally */
63 | align-items: center; /* Center items vertically */
64 | justify-content: space-between; /* Space out the content */
65 | margin-bottom: 10px;
66 | margin-top: 10px;
67 | border: 1px solid #ddd;
68 | border-radius: 8px;
69 | padding: 8px;
70 | background: #2E3440;
71 | box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
72 | margin-left: 5px;
73 | margin-right: 5px;
74 | width: calc(100% - 10px);
75 | box-sizing: border-box;
76 | }
77 |
78 | table tbody tr td {
79 | padding: 5px 8px;
80 | border: none !important; /* This forces the override */
81 | box-sizing: border-box;
82 | }
83 |
84 | /* Subcategory and amount in a single container */
85 | table tbody tr .subcategory-amount {
86 | display: flex;
87 | flex-direction: column; /* Stack subcategory and amount vertically */
88 | flex-grow: 1; /* Allow this section to take available space */
89 | margin-left: 10px;
90 | }
91 |
92 | table tbody tr .subcategory {
93 | font-weight: bold;
94 | color: #61dafb;
95 | margin-bottom: 3px; /* Small space between subcategory and amount */
96 | }
97 |
98 | table tbody tr .amount {
99 | font-size: 16px;
100 | color: #e0e7ff;
101 | }
102 |
103 | /* Delete button styling */
104 | table tbody tr .delete-column {
105 | display: flex;
106 | align-items: center;
107 | justify-content: center;
108 | width: 40px; /* Fixed width for the delete button column */
109 | margin-left: 10px;
110 | }
111 |
112 | table tbody tr .delete-btn {
113 | background: none;
114 | border: none;
115 | color: #ff6b6b;
116 | cursor: pointer;
117 | font-size: 18px;
118 | transition: transform 0.2s ease, color 0.2s ease;
119 | }
120 |
121 | table tbody tr .delete-btn:hover {
122 | color: #ff4c4c;
123 | transform: scale(1.2);
124 | }
125 |
126 | table tbody tr .delete-btn:focus {
127 | outline: none;
128 | }
129 | .sb-table-container {
130 |
131 | width: 85%; /* Increased width to prevent overflow */
132 | }
133 | }
134 |
135 |
136 | @media (max-width: 768px) {
137 |
138 | .mobile-header-logo .nav-icon
139 | {
140 | display: none;
141 | }
142 | }
143 |
144 |
145 |
146 |
--------------------------------------------------------------------------------
/SparkyBudget/static/css/budget_summary.css:
--------------------------------------------------------------------------------
1 |
2 | #budgetsummaryTable a {
3 | color: #fff; /* White font color for the hyperlink */
4 | text-decoration: none; /* Remove underline */
5 | }
6 |
7 |
8 | .subcategory-text {
9 | color: #61DAFB; /* Default color for subcategory text and icon */
10 | }
11 |
12 | body.light-theme .subcategory-text {
13 | color: #000; /* Black color for subcategory text and icon in light theme */
14 | }
15 |
16 | .delete-icon {
17 | color: #ff6b6b; /* Red color for the delete icon */
18 | }
19 |
20 | /* New container for each subcategory */
21 | .subcategory-container {
22 | border: 1px solid #4a5263; /* Adjusted border color to complement the new background */
23 |
24 | border-radius: 8px; /* Rounded corners for a smoother look */
25 | padding: 5px; /* Inner padding to keep content from touching the border */
26 | margin-bottom: 10px; /* Space between each subcategory section */
27 | width: 90%; /* Match the original width of top-content */
28 | margin-left: auto; /* Center the container */
29 | margin-right: auto; /* Center the container */
30 | box-sizing: border-box; /* Include padding and border in width calculation */
31 | }
32 |
33 | .top-content {
34 | display: flex;
35 | justify-content: space-between; /* Push children to opposite ends */
36 | align-items: center; /* Ensure all children are vertically centered */
37 | width: 100% !important; /* Full width within the subcategory-container */
38 | margin: 0; /* Remove any default margins */
39 | padding: 0; /* Remove any default padding */
40 | box-sizing: border-box; /* Include padding and border in width calculation */
41 | }
42 |
43 | .left-content {
44 | display: flex; /* Ensure the content inside is aligned properly */
45 | align-items: center; /* Align items vertically */
46 | gap: 5px; /* Add spacing between elements inside left-content */
47 | }
48 |
49 | .left-content p {
50 | display: inline-block;
51 | margin: 0;
52 | vertical-align: middle;
53 | line-height: 1; /* Normalize line height to avoid vertical offset */
54 | }
55 |
56 | .left-content img {
57 | width: 30px;
58 | height: 30px;
59 | vertical-align: middle;
60 | margin-right: 8px;
61 | }
62 |
63 | .right-content {
64 | display: flex; /* Make right-content a flex container */
65 | align-items: center; /* Center its content vertically */
66 | text-align: right; /* Align text to the right */
67 | margin: 0; /* Remove any default margins */
68 | padding: 0; /* Remove any default padding */
69 | }
70 |
71 | .right-content p {
72 | margin: 0; /* Remove default margins on the
tag */
73 | line-height: 1; /* Normalize line height to match left-content */
74 | }
75 |
76 | .category-bar {
77 | width: 100% !important; /* Full width within the subcategory-container */
78 | margin: 5px 0; /* Adjusted margin for spacing */
79 | height: 20px; /* Increase from default (e.g., 8px or 10px) */
80 | border-radius: 8px; /* Optional for rounded corners */
81 | background-color: #ffffff; /* Light gray background for the empty portion */
82 | }
83 |
84 | body.light-theme .category-bar {
85 | background-color: #706e6e; /* Light gray background for the empty portion in light theme */
86 | }
87 |
88 | .subcategory-bar {
89 | height: 20px; /* Match the category-bar height */
90 | border-radius: 8px;
91 | }
92 |
93 | .bottom-content-wrapper {
94 | display: flex;
95 | justify-content: space-between; /* Push content to opposite ends */
96 | align-items: center; /* Align items vertically */
97 | width: 100% !important; /* Full width within the subcategory-container */
98 | margin-top: 10px; /* Add spacing above if needed */
99 | box-sizing: border-box; /* Include padding in width calculation */
100 | padding-bottom: 5px;
101 | }
102 |
103 | .bottom-left-content {
104 | text-align: left; /* Align text to the left */
105 | margin: 0; /* Remove any default margins */
106 | padding: 0; /* Remove any default padding */
107 | align-self: flex-start; /* Align it to the start of the parent container */
108 | }
109 |
110 | .bottom-right-content {
111 | text-align: right; /* Align text to the right */
112 | margin: 0; /* Remove any default margins */
113 | padding: 0; /* Remove any default padding */
114 | }
115 |
116 | @media screen and (max-width: 768px) {
117 | .container {
118 | width: 100%;
119 | margin: 0;
120 | padding: 0;
121 | }
122 |
123 | .subcategory-container {
124 | width: 100% !important; /* Full width in mobile view */
125 | margin: 0 0 15px 0; /* Remove side margins, keep bottom margin */
126 | padding: 10px; /* Consistent padding */
127 | border: 1px solid #4a5263; /* Keep the adjusted border color */
128 |
129 | border-radius: 8px; /* Keep rounded corners */
130 | box-sizing: border-box;
131 | }
132 |
133 | .top-content {
134 | display: flex;
135 | justify-content: space-between;
136 | align-items: center;
137 | }
138 | }
--------------------------------------------------------------------------------
/SparkyBudget/static/css/budget_transaction_details.css:
--------------------------------------------------------------------------------
1 | .transaction-subcategory-header {
2 | text-align: center;
3 | color: var(--text-color); /* Use theme variable for text color */
4 | margin-bottom: 15px; /* Add some space below the header */
5 | }
6 |
7 | /* Container for the table with glowing effect */
8 | .budget_transaction_details_wrapper {
9 | /*background: linear-gradient(135deg, #1e2a44 0%, #2a3b5a 100%); */
10 | /* Removed background to avoid duplicate container appearance */
11 | /* Removed padding to avoid duplicate container appearance */
12 | border-radius: 16px;
13 | margin: 10px 0;
14 | position: relative;
15 | }
16 |
17 |
18 | /* Default styles for desktop - Enhanced version with new colors */
19 | .budget-transaction-table {
20 | }
21 |
22 | .budget-transaction-table thead {
23 | }
24 |
25 | .budget-transaction-table th {
26 | }
27 |
28 | .budget-transaction-table th::after {
29 | }
30 |
31 |
32 | .budget-transaction-table tbody {
33 | }
34 |
35 | .budget-transaction-table tr {
36 | }
37 |
38 | .budget-transaction-table tr:hover {
39 | }
40 |
41 | .budget-transaction-table td {
42 | }
43 |
44 |
45 | .budget-transaction-table .reCategorizeButton {
46 | background: linear-gradient(90deg, #ff6f61, #ff8a65);
47 | color: white;
48 | border: none;
49 | padding: 8px 16px;
50 | border-radius: 6px;
51 | cursor: pointer;
52 | transition: all 0.3s ease;
53 | font-weight: 500;
54 | }
55 |
56 | .budget-transaction-table .reCategorizeButton:hover {
57 | transform: translateY(-1px);
58 | box-shadow: 0 4px 12px rgba(255, 111, 97, 0.4);
59 | }
60 |
61 | .budget-transaction-table .updateSubcategoryButton {
62 | background: linear-gradient(90deg, #4dd0e1, #81d4fa);
63 | color: white;
64 | border: none;
65 | padding: 8px 16px;
66 | border-radius: 6px;
67 | cursor: pointer;
68 | transition: all 0.3s ease;
69 | font-weight: 500;
70 | }
71 |
72 | .budget-transaction-table .updateSubcategoryButton:hover {
73 | transform: translateY(-1px);
74 | box-shadow: 0 4px 12px rgba(77, 182, 172, 0.4);
75 | }
76 |
77 | .budget-transaction-table .subcategoryLabel {
78 | color: #e0e7ff;
79 | margin-right: 10px;
80 | font-weight: 500;
81 | }
82 |
83 | .budget-transaction-table .select2-container .select2-selection--single {
84 | background: #3e4b6a;
85 | border: 1px solid rgba(255, 255, 255, 0.15);
86 | color: #e0e7ff;
87 | border-radius: 6px;
88 | height: 36px;
89 | }
90 |
91 | .budget-transaction-table .select2-container--default .select2-selection--single .select2-selection__rendered {
92 | color: #e0e7ff;
93 | line-height: 36px;
94 | }
95 |
96 | .budget-transaction-table .select2-container--default .select2-selection--single .select2-selection__arrow {
97 | height: 36px;
98 | }
99 |
100 | /* Hide mobile view on desktop */
101 | .budget-transaction-mobile-container {
102 | display: none;
103 | }
104 |
105 | /* Mobile styles (unchanged as requested) */
106 | @media (max-width: 768px) {
107 | /* Hide table on mobile */
108 | .budget-transaction-container {
109 | display: none;
110 | }
111 |
112 | /* Show mobile view */
113 | .budget-transaction-mobile-container {
114 | display: block;
115 | }
116 |
117 | /* Card styling */
118 | .budget_transaction_details_card {
119 | background-color: #1a1a1a;
120 | border-radius: 8px;
121 | margin: 10px 0;
122 | padding: 15px;
123 | color: #fff;
124 | box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
125 | }
126 |
127 | .budget_transaction_details_header {
128 | display: flex;
129 | justify-content: space-between;
130 | align-items: center;
131 | }
132 |
133 | .budget_transaction_details_info {
134 | flex-grow: 1;
135 | }
136 |
137 | .budget_transaction_details_payee {
138 | font-size: 16px;
139 | font-weight: bold;
140 | display: block;
141 | }
142 |
143 | .budget_transaction_details_date {
144 | font-size: 12px;
145 | color: #aaa;
146 | }
147 |
148 | .budget_transaction_details_amount {
149 | font-size: 16px;
150 | font-weight: bold;
151 | display: block;
152 | }
153 |
154 | .budget_transaction_details_status {
155 | font-size: 12px;
156 | color: #00cc00;
157 | cursor: pointer;
158 | }
159 |
160 | .budget_transaction_details_toggle {
161 | background: none;
162 | border: none;
163 | color: #fff;
164 | font-size: 16px;
165 | cursor: pointer;
166 | padding: 0;
167 | }
168 |
169 | .budget_transaction_details_body {
170 | margin-top: 10px;
171 | }
172 |
173 | .budget_transaction_details_field {
174 | margin: 5px 0;
175 | }
176 |
177 | .budget_transaction_details_label {
178 | font-weight: bold;
179 | color: #aaa;
180 | }
181 |
182 | .budget_transaction_details_reCategorizeButton {
183 | background-color: #333;
184 | color: #fff;
185 | border: none;
186 | padding: 5px 10px;
187 | border-radius: 4px;
188 | cursor: pointer;
189 | }
190 |
191 | .budget_transaction_details_subcategoryLabel,
192 | .budget_transaction_details_select2,
193 | .budget_transaction_details_updateSubcategoryButton {
194 | margin-top: 5px;
195 | }
196 | }
197 |
198 | /* Styles for the popup overlay */
199 | .popup-overlay {
200 | position: fixed;
201 | top: 0;
202 | left: 0;
203 | width: 100%;
204 | height: 100%;
205 | background-color: rgba(0, 0, 0, 0.7); /* Semi-transparent black background */
206 | display: flex; /* Hide by default */
207 | justify-content: center;
208 | align-items: center;
209 | z-index: 1000; /* Ensure it's above other content */
210 | }
211 |
212 | /* Styles for the popup content */
213 | .popup-content {
214 | background-color: #1e1e2f; /* Light blue color */
215 | color: #1e1e2f; /* Dark text color for contrast */
216 | padding: 20px;
217 | border-radius: 8px;
218 | box-shadow: 0 2px 10px rgba(0, 0, 0, 0.3);
219 | max-width: 500px; /* Limit the width of the popup */
220 | width: 90%; /* Make it responsive */
221 | position: relative;
222 | }
223 |
224 | .popup-header {
225 | display: flex;
226 | justify-content: flex-start; /* Align items to the start (left) */
227 | align-items: center;
228 | margin-bottom: 15px;
229 | padding-bottom: 10px;
230 | border-bottom: 1px solid var(--table-border-color); /* Use theme variable */
231 | }
232 |
233 | .popup-header h2 {
234 | margin: 0;
235 | font-size: 1.5em;
236 | align-items: center;
237 | }
238 |
239 | .popup-header p {
240 | margin: 5px 0 0;
241 | color: #aaa; /* Lighter text for description */
242 | }
243 |
244 | .popup-header .close-button {
245 | background: none;
246 | border: none;
247 | font-size: 1.5em;
248 | cursor: pointer;
249 | color: var(--text-color); /* Use theme variable */
250 | }
251 |
252 | .form-group {
253 | margin-bottom: 15px;
254 | }
255 |
256 | .form-group label {
257 | display: block;
258 | margin-bottom: 5px;
259 | font-weight: bold;
260 | }
261 |
262 | .form-group input[type="text"],
263 | .form-group input[type="number"],
264 | .form-group input[type="date"],
265 | .form-group select {
266 | width: 100%;
267 | padding: 8px;
268 | border: 1px solid var(--table-border-color); /* Use theme variable */
269 | border-radius: 4px;
270 | box-sizing: border-box; /* Include padding and border in the element's total width and height */
271 | background-color: var(--input-background); /* Use theme variable */
272 | color: var(--text-color); /* Use theme variable */
273 | }
274 |
275 | .form-actions {
276 | display: flex;
277 | justify-content: flex-end;
278 | gap: 10px;
279 | margin-top: 20px;
280 | }
281 |
282 | .form-actions button {
283 | padding: 10px 15px;
284 | border: none;
285 | border-radius: 4px;
286 | cursor: pointer;
287 | font-size: 1em;
288 | }
289 |
290 | .form-actions .btn-primary {
291 | background-color: #007bff; /* Primary button color */
292 | color: white;
293 | }
294 |
295 | .form-actions .btn-secondary {
296 | background-color: #6c757d; /* Secondary button color */
297 | color: white;
298 | }
299 |
300 | /* Select2 adjustments for popup */
301 | .select2-container--default .select2-selection--single {
302 | background-color: var(--input-background); /* Use theme variable */
303 | color: var(--text-color); /* Use theme variable */
304 | border: 1px solid var(--table-border-color); /* Use theme variable */
305 | }
306 |
307 | .select2-container--default .select2-selection--single .select2-selection__rendered {
308 | color: var(--text-color); /* Use theme variable */
309 | }
310 |
311 | .select2-container--default .select2-results__option--highlighted[aria-selected] {
312 | background-color: #555 !important;
313 | color: #fff !important;
314 | }
315 |
316 | .select2-container--default .select2-results>.select2-results__options {
317 | background-color: var(--container-background); /* Use theme variable */
318 | color: var(--text-color); /* Use theme variable */
319 | }
320 |
321 | .select2-container--default .select2-search--dropdown .select2-search__field {
322 | background-color: var(--input-background); /* Use theme variable */
323 | color: var(--text-color); /* Use theme variable */
324 | border-color: var(--table-border-color); /* Use theme variable */
325 | }
326 |
327 | /* Styles for the split button */
328 | .split-button {
329 | background: linear-gradient(90deg, #4dd0e1, #81d4fa);
330 | color: white;
331 | border: none;
332 | padding: 8px 16px;
333 | border-radius: 6px;
334 | cursor: pointer;
335 | transition: all 0.3s ease;
336 | font-weight: 500;
337 | }
338 |
339 | .split-button:hover {
340 | transform: translateY(-1px);
341 | box-shadow: 0 4px 12px rgba(77, 182, 172, 0.4);
342 | }
343 |
--------------------------------------------------------------------------------
/SparkyBudget/static/css/category.css:
--------------------------------------------------------------------------------
1 |
2 | /* Container for the category table with glowing effect */
3 | .category-table-container {
4 | background: linear-gradient(145deg, #2E3440, #353b48);
5 | padding: 20px;
6 | border-radius: 16px;
7 |
8 | width: 40%;
9 | position: relative;
10 | }
11 |
12 | .dataTables_paginate .paginate_button,
13 | .dataTables_info,
14 | .dataTables_length label,
15 | .dataTables_filter label,
16 | .dataTables_processing,
17 | .dataTables_paginate span a,
18 | .dataTables_wrapper .dataTables_paginate .paginate_button,
19 | .dataTables_wrapper .dataTables_filter input {
20 | color: #ffffff !important;
21 | background-color: #444444 !important;
22 | }
23 |
24 | .containerCategoryManagement .dataTables_length select,
25 | .containerRuleManagement .dataTables_length select {
26 | background-color: #444444 !important;
27 | color: #fff !important;
28 | border: 1px solid #555555 !important;
29 | border-radius: 4px;
30 | padding: 2px 5px;
31 | cursor: pointer;
32 | }
33 |
34 | .dataTables_wrapper .dataTables_filter input:focus {
35 | background-color: #555555 !important;
36 | }
37 |
38 | /* --- Delete Button Styles --- */
39 | #accountTypeTable td,
40 | #categoryTable td,
41 | #categoryRuleTable td {
42 | border: 1px solid rgba(95, 101, 102, 0.3); /* Add border to body cells */
43 | }
44 |
45 | #accountTypeTable .delete-btn,
46 | #categoryTable .delete-btn,
47 | #categoryRuleTable .delete-btn {
48 | background: none;
49 | border: none;
50 | color: #ff4444;
51 | cursor: pointer;
52 | font-size: 16px;
53 | padding: 5px;
54 | }
55 |
56 | #accountTypeTable .delete-btn:hover,
57 | #categoryTable .delete-btn:hover,
58 | #categoryRuleTable .delete-btn:hover {
59 | color: #cc0000;
60 | }
61 |
62 | /* --- Delete Button Styles --- */
63 | #accountTypeTable .delete-btn,
64 | #categoryTable .delete-btn,
65 | #categoryRuleTable .delete-btn {
66 | background: none;
67 | border: none;
68 | color: #ff4444;
69 | cursor: pointer;
70 | font-size: 16px;
71 | padding: 5px;
72 | }
73 |
74 | #accountTypeTable .delete-btn:hover,
75 | #categoryTable .delete-btn:hover,
76 | #categoryRuleTable .delete-btn:hover {
77 | color: #cc0000;
78 | }
79 |
80 | /* --- Add and Add Rule Button Styles --- */
81 | #addCategoryForm button,
82 | #addSubCategoryRuleForm button {
83 | border: none;
84 | width: 10em;
85 | height: 3em;
86 | border-radius: 2em;
87 | display: flex;
88 | justify-content: center;
89 | align-items: center;
90 | gap: 8px;
91 | background: #3a404c;
92 | cursor: pointer;
93 | transition: all 450ms ease-in-out;
94 | /* Removed margin: 0 auto; */
95 | font-weight: 600;
96 | color: white;
97 | font-size: small;
98 | }
99 |
100 | #addCategoryForm button:hover,
101 | #addSubCategoryRuleForm button:hover {
102 | background: linear-gradient(0deg,#A47CF3,#683FEA);
103 | box-shadow: inset 0px 1px 0px 0px rgba(255, 255, 255, 0.4),
104 | inset 0px -4px 0px 0px rgba(0, 0, 0, 0.2),
105 | 0px 0px 0px 4px rgba(255, 255, 255, 0.2),
106 | 0px 0px 180px 0px #9917FF;
107 | transform: translateY(-2px);
108 | color: white;
109 | }
110 |
111 | /* --- Form Container Styles --- */
112 | .AddSubCategoryContainer,
113 | .AddSubCategoryRuleContainer {
114 | display: flex;
115 | align-items: center;
116 | gap: 10px; /* Adjust gap as needed */
117 | margin-bottom: 20px; /* Add some space below the forms */
118 | /* Styles for the rectangular box */
119 | padding: 15px;
120 | border: 1px solid #4a5263;
121 | background: linear-gradient(145deg, #2E3440, #353b48);
122 | border-radius: 8px;
123 | box-sizing: border-box; /* Include padding and border in the element's total width and height */
124 | width: 40%; /* Set width to 40% for desktop */
125 | margin-left: 20px; /* Align to the left with spacing */
126 | }
127 |
128 | .AddSubCategoryContainer form,
129 | .AddSubCategoryRuleContainer form {
130 | display: flex;
131 | align-items: center;
132 | gap: 10px; /* Adjust gap as needed */
133 | width: 100%; /* Ensure form takes full width of container */
134 | }
135 |
136 | .AddSubCategoryContainer select,
137 | .AddSubCategoryContainer input[type="text"],
138 | .AddSubCategoryRuleContainer select,
139 | .AddSubCategoryRuleContainer input[type="text"] {
140 | flex-grow: 1; /* Allow inputs and selects to take available space */
141 | }
142 |
143 | /* --- Container and Table Widths --- */
144 | .containerAccountTypes,
145 | .containerCategoryManagement,
146 | .containerRuleManagement {
147 | width: 90%;
148 | box-sizing: border-box;
149 | }
150 |
151 | .containerAccountTypes .dataTables_wrapper,
152 | .containerCategoryManagement .dataTables_wrapper,
153 | .containerRuleManagement .dataTables_wrapper {
154 | width: 100% !important;
155 | }
156 |
157 | #accountTypeTable,
158 | #categoryTable,
159 | #categoryRuleTable {
160 | width: 100% !important;
161 | border-collapse: separate; /* Ensure border-spacing works */
162 | border-spacing: 0; /* Remove space between borders */
163 | border: 0.25px white !important; /* Add border around the table */
164 | border-radius: 12px;
165 | overflow: hidden;
166 | }
167 |
168 | /* --- Media Queries --- */
169 | @media (min-width: 768px) {
170 | .containerAccountTypes,
171 | .containerCategoryManagement,
172 | .containerRuleManagement{
173 |
174 | margin: 20px auto; /* Adjusted margin to match trend.css container */
175 | }
176 | .category-table-container {
177 | width: 60%;
178 | }
179 |
180 | }
181 |
182 | @media (max-width: 767px) {
183 |
184 | .category-table-container {
185 | width: 85% !important; /* Full width on mobile */
186 |
187 | }
188 |
189 | #accountTypeTable,
190 | #categoryTable,
191 | #categoryRuleTable {
192 | width: 95% !important; /* Set table width to 95% on mobile */
193 | }
194 |
195 | .containerAccountTypes,
196 | .containerCategoryManagement,
197 | .containerRuleManagement {
198 | width: 95% !important; /* Set container width to 95% on mobile */
199 | }
200 | }
201 |
202 |
203 | /* Style Datatables Dropdown */
204 | .dataTables_length select {
205 | background-color: #3c444f !important; /* Target background */
206 | color: #eee !important; /* Text color */
207 | border: 1px solid #555 !important;
208 | border-radius: 4px; /* Rounded */
209 | padding: 6px !important; /* Adjust padding */
210 | -moz-appearance: none; /* Optional: Normalize for Firefox */
211 | appearance: none; /* Optional: Normalize for other browsers */
212 | }
213 |
214 | /* Style the options inside the dropdown */
215 | .dataTables_length select option {
216 | background-color: #3c444f !important; /* Match the select background */
217 | color: #eee !important; /* Match the select text color */
218 | }
219 |
220 | /* Optional: Hover effect for options (if supported) */
221 | .dataTables_length select option:hover {
222 |
223 | background: rgba(0, 188, 212, 0.1) !important; /* Subtle blue background */
224 | transform: translateY(-2px);
225 | box-shadow: 0 0 10px rgba(0, 188, 212, 0.5), 0 0 20px rgba(0, 188, 212, 0.3); /* Glowing effect */
226 | }
227 |
228 | /* Added: Focus style */
229 | .dataTables_length select:focus {
230 | border-color: #3399cc !important; /* Border when dropdown is focused */
231 | outline: none !important; /* Remove default */
232 | }
233 |
234 | /* Ensure the parent container doesn’t interfere */
235 | .dataTables_length {
236 | background-color: transparent !important; /* Prevent any white background */
237 | }
238 |
239 |
240 | @media (max-width: 768px) {
241 | .mobile-header-logo .nav-icon {
242 | display: none;
243 | }
244 | }
245 |
246 |
247 |
248 | .light-theme .AddSubCategoryContainer,
249 | .light-theme .AddSubCategoryRuleContainer {
250 | background: #f8fafc; /* Light background color */
251 | border-color: #cccccc; /* Lighter border color */
252 | }
253 |
254 |
--------------------------------------------------------------------------------
/SparkyBudget/static/css/login.css:
--------------------------------------------------------------------------------
1 | body, html {
2 | height: 100%;
3 | width: 100%;
4 | font-family: 'Noto Sans', sans-serif;
5 | overflow: hidden;
6 | margin: 0;
7 | }
8 |
9 |
10 | * {
11 | box-sizing: border-box;
12 | }
13 |
14 | .background {
15 | position: fixed;
16 | top: 0;
17 | left: 0;
18 | height: 100%;
19 | width: 100%;
20 | background-image: url("/static/images/homepage.webp");
21 | background-position: center;
22 | background-repeat: no-repeat;
23 | background-size: cover;
24 | filter: blur(10px);
25 | opacity: 0.9;
26 | }
27 |
28 | .login {
29 | position: fixed;
30 | top: 50%;
31 | left: 50%;
32 | transform: translate(-50%, -50%);
33 | background-color: #333;
34 | padding: 2rem;
35 | max-width: 100%;
36 | text-align: center;
37 | border-radius: 1rem;
38 | color: #d6dadf;
39 | font-weight: 500;
40 | box-shadow: 0 0 2rem #d6dadf;
41 |
42 | @media screen and (max-width: 600px) {
43 | padding: 0.5rem;
44 | }
45 | }
46 |
47 | h1 {
48 | color: #f57c00;
49 | margin-top: 1.25rem;
50 | margin-bottom: 0;
51 | padding: 0;
52 | }
53 |
54 | h2 {
55 | margin-top: 0;
56 | margin-bottom: 1rem;
57 | padding: 0;
58 | }
59 |
60 | label,
61 | button,
62 | input {
63 | font-size: 1rem;
64 | }
65 |
66 | .loginfield {
67 | background-color: #d6dadf;
68 | border: none;
69 | appearance: none;
70 | -moz-appearance: none;
71 | -webkit-appearance: none;
72 | }
73 |
74 | fieldset {
75 | display: flex;
76 | justify-content: space-between;
77 | align-items: center;
78 | gap: 0.5rem;
79 | border: none;
80 | }
81 |
82 | .submit {
83 | margin-top: 1rem;
84 | padding-block: 0.5rem;
85 | padding-inline: 1rem;
86 | border-radius: 0.25rem;
87 | }
88 |
89 | .submit:hover {
90 | background-color: #abaeb2;
91 | }
92 |
93 |
94 | .input-icon-wrapper {
95 | position: relative;
96 | width: 100%;
97 | }
98 |
99 | .input-icon-wrapper i {
100 | position: absolute;
101 | top: 50%;
102 | left: 0.75rem;
103 | transform: translateY(-50%);
104 | color: #333;
105 | font-size: 1rem; /* Consistent icon size */
106 | height: 1rem;
107 | width: 1rem;
108 | display: flex;
109 | align-items: center;
110 | justify-content: center;
111 | pointer-events: none;
112 | }
113 |
114 | .input-icon-wrapper input {
115 | width: 100%;
116 | padding-left: 2.5rem; /* Leave room for icon */
117 | padding-right: 0.5rem;
118 | height: 2rem;
119 | border-radius: 0.25rem;
120 | box-sizing: border-box;
121 | line-height: 1rem;
122 | }
123 |
124 |
125 | @media (max-width: 600px) {
126 | h1 {
127 | height: 100%;
128 | width: 100%;
129 | font-size: Larger;
130 | }
131 | .login {
132 | height: 50%;
133 | width: 90%;
134 | }
135 | }
--------------------------------------------------------------------------------
/SparkyBudget/static/css/navigation_bar.css:
--------------------------------------------------------------------------------
1 | /* This CSS file is for the navigation bar at the top of the page */
2 | .header-container {
3 | display: flex;
4 | align-items: center;
5 | justify-content: space-between;
6 | height: 60px;
7 | background: #333;
8 | backdrop-filter: blur(10px);
9 | padding: 0 2rem;
10 | color: #fff;
11 | z-index: 1000;
12 | }
13 |
14 | /* Gradient line at the bottom of the top nav */
15 | .header-container::before {
16 | content: "";
17 | position: absolute;
18 | bottom: 0;
19 | top: auto;
20 | left: 0;
21 | right: 0;
22 | height: 1px;
23 | background: linear-gradient(to right, #feae00, #fe4f92);
24 | }
25 |
26 | .header-logo {
27 | display: flex;
28 | align-items: center;
29 | gap: 1rem;
30 | }
31 |
32 | .header-logo img {
33 | height: 40px;
34 | width: auto;
35 | filter: drop-shadow(0 0 5px rgba(0, 242, 254, 0.5));
36 | }
37 |
38 | /* Logo text visible by default */
39 | .header-logo h1 {
40 | font-size: 1.5rem !important;
41 | margin: 0;
42 | color: #f57c00; /* Apply the desired color */
43 | /* Remove gradient styles that conflict */
44 | -webkit-background-clip: unset;
45 | -webkit-text-fill-color: unset;
46 | background-clip: unset;
47 | display: block;
48 | }
49 |
50 | .header-nav {
51 | display: flex;
52 | align-items: center;
53 | gap: 1.5rem;
54 | width: auto;
55 | justify-content: flex-end;
56 | }
57 |
58 | .nav-item {
59 | color: #f8fafc;
60 | font-weight: 500; /* Explicitly set font weight */
61 | font-size: 1rem; /* Explicitly set font size */
62 | padding: 0.5rem 1rem;
63 | border-radius: 8px;
64 | transition: background 0.3s ease; /* Only transition background */
65 | display: inline-flex;
66 | align-items: center;
67 | gap: 0.5rem;
68 | text-decoration: none; /* Explicitly no underline */
69 | }
70 |
71 | .nav-item:hover {
72 | background: rgba(0, 242, 254, 0.1); /* Background hover effect */
73 | }
74 |
75 | /* Hover effect only for text */
76 | .nav-item:hover .nav-text {
77 | color: #00f2fe;
78 | }
79 |
80 | /* Ensure icon stays static */
81 | .nav-icon {
82 | font-size: 1.5rem;
83 | color: #f8fafc;
84 | transition: none; /* No changes on hover */
85 | }
86 |
87 | /* Prevent any hover changes to icon */
88 | .nav-item:hover .nav-icon {
89 | color: #f8fafc; /* Keep original color */
90 | }
91 |
92 | /* Active state */
93 | .nav-item.active {
94 | color: #00f2fe;
95 | background: rgba(0, 242, 254, 0.15);
96 | font-weight: 500; /* Ensure consistent font weight in active state */
97 | font-size: 1rem; /* Ensure consistent font size in active state */
98 | }
99 |
100 | .nav-item.active .nav-icon {
101 | color: #f8fafc; /* Icon stays unchanged */
102 | }
103 |
104 | .nav-text {
105 | display: inline;
106 | }
107 |
108 | /* Default behavior for larger screens (header at the top) */
109 | @media (min-width: 769px) {
110 | .header-container {
111 | position: relative;
112 | }
113 |
114 | body {
115 |
116 | padding-bottom: 0;
117 | margin: 0;
118 | }
119 | }
120 |
121 | /* Mobile devices (header at the bottom) */
122 | @media (max-width: 768px) {
123 | .header-container {
124 | position: fixed;
125 | bottom: 0;
126 | left: 0;
127 | width: 100%;
128 | border-bottom: none;
129 | border-top: 10px solid #1d1d1d;
130 | background: #333;
131 |
132 | }
133 |
134 | /* Move gradient line to the top of the bottom nav */
135 | .header-container::before {
136 | top: 0;
137 | bottom: auto;
138 | }
139 |
140 | /* Hide logo text on mobile */
141 | .header-logo h1 {
142 | display: none;
143 | }
144 |
145 | /* Center logo image if text is hidden */
146 | .header-logo {
147 | justify-content: center;
148 | }
149 |
150 | .header-nav {
151 | width: 100%;
152 | justify-content: space-around;
153 | gap: 0.5rem;
154 | }
155 |
156 | /* Hide nav text on mobile */
157 | .nav-text {
158 | display: none;
159 | }
160 |
161 | /* Adjust nav items for icon-only display */
162 | .nav-item {
163 | padding: 0.5rem;
164 | gap: 0;
165 | justify-content: center;
166 | text-decoration: none;
167 | }
168 |
169 | .nav-icon {
170 | font-size: 1.8rem;
171 | color: #f8fafc;
172 | transition: none;
173 | }
174 |
175 | .nav-item:hover .nav-icon {
176 | color: #f8fafc;
177 | }
178 |
179 | body {
180 | padding-top: 0;
181 | padding-bottom: 70px; /* Height (60px) + border (10px) */
182 | margin: 0;
183 | }
184 |
185 | }
186 |
187 |
188 | /* Default: Hide mobile header logo */
189 | .mobile-header-logo {
190 | display: none;
191 | }
192 |
193 | /* Show mobile header logo only on mobile (max-width: 768px) */
194 | @media (max-width: 768px) {
195 | .mobile-header-logo {
196 | display: flex;
197 | align-items: center;
198 | background: #333;
199 | font-size: small;
200 | }
201 |
202 | .mobile-header-logo img {
203 | height: 40px;
204 | width: auto;
205 | filter: drop-shadow(0 0 5px rgba(0, 242, 254, 0.5));
206 | }
207 |
208 | /* Optionally hide desktop logo on mobile */
209 | .header-logo {
210 | display: none;
211 | }
212 | .mobile-header-logo h1 {
213 | font-size: 1.5rem; /* Adjust this value as needed (e.g., 1.2rem, 1rem) */
214 | padding-top: 10px;
215 | color: #f57c00; /* Apply the desired color for mobile */
216 | }
217 |
218 | }
219 |
220 |
221 | .spinning {
222 | animation: spin 1s linear infinite;
223 | }
224 |
225 | @keyframes spin {
226 | 0% { transform: rotate(0deg); }
227 | 100% { transform: rotate(360deg); }
228 | }
229 |
230 | .nav-item {
231 | pointer-events: auto;
232 | }
233 |
234 | .nav-text {
235 | display: inline; /* Desktop */
236 | pointer-events: auto;
237 | user-select: auto;
238 | }
239 |
240 | @media (max-width: 768px) {
241 | .nav-text {
242 | display: none; /* Mobile */
243 | }
244 | }
245 |
246 | /* Light theme styles for navigation bar */
247 | .light-theme .header-container {
248 | background: #f8fafc; /* Light background color */
249 | color: #333; /* Dark font color */
250 | }
251 |
252 | .light-theme .header-container::before {
253 | background: linear-gradient(to right, #007bff, #6c757d); /* Adjust gradient for light theme */
254 | }
255 |
256 | .light-theme .nav-item {
257 | color: #333; /* Dark font color for nav items */
258 | }
259 |
260 | .light-theme .nav-item:hover {
261 | background: rgba(0, 123, 255, 0.1); /* Light theme hover background */
262 | }
263 |
264 | .light-theme .nav-item:hover .nav-text {
265 | color: #007bff; /* Light theme hover text color */
266 | }
267 |
268 | .light-theme .nav-icon {
269 | color: #333; /* Dark icon color */
270 | }
271 |
272 | .light-theme .nav-item.active {
273 | color: #007bff; /* Light theme active text color */
274 | background: rgba(0, 123, 255, 0.15); /* Light theme active background */
275 | }
276 |
277 | .light-theme .nav-item.active .nav-icon {
278 | color: #333; /* Light theme active icon color */
279 | }
280 |
281 | .light-theme .nav-item:hover .nav-icon {
282 | color: #333; /* Keep dark icon color on hover in light theme */
283 | }
284 |
285 | .light-theme .mobile-header-logo {
286 | background: #f8fafc; /* Light background for mobile logo */
287 | }
288 |
289 | .light-theme .mobile-header-logo h1 {
290 | color: #333; /* Dark font color for mobile logo text */
291 | }
--------------------------------------------------------------------------------
/SparkyBudget/static/images/Sparky.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodeWithCJ/SparkyBudget/c4c550580c72e7bcfdb171542351fa2d3a7a73eb/SparkyBudget/static/images/Sparky.jpg
--------------------------------------------------------------------------------
/SparkyBudget/static/images/SparkyBudget.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodeWithCJ/SparkyBudget/c4c550580c72e7bcfdb171542351fa2d3a7a73eb/SparkyBudget/static/images/SparkyBudget.png
--------------------------------------------------------------------------------
/SparkyBudget/static/images/budget_icons/ ICE Skating.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodeWithCJ/SparkyBudget/c4c550580c72e7bcfdb171542351fa2d3a7a73eb/SparkyBudget/static/images/budget_icons/ ICE Skating.png
--------------------------------------------------------------------------------
/SparkyBudget/static/images/budget_icons/Auto Gas.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodeWithCJ/SparkyBudget/c4c550580c72e7bcfdb171542351fa2d3a7a73eb/SparkyBudget/static/images/budget_icons/Auto Gas.png
--------------------------------------------------------------------------------
/SparkyBudget/static/images/budget_icons/Auto Insurance.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodeWithCJ/SparkyBudget/c4c550580c72e7bcfdb171542351fa2d3a7a73eb/SparkyBudget/static/images/budget_icons/Auto Insurance.png
--------------------------------------------------------------------------------
/SparkyBudget/static/images/budget_icons/Auto Payment.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodeWithCJ/SparkyBudget/c4c550580c72e7bcfdb171542351fa2d3a7a73eb/SparkyBudget/static/images/budget_icons/Auto Payment.png
--------------------------------------------------------------------------------
/SparkyBudget/static/images/budget_icons/BAP.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodeWithCJ/SparkyBudget/c4c550580c72e7bcfdb171542351fa2d3a7a73eb/SparkyBudget/static/images/budget_icons/BAP.png
--------------------------------------------------------------------------------
/SparkyBudget/static/images/budget_icons/Bank Fee.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodeWithCJ/SparkyBudget/c4c550580c72e7bcfdb171542351fa2d3a7a73eb/SparkyBudget/static/images/budget_icons/Bank Fee.png
--------------------------------------------------------------------------------
/SparkyBudget/static/images/budget_icons/Books.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodeWithCJ/SparkyBudget/c4c550580c72e7bcfdb171542351fa2d3a7a73eb/SparkyBudget/static/images/budget_icons/Books.png
--------------------------------------------------------------------------------
/SparkyBudget/static/images/budget_icons/CC Rewards.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodeWithCJ/SparkyBudget/c4c550580c72e7bcfdb171542351fa2d3a7a73eb/SparkyBudget/static/images/budget_icons/CC Rewards.png
--------------------------------------------------------------------------------
/SparkyBudget/static/images/budget_icons/Cash Withdrawal.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodeWithCJ/SparkyBudget/c4c550580c72e7bcfdb171542351fa2d3a7a73eb/SparkyBudget/static/images/budget_icons/Cash Withdrawal.png
--------------------------------------------------------------------------------
/SparkyBudget/static/images/budget_icons/Clothing.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodeWithCJ/SparkyBudget/c4c550580c72e7bcfdb171542351fa2d3a7a73eb/SparkyBudget/static/images/budget_icons/Clothing.png
--------------------------------------------------------------------------------
/SparkyBudget/static/images/budget_icons/Dance Class.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodeWithCJ/SparkyBudget/c4c550580c72e7bcfdb171542351fa2d3a7a73eb/SparkyBudget/static/images/budget_icons/Dance Class.png
--------------------------------------------------------------------------------
/SparkyBudget/static/images/budget_icons/Doctor.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodeWithCJ/SparkyBudget/c4c550580c72e7bcfdb171542351fa2d3a7a73eb/SparkyBudget/static/images/budget_icons/Doctor.png
--------------------------------------------------------------------------------
/SparkyBudget/static/images/budget_icons/EZ Pass.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodeWithCJ/SparkyBudget/c4c550580c72e7bcfdb171542351fa2d3a7a73eb/SparkyBudget/static/images/budget_icons/EZ Pass.png
--------------------------------------------------------------------------------
/SparkyBudget/static/images/budget_icons/Eating Outside.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodeWithCJ/SparkyBudget/c4c550580c72e7bcfdb171542351fa2d3a7a73eb/SparkyBudget/static/images/budget_icons/Eating Outside.png
--------------------------------------------------------------------------------
/SparkyBudget/static/images/budget_icons/Electronics and Software.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodeWithCJ/SparkyBudget/c4c550580c72e7bcfdb171542351fa2d3a7a73eb/SparkyBudget/static/images/budget_icons/Electronics and Software.png
--------------------------------------------------------------------------------
/SparkyBudget/static/images/budget_icons/Entertainment.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodeWithCJ/SparkyBudget/c4c550580c72e7bcfdb171542351fa2d3a7a73eb/SparkyBudget/static/images/budget_icons/Entertainment.png
--------------------------------------------------------------------------------
/SparkyBudget/static/images/budget_icons/Food and Dining.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodeWithCJ/SparkyBudget/c4c550580c72e7bcfdb171542351fa2d3a7a73eb/SparkyBudget/static/images/budget_icons/Food and Dining.png
--------------------------------------------------------------------------------
/SparkyBudget/static/images/budget_icons/Gas and Electric.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodeWithCJ/SparkyBudget/c4c550580c72e7bcfdb171542351fa2d3a7a73eb/SparkyBudget/static/images/budget_icons/Gas and Electric.png
--------------------------------------------------------------------------------
/SparkyBudget/static/images/budget_icons/Groceries.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodeWithCJ/SparkyBudget/c4c550580c72e7bcfdb171542351fa2d3a7a73eb/SparkyBudget/static/images/budget_icons/Groceries.png
--------------------------------------------------------------------------------
/SparkyBudget/static/images/budget_icons/Hair.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodeWithCJ/SparkyBudget/c4c550580c72e7bcfdb171542351fa2d3a7a73eb/SparkyBudget/static/images/budget_icons/Hair.png
--------------------------------------------------------------------------------
/SparkyBudget/static/images/budget_icons/Home Applicances.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodeWithCJ/SparkyBudget/c4c550580c72e7bcfdb171542351fa2d3a7a73eb/SparkyBudget/static/images/budget_icons/Home Applicances.png
--------------------------------------------------------------------------------
/SparkyBudget/static/images/budget_icons/Home Improvement.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodeWithCJ/SparkyBudget/c4c550580c72e7bcfdb171542351fa2d3a7a73eb/SparkyBudget/static/images/budget_icons/Home Improvement.png
--------------------------------------------------------------------------------
/SparkyBudget/static/images/budget_icons/Home Insurance.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodeWithCJ/SparkyBudget/c4c550580c72e7bcfdb171542351fa2d3a7a73eb/SparkyBudget/static/images/budget_icons/Home Insurance.png
--------------------------------------------------------------------------------
/SparkyBudget/static/images/budget_icons/Hula Hoop Class.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodeWithCJ/SparkyBudget/c4c550580c72e7bcfdb171542351fa2d3a7a73eb/SparkyBudget/static/images/budget_icons/Hula Hoop Class.png
--------------------------------------------------------------------------------
/SparkyBudget/static/images/budget_icons/ICE Skating.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodeWithCJ/SparkyBudget/c4c550580c72e7bcfdb171542351fa2d3a7a73eb/SparkyBudget/static/images/budget_icons/ICE Skating.png
--------------------------------------------------------------------------------
/SparkyBudget/static/images/budget_icons/India Ticket.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodeWithCJ/SparkyBudget/c4c550580c72e7bcfdb171542351fa2d3a7a73eb/SparkyBudget/static/images/budget_icons/India Ticket.png
--------------------------------------------------------------------------------
/SparkyBudget/static/images/budget_icons/Interest Income.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodeWithCJ/SparkyBudget/c4c550580c72e7bcfdb171542351fa2d3a7a73eb/SparkyBudget/static/images/budget_icons/Interest Income.png
--------------------------------------------------------------------------------
/SparkyBudget/static/images/budget_icons/Internet.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodeWithCJ/SparkyBudget/c4c550580c72e7bcfdb171542351fa2d3a7a73eb/SparkyBudget/static/images/budget_icons/Internet.png
--------------------------------------------------------------------------------
/SparkyBudget/static/images/budget_icons/Jewellery.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodeWithCJ/SparkyBudget/c4c550580c72e7bcfdb171542351fa2d3a7a73eb/SparkyBudget/static/images/budget_icons/Jewellery.png
--------------------------------------------------------------------------------
/SparkyBudget/static/images/budget_icons/Mobile Phone.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodeWithCJ/SparkyBudget/c4c550580c72e7bcfdb171542351fa2d3a7a73eb/SparkyBudget/static/images/budget_icons/Mobile Phone.png
--------------------------------------------------------------------------------
/SparkyBudget/static/images/budget_icons/Parking.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodeWithCJ/SparkyBudget/c4c550580c72e7bcfdb171542351fa2d3a7a73eb/SparkyBudget/static/images/budget_icons/Parking.png
--------------------------------------------------------------------------------
/SparkyBudget/static/images/budget_icons/Paycheck.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodeWithCJ/SparkyBudget/c4c550580c72e7bcfdb171542351fa2d3a7a73eb/SparkyBudget/static/images/budget_icons/Paycheck.png
--------------------------------------------------------------------------------
/SparkyBudget/static/images/budget_icons/Pharmacy.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodeWithCJ/SparkyBudget/c4c550580c72e7bcfdb171542351fa2d3a7a73eb/SparkyBudget/static/images/budget_icons/Pharmacy.png
--------------------------------------------------------------------------------
/SparkyBudget/static/images/budget_icons/Rent.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodeWithCJ/SparkyBudget/c4c550580c72e7bcfdb171542351fa2d3a7a73eb/SparkyBudget/static/images/budget_icons/Rent.png
--------------------------------------------------------------------------------
/SparkyBudget/static/images/budget_icons/Service Fee.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodeWithCJ/SparkyBudget/c4c550580c72e7bcfdb171542351fa2d3a7a73eb/SparkyBudget/static/images/budget_icons/Service Fee.png
--------------------------------------------------------------------------------
/SparkyBudget/static/images/budget_icons/Shopping.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodeWithCJ/SparkyBudget/c4c550580c72e7bcfdb171542351fa2d3a7a73eb/SparkyBudget/static/images/budget_icons/Shopping.png
--------------------------------------------------------------------------------
/SparkyBudget/static/images/budget_icons/Subscription.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodeWithCJ/SparkyBudget/c4c550580c72e7bcfdb171542351fa2d3a7a73eb/SparkyBudget/static/images/budget_icons/Subscription.png
--------------------------------------------------------------------------------
/SparkyBudget/static/images/budget_icons/Swimming Class.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodeWithCJ/SparkyBudget/c4c550580c72e7bcfdb171542351fa2d3a7a73eb/SparkyBudget/static/images/budget_icons/Swimming Class.png
--------------------------------------------------------------------------------
/SparkyBudget/static/images/budget_icons/Tamil School.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodeWithCJ/SparkyBudget/c4c550580c72e7bcfdb171542351fa2d3a7a73eb/SparkyBudget/static/images/budget_icons/Tamil School.png
--------------------------------------------------------------------------------
/SparkyBudget/static/images/budget_icons/Taxi.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodeWithCJ/SparkyBudget/c4c550580c72e7bcfdb171542351fa2d3a7a73eb/SparkyBudget/static/images/budget_icons/Taxi.png
--------------------------------------------------------------------------------
/SparkyBudget/static/images/budget_icons/Train.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodeWithCJ/SparkyBudget/c4c550580c72e7bcfdb171542351fa2d3a7a73eb/SparkyBudget/static/images/budget_icons/Train.png
--------------------------------------------------------------------------------
/SparkyBudget/static/images/budget_icons/USMLE.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodeWithCJ/SparkyBudget/c4c550580c72e7bcfdb171542351fa2d3a7a73eb/SparkyBudget/static/images/budget_icons/USMLE.png
--------------------------------------------------------------------------------
/SparkyBudget/static/images/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodeWithCJ/SparkyBudget/c4c550580c72e7bcfdb171542351fa2d3a7a73eb/SparkyBudget/static/images/favicon.ico
--------------------------------------------------------------------------------
/SparkyBudget/static/images/homepage.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodeWithCJ/SparkyBudget/c4c550580c72e7bcfdb171542351fa2d3a7a73eb/SparkyBudget/static/images/homepage.webp
--------------------------------------------------------------------------------
/SparkyBudget/static/images/logout.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/SparkyBudget/static/images/money-bill-trend-up-solid.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/SparkyBudget/static/images/refresh.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/SparkyBudget/static/js/balance_summary.js:
--------------------------------------------------------------------------------
1 | $(document).ready(function () {
2 | // Add click event for expanding/collapsing rows
3 | $('.type-row td').click(function () {
4 | var $parentRow = $(this).closest('tr');
5 | var $childRows = $parentRow.nextUntil(':not(.child-row)');
6 |
7 | if ($parentRow.hasClass('expanded')) {
8 | // Collapse child rows smoothly
9 | $childRows.each(function () {
10 | var $row = $(this);
11 | $row.css({
12 | maxHeight: $row.outerHeight() + 'px',
13 | opacity: 1
14 | }).animate({
15 | maxHeight: 0,
16 | opacity: 0
17 | }, 500, function () {
18 | $row.hide(); // Hide after animation completes
19 | });
20 | });
21 | } else {
22 | // Expand child rows smoothly
23 | $childRows.each(function () {
24 | var $row = $(this);
25 | $row.show(); // Show immediately
26 | var height = $row.get(0).scrollHeight; // Get full height of the row
27 | $row.css({
28 | maxHeight: 0,
29 | opacity: 0
30 | }).animate({
31 | maxHeight: height + 'px', // Animate to full height
32 | opacity: 1
33 | }, 500);
34 | });
35 | }
36 |
37 | $parentRow.toggleClass('expanded');
38 | });
39 |
40 |
41 |
42 |
43 |
44 |
45 | // Create a bar chart
46 | const balanceChart = document.getElementById('balanceChart');
47 | if (balanceChart) {
48 | const ctx = balanceChart.getContext('2d');
49 | var myChart = new Chart(ctx, {
50 | type: 'bar',
51 | data: {
52 | labels: window.appState.labels,
53 | datasets: [{
54 | label: 'Balance',
55 | data: window.appState.balances,
56 | backgroundColor: [
57 | '#32A45F', // Color for the first account type
58 | '#90EBFC', // Color for the second account type
59 | '#846BFF',
60 | '#FF8E5F',
61 | // Add more colors as needed
62 | ],
63 | borderColor: 'white', // Adjust color as needed
64 | borderWidth: 2
65 | }]
66 | },
67 | options: {
68 | indexAxis: 'y', // Set the index axis to 'y' for a vertical bar chart
69 | scales: {
70 | x: {
71 | beginAtZero: true,
72 | ticks: {
73 | color: 'white', // Set x-axis label color to white
74 | fontSize: 14 // Set font size for x-axis labels
75 | }
76 | },
77 | y: {
78 | beginAtZero: true,
79 | ticks: {
80 | color: 'white', // Set y-axis label color to white
81 | fontSize: 14 // Set font size for y-axis labels
82 | }
83 | }
84 | },
85 | maintainAspectRatio: false, // Make the chart responsive
86 | aspectRatio: 8, // Adjust this value to control the height of the chart
87 | plugins: {
88 | legend: {
89 | display: false,
90 |
91 | },
92 | datalabels: {
93 |
94 | color: 'white', // Set the color of the datalabels
95 | font: {
96 | size: 14 // Set the font size of the datalabels
97 | },
98 | anchor: 'end',
99 | align: 'end'
100 |
101 |
102 | }
103 | }
104 | }
105 | });
106 | }
107 | });
108 |
109 | document.addEventListener('DOMContentLoaded', function () {
110 | // Toggle the collapsed class on button click
111 | document.getElementById('toggleBalanceSummary').addEventListener('click', function () {
112 | document.querySelector('.balance-summary-container').classList.toggle('collapsed');
113 | });
114 | });
115 |
116 |
117 |
118 | $(document).ready(function () {
119 | // Toggle balance summary visibility and icon style
120 | $('#toggleBalanceSummary').on('click', function () {
121 | const $balanceSummary = $('.balance-summary-container');
122 | const $icon = $(this).find('i');
123 | $balanceSummary.toggle();
124 | });
125 |
126 | // Update arrow icon when collapse is shown
127 | $('.account-type-header[data-toggle="collapse"]').on('show.bs.collapse', function () {
128 | console.log('Collapse is being shown for:', $(this).find('.account-name').text());
129 | const $icon = $(this).find('.expand-icon');
130 | $icon.text('▼');
131 | });
132 |
133 | // Update arrow icon when collapse is hidden
134 | $('.account-type-header[data-toggle="collapse"]').on('hide.bs.collapse', function () {
135 | console.log('Collapse is being hidden for:', $(this).find('.account-name').text());
136 | const $icon = $(this).find('.expand-icon');
137 | $icon.text('▶');
138 | });
139 |
140 | // Initialize arrows (all collapsed by default)
141 | $('.account-type-header[data-toggle="collapse"]').each(function () {
142 | $(this).find('.expand-icon').text('▶');
143 | });
144 | });
--------------------------------------------------------------------------------
/SparkyBudget/static/js/budget.js:
--------------------------------------------------------------------------------
1 | //static/js/recurring_budget_details.js
2 | // Add any interactivity or functionality for the recurring budget details page here
3 |
4 | // Example: Highlight rows on hover
5 | document.addEventListener("DOMContentLoaded", function () {
6 | const rows = document.querySelectorAll(".recurring-budget-container table tbody tr");
7 | rows.forEach(row => {
8 | row.addEventListener("mouseenter", () => {
9 | row.style.backgroundColor = "#d1ecf1";
10 | });
11 | row.addEventListener("mouseleave", () => {
12 | row.style.backgroundColor = "";
13 | });
14 | });
15 | });
16 |
17 | $(document).ready(function () {
18 | // Fetch subcategories using an AJAX request
19 | $.get('/getDistinctSubcategories', function (subcategories) {
20 | if (subcategories && subcategories.length > 0) {
21 | // Format the data for select2
22 | const formattedSubcategories = subcategories.map(subcategory => ({
23 | id: subcategory,
24 | text: subcategory
25 | }));
26 |
27 | // Initialize the select2 dropdown with a custom width
28 | $('#subCategoryInput').select2({
29 | data: formattedSubcategories,
30 | width: '40%', // Set the width to 40%
31 | placeholder: 'Select',
32 | allowClear: true,
33 | });
34 | } else {
35 | console.error('No subcategories found.');
36 | alert('No subcategories available to populate the dropdown.');
37 | }
38 | }).fail(function (error) {
39 | console.error('Error fetching subcategories:', error);
40 | alert('An error occurred while fetching subcategories.');
41 | });
42 | });
43 |
44 | // Function to delete a recurring budget record
45 | function deleteRecurringBudget(subCategory) {
46 | if (confirm(`Are you sure you want to delete the recurring budget for ${subCategory}?`)) {
47 | // Send a DELETE request to the server
48 | fetch(`/delete_recurring_budget`, {
49 | method: 'POST',
50 | headers: {
51 | 'Content-Type': 'application/json',
52 | },
53 | body: JSON.stringify({ subCategory: subCategory }),
54 | })
55 | .then(response => response.json())
56 | .then(data => {
57 | if (data.success) {
58 | alert(data.message);
59 | // Refresh the table by reloading the page
60 | location.reload();
61 | } else {
62 | alert(`Error: ${data.message}`);
63 | }
64 | })
65 | .catch(error => {
66 | console.error('Error:', error);
67 | alert('An error occurred while deleting the recurring budget.');
68 | });
69 | }
70 | }
71 |
72 |
73 |
74 | // Function to handle form submission
75 | function addRecurringBudget() {
76 | // Get the form data
77 | const subCategory = $('#subCategoryInput').val();
78 | const budgetAmount = $('#budgetAmountInput').val();
79 |
80 | // Validate the form data
81 | if (!subCategory || !budgetAmount) {
82 | alert('Please fill out all fields.');
83 | return;
84 | }
85 |
86 | // Send the data to the server
87 | fetch('/add_recurring_budget', {
88 | method: 'POST',
89 | headers: {
90 | 'Content-Type': 'application/json',
91 | },
92 | body: JSON.stringify({
93 | subCategory: subCategory,
94 | budgetAmount: parseFloat(budgetAmount),
95 | }),
96 | })
97 | .then(response => response.json())
98 | .then(data => {
99 | if (data.success) {
100 | alert(data.message);
101 | // Refresh the table
102 | location.reload();
103 | } else {
104 | alert(`Error: ${data.message}`);
105 | }
106 | })
107 | .catch(error => {
108 | console.error('Error:', error);
109 | alert('An error occurred while adding the recurring budget.');
110 | });
111 | }
--------------------------------------------------------------------------------
/SparkyBudget/static/js/budget_summary.js:
--------------------------------------------------------------------------------
1 | var editedRow; // Declare editedRow at a higher scope
2 |
3 | $(document).ready(function () {
4 | var table = $('#budgetsummaryTable').DataTable({
5 | "pageLength": 10,
6 | "lengthMenu": [
7 | [5, 10, 25, -1],
8 | [5, 10, 25, "All"]
9 | ],
10 | "autoWidth": false,
11 | "columns": [
12 | null, // Category
13 | null, // Sub Category
14 | { "orderable": false, "className": "editButton" }, // Budget (make it non-orderable and add a class)
15 | null, // Spent
16 | null // Balance
17 | ]
18 | });
19 |
20 | $('#budgetsummaryTable tbody').on('click', 'button.editButton', function () {
21 | var index = $(this).data('index');
22 | console.log('Edit button clicked for index:', index);
23 | enableInlineEdit(index);
24 | });
25 |
26 | $(document).on('click', function (e) {
27 | if (!$(e.target).closest('.editable').length && editedRow !== undefined) {
28 | console.log('Click outside editable area, updating budget for index:', editedRow);
29 | updateBudget(editedRow);
30 | }
31 | });
32 |
33 | $('#budgetsummaryTable tbody').on('click', 'td.editable', function () {
34 | var index = $(this).closest('tr').index();
35 | console.log('Editable cell clicked, enabling inline edit for index:', index);
36 | enableInlineEdit(index);
37 | });
38 |
39 | function enableInlineEdit(index) {
40 | console.log('Enabling inline edit for index:', index);
41 | var row = $('#budgetsummaryTable').DataTable().row(index).nodes().to$();
42 | var budgetValue = row.find('.budgetValue').text();
43 |
44 | // Hide the span and show the input field
45 | row.find('.budgetValue').hide();
46 | row.find('.editInput').show().val(budgetValue).focus();
47 |
48 | // Store the edited row information in a data attribute
49 | editedRow = index;
50 | row.data('editedRow', index);
51 | console.log('New value is', editedRow);
52 | }
53 |
54 | function updateBudget(index) {
55 | var row = $('#budgetsummaryTable').DataTable().row(index).nodes().to$();
56 | var editedValue = row.find('.editInput').val();
57 |
58 | var selectedYear = '2023';//$('#transactionYear').val();
59 | var selectedMonth = '12'; //$('#monthButtons .active').data('month');
60 | var selectedSubcategory = row.find('td:eq(1)').text();
61 | console.log('data written are ', selectedYear, selectedMonth, selectedSubcategory, editedValue);
62 |
63 | $.ajax({
64 | url: '/inline_edit_budget', // Change the URL to match your Flask route
65 | type: 'POST',
66 | contentType: 'application/json', // Add this line to specify JSON content type
67 | data: JSON.stringify({
68 | year: selectedYear, // Make sure this variable is defined
69 | month: selectedMonth,
70 | subcategory: selectedSubcategory,
71 | budget: editedValue
72 | }),
73 | success: function (data, textStatus, jqXHR) {
74 | console.log('Budget updated successfully:', data);
75 | row.find('.budgetValue').text(editedValue);
76 | disableInlineEdit(row);
77 | },
78 | error: function (jqXHR, textStatus, errorThrown) {
79 | console.error('Error updating budget:', textStatus);
80 | }
81 | });
82 | }
83 |
84 | function disableInlineEdit(row) {
85 | row.find('.budgetValue').show();
86 | row.find('.editInput').hide();
87 | // Clear the edited row information
88 | row.data('editedRow', null);
89 | editedRow = undefined; // Reset editedRow
90 | }
91 | });
--------------------------------------------------------------------------------
/SparkyBudget/static/js/budget_transaction_details.js:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodeWithCJ/SparkyBudget/c4c550580c72e7bcfdb171542351fa2d3a7a73eb/SparkyBudget/static/js/budget_transaction_details.js
--------------------------------------------------------------------------------
/SparkyBudget/static/js/navigation_bar.js:
--------------------------------------------------------------------------------
1 | $(document).ready(function () {
2 | $(document).on('click', '#syncNavItem, #syncNavItem .nav-text', function (e) {
3 | e.preventDefault();
4 | console.log('Sync clicked:', e.target);
5 | $('#downloadButton').addClass('spinning');
6 | $.ajax({
7 | url: '/download_data',
8 | type: 'POST',
9 | success: function (response) {
10 | $('#downloadButton').removeClass('spinning');
11 | if (response.success) {
12 | alert(response.message);
13 | location.reload();
14 | } else {
15 | alert('Error: ' + response.message);
16 | }
17 | },
18 | error: function (error) {
19 | $('#downloadButton').removeClass('spinning');
20 | alert('Error: ' + error.statusText);
21 | }
22 | });
23 | });
24 | });
--------------------------------------------------------------------------------
/SparkyBudget/static/js/script.js:
--------------------------------------------------------------------------------
1 | function toggleRow(element) {
2 | console.log('Toggle function called');
3 | var row = element.closest('tr');
4 | var nextRow = row.nextElementSibling;
5 |
6 | if (nextRow && nextRow.classList.contains('child-row')) {
7 | if (nextRow.style.display === 'none') {
8 | console.log('Expanding row');
9 | nextRow.style.display = '';
10 | element.innerHTML = '▼'; // Down arrow
11 | } else {
12 | console.log('Collapsing row');
13 | nextRow.style.display = 'none';
14 | element.innerHTML = '▶'; // Right arrow
15 | }
16 | } else {
17 | // If the next row is not a direct sibling, search in the nested tbody
18 | nextRow = row.parentElement.querySelector('.child-row');
19 | if (nextRow) {
20 | if (nextRow.style.display === 'none') {
21 | console.log('Expanding row');
22 | nextRow.style.display = '';
23 | element.innerHTML = '▼'; // Down arrow
24 | } else {
25 | console.log('Collapsing row');
26 | nextRow.style.display = 'none';
27 | element.innerHTML = '▶'; // Right arrow
28 | }
29 | }
30 | }
31 | }
32 |
33 | function balanceDetailsTableToggleVisibility() {
34 | var balanceDetailsTable = document.getElementById('balanceDetailsTable');
35 | var isHidden = balanceDetailsTable.classList.contains('hidden-balance-details');
36 |
37 | if (isHidden) {
38 | balanceDetailsTable.classList.remove('hidden-balance-details');
39 | } else {
40 | balanceDetailsTable.classList.add('hidden-balance-details');
41 | }
42 | }
--------------------------------------------------------------------------------
/SparkyBudget/static/js/theme.js:
--------------------------------------------------------------------------------
1 | document.addEventListener('DOMContentLoaded', () => {
2 | const themeToggle = document.getElementById('themeToggle');
3 | const body = document.body;
4 | const currentTheme = localStorage.getItem('theme');
5 |
6 | // Apply saved theme on load
7 | if (currentTheme) {
8 | body.classList.add(currentTheme);
9 | updateThemeIcon(currentTheme);
10 | } else {
11 | // Default to dark theme if no preference is saved
12 | body.classList.add('dark-theme'); // Assuming dark-theme is the default
13 | updateThemeIcon('dark-theme');
14 | }
15 |
16 | themeToggle.addEventListener('click', (e) => {
17 | e.stopPropagation();
18 | if (body.classList.contains('dark-theme')) {
19 | body.classList.remove('dark-theme');
20 | body.classList.add('light-theme');
21 | localStorage.setItem('theme', 'light-theme');
22 | updateThemeIcon('light-theme');
23 | } else {
24 | body.classList.remove('light-theme');
25 | body.classList.add('dark-theme');
26 | localStorage.setItem('theme', 'dark-theme');
27 | updateThemeIcon('dark-theme');
28 | }
29 | });
30 |
31 | function updateThemeIcon(theme) {
32 | const icon = themeToggle.querySelector('i');
33 | if (theme === 'light-theme') {
34 | icon.classList.remove('fa-sun');
35 | icon.classList.add('fa-moon');
36 | } else {
37 | icon.classList.remove('fa-moon');
38 | icon.classList.add('fa-sun');
39 | }
40 | }
41 | });
--------------------------------------------------------------------------------
/SparkyBudget/static/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "SparkyBudget",
3 | "short_name": "SparkyBudget",
4 | "start_url": "/",
5 | "display": "standalone",
6 | "background_color": "#ffffff",
7 | "theme_color": "#f57c00",
8 | "icons": [
9 | {
10 | "src": "/static/images/SparkyBudget.png",
11 | "sizes": "192x192",
12 | "type": "image/png"
13 | },
14 | {
15 | "src": "/static/images/SparkyBudget.png",
16 | "sizes": "512x512",
17 | "type": "image/png"
18 | }
19 | ]
20 | }
--------------------------------------------------------------------------------
/SparkyBudget/templates/balance_details.html.jinja:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
66 |
67 |
68 |
69 |
105 |
106 |
107 | {% set grouped_by_bank = bank_account_name_balance_details | groupby(2) %}
108 | {% for bank, accounts in grouped_by_bank %}
109 |
110 |
{{ bank }}
111 | {% for account in accounts %}
112 |
113 |
126 |
127 |
128 | Account Type:
129 | {{ account[1] }}
130 |
131 |
132 | Available Balance:
133 | ${{ account[6] }}
134 |
135 |
136 |
137 | {% endfor %}
138 |
139 | {% endfor %}
140 |
141 |
142 |
143 |
144 |
145 |
146 |
147 |
148 |
149 |
150 |
--------------------------------------------------------------------------------
/SparkyBudget/templates/balance_summary.html.jinja:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Balance Summary
8 |
9 |
10 |
11 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 | {% for data in account_type_data %}
25 |
26 | {% set show_arrow = false %}
27 | {% if data[0] not in ['Net Cash', 'Net Worth'] and data[1] != 0 %}
28 | {% set show_arrow = true %}
29 | {% endif %}
30 |
31 |
32 |
33 |
50 |
51 | {% if show_arrow %}
52 |
53 | {% for bank_data in account_type_banak_data %}
54 | {% if bank_data[0] == data[0] %}
55 | {% if bank_data[2] != 0.0 or bank_data[3] != 0.0 %}
56 |
57 |
{{ bank_data[1] }}
58 |
59 | {{ bank_data[2] | tocurrency_whole }}
60 | {{ bank_data[3] | tocurrency_whole }} available
61 |
62 |
63 | {% endif %}
64 | {% endif %}
65 | {% endfor %}
66 |
67 | {% endif %}
68 |
69 | {% endfor %}
70 |
71 |
72 |
73 |
--------------------------------------------------------------------------------
/SparkyBudget/templates/budget.html.jinja:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Budget {# Updated title #}
8 |
9 | {# Updated CSS link #}
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 | {% include 'components/navigation_bar.html.jinja' %}
21 |
22 |
Recurring Budgets
23 |
38 |
39 | {% if error %}
40 |
43 | {% endif %}
44 |
45 |
46 |
47 | |
48 | SubCategory |
49 | Budget Amount |
50 |
51 |
52 |
53 | {% if data %}
54 | {% for row in data %}
55 |
56 |
57 |
58 |
61 | |
62 |
63 |
64 | {{ row.SubCategory }}
65 | |
66 |
67 |
68 | ${{ "%.2f"|format(row.BudgetAmount) }}
69 | |
70 |
71 | {% endfor %}
72 | {% else %}
73 |
74 | No recurring budget details found. |
75 |
76 | {% endif %}
77 |
78 |
79 |
80 |
81 |
82 |
--------------------------------------------------------------------------------
/SparkyBudget/templates/budget_summary_chart.html.jinja:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | {% for data in budget_summary_chart %}
7 |
8 |
9 |
10 |
11 |
12 |
13 | {{ data.SubCategory }}
14 |
15 |
16 |
17 |
18 |
20 | {{ data.BudgetAmount | tocurrency }}
21 |
22 |
23 |
24 |
25 |
29 |
30 |
31 | {{ data.TotalTransactionAmount | tocurrency }}
32 |
33 |
34 | {{ data.RemainingBudget | tocurrency }}
35 |
36 |
37 |
38 | {% endfor %}
39 |
40 |
--------------------------------------------------------------------------------
/SparkyBudget/templates/budget_transaction_details.html.jinja:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | Date |
10 | Account |
11 | Description |
12 | Payee |
13 | Amount |
14 | |
15 | Split |
16 |
17 |
18 |
19 |
20 | {% for detail in transaction_details %}
21 |
22 | {{ detail[1] }} |
23 | {{ detail[2] }} |
24 | {{ detail[3] }} |
25 | {{ detail[4] }} |
26 | {{ detail[5] | tocurrency }} |
27 |
28 |
29 |
30 |
33 |
34 | |
35 |
36 |
37 | |
38 |
39 | {% endfor %}
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 | {% for detail in transaction_details %}
48 |
49 |
59 |
60 |
61 | Account:
62 | {{ detail[2] }}
63 |
64 |
65 | Desc:
66 | {{ detail[3] }}
67 |
68 |
69 |
70 |
71 |
74 |
75 |
76 |
{# Add Split button field #}
77 |
78 |
79 |
80 |
81 | {% endfor %}
82 |
83 |
84 |
85 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
--------------------------------------------------------------------------------
/SparkyBudget/templates/category.html.jinja:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Category Management
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 | {% include 'components/navigation_bar.html.jinja' %}
22 |
23 |
24 |
25 |
26 |
27 |
Account Types
28 |
29 |
30 |
31 |
32 | Delete |
33 | AccountType |
34 | HideFromBudget |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
Category
49 |
50 |
55 |
56 |
57 |
58 |
59 |
60 | Delete |
61 | SubCategoryKey |
62 | Category |
63 | SubCategory |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
Transaction Rules
75 |
76 |
81 |
82 |
83 |
84 |
85 |
86 | Delete |
87 | RuleKey |
88 | Default_SubCategory |
89 | Rule_Category |
90 | Rule_Pattern |
91 | Match_Word |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
--------------------------------------------------------------------------------
/SparkyBudget/templates/components/navigation_bar.html.jinja:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
24 |
25 |
--------------------------------------------------------------------------------
/SparkyBudget/templates/index.html.jinja:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | Sparky Budget
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 | {% include 'components/navigation_bar.html.jinja' %}
42 |
43 |
44 |
45 |
46 |
47 |
48 | {% include 'balance_summary.html.jinja' %}
49 |
50 |
51 |
52 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 | {% for account_type in account_type_data %}
75 |
79 | {% endfor %}
80 |
81 |
82 |
83 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 | Daily Balance
95 |
99 |
100 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 | {% include 'balance_details.html.jinja' %}
124 |
125 |
126 |
127 |
159 |
160 |
161 |
162 |
163 |
166 |
167 | Add Budget
168 |
169 |
170 |
171 |
187 |
188 |
189 |
190 |
191 |
Income - $6,273.00
192 |
193 |
194 |
195 |
196 |
Budget - $5,476.00
197 |
198 |
199 |
200 |
201 |
202 |
203 |
204 |
211 |
215 |
216 |
217 |
218 | {% include 'budget_summary_chart.html.jinja' %}
219 |
220 |
221 |
222 |
225 |
226 |
227 |
228 |
229 |
230 |
231 |
232 |
233 |
234 |
235 |
236 |
237 |
238 |
239 |
240 |
--------------------------------------------------------------------------------
/SparkyBudget/templates/login.html.jinja:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Login
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
26 |
27 |
28 |
29 |
54 |
55 |
56 |
--------------------------------------------------------------------------------
/demo/SparkyBudget-Desktop.mp4:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodeWithCJ/SparkyBudget/c4c550580c72e7bcfdb171542351fa2d3a7a73eb/demo/SparkyBudget-Desktop.mp4
--------------------------------------------------------------------------------
/demo/SparkyBudget-Mobile.mp4:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodeWithCJ/SparkyBudget/c4c550580c72e7bcfdb171542351fa2d3a7a73eb/demo/SparkyBudget-Mobile.mp4
--------------------------------------------------------------------------------
/docker-compose.yaml:
--------------------------------------------------------------------------------
1 | services:
2 | app:
3 | container_name: sparkybudget
4 | env_file:
5 | - .env
6 | image: codewithcj/sparkybudget:latest # Use latest or specific version
7 | volumes:
8 | - ./:/private
9 | ports:
10 | - 5050:5000 # Update 5050 to your desired port
11 | restart: unless-stopped
12 |
--------------------------------------------------------------------------------
/dockerfile:
--------------------------------------------------------------------------------
1 | FROM python:3.13.2-slim-bookworm
2 | WORKDIR /SparkyBudget
3 | COPY requirements.txt ./requirements.txt
4 | RUN pip install --no-cache-dir -r ./requirements.txt
5 | RUN apt-get update && \
6 | apt-get install -y --no-install-recommends locales tzdata && \
7 | apt-get -y autoremove && apt-get clean -y && rm -rf /var/lib/apt/lists/* && \
8 | sed -i -e 's/# en_US.UTF-8 UTF-8/en_US.UTF-8 UTF-8/' /etc/locale.gen && locale-gen
9 | ARG TZ=America/New_York
10 | ENV TZ=${TZ}
11 | RUN echo "${TZ}" > /etc/timezone && dpkg-reconfigure -f noninteractive tzdata
12 | ENV FLASK_ENV=production
13 | ENV LC_ALL=en_US.UTF-8
14 | ENV LANG=en_US.UTF-8
15 | ENV LANGUAGE=en_US:en
16 | COPY SparkyBudget/ ./
17 | COPY entrypoint.sh ./entrypoint.sh
18 | RUN chmod +x ./entrypoint.sh
19 | EXPOSE 5000
20 | ENTRYPOINT ["./entrypoint.sh"]
--------------------------------------------------------------------------------
/entrypoint.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | python py_utils/scheduler.py &
3 | exec gunicorn -b :5000 --timeout 120 --workers 4 --threads 4 SparkyBudget:app
--------------------------------------------------------------------------------
/mypy.ini:
--------------------------------------------------------------------------------
1 | # Global options:
2 |
3 | [mypy]
4 | warn_return_any = True
5 | warn_unused_configs = True
6 | disallow_untyped_defs = False
7 | disallow_any_unimported = False
8 | check_untyped_defs = True
9 | show_error_codes = True
10 | warn_unused_ignores = True
11 | exclude = containers
12 |
13 | # Per-module options:
14 | [mypy-flask_login.*]
15 | ignore_missing_imports = True
16 |
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
1 | [project]
2 | name = "SparkyBudget"
3 | version = "0.1.0"
4 | description = "Default template for PDM package"
5 | authors = [
6 | {name = "CodeWithCJ", email = "CodeWithCJ@users.noreply.github.com"},
7 | ]
8 | dependencies = [
9 | "Flask>=3.0.2",
10 | "flask-login>=0.6.3",
11 | "requests>=2.31.0",
12 | "schedule>=1.2.1",
13 | "gunicorn>=21.2.0",
14 | ]
15 | requires-python = "==3.10.*"
16 | readme = "README.md"
17 | license = {text = "AGPL-3.0-or-later"}
18 |
19 |
20 | [tool.pdm]
21 | distribution = false
22 |
23 | [tool.pdm.dev-dependencies]
24 | lint = [
25 | "black>=24.2.0",
26 | "pylint>=3.1.0",
27 | "isort>=5.13.2",
28 | "mypy>=1.8.0",
29 | ]
30 |
31 | [tool.black]
32 | line-length = 119
33 |
34 | [tool.isort]
35 | multi_line_output = 3
36 | include_trailing_comma = true
37 | force_grid_wrap = 0
38 | line_length = 119
39 | profile = "black"
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | Flask==3.1.0
2 | flask_login==0.6.3
3 | Requests==2.32.3
4 | schedule==1.2.2
5 | python-dotenv==1.1.0
6 | gunicorn
7 | pandas==2.2.3
--------------------------------------------------------------------------------
/sparkybudget_unraid.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | SparkyBudget
4 | codewithcj/sparkybudget
5 | https://hub.docker.com/r/codewithcj/sparkybudget/
6 | bridge
7 | false
8 | https://github.com/codewithcj/SparkyBudget/issues
9 |
10 | latest
11 | Latest stable release
12 | {{ now() | datetimeformat("%Y-%m-%d") }}
13 |
14 |
15 | SparkyBudget is a personal finance management application.
16 |
17 | http://[IP]:[PORT:5000]/
18 |
19 | https://raw.githubusercontent.com/codewithcj/SparkyBudget/main/SparkyBudget.png
20 |
21 |
22 |
23 |
24 |
25 |
26 | 5000
27 |
28 | /mnt/user/appdata/sparkybudget/database
29 |
30 |
31 | /mnt/user/appdata/sparkybudget/config
32 |
33 |
34 |
--------------------------------------------------------------------------------