Loading...
;
+
+ return (
+
+
+
Goals & Milestones
+
+
+
+
+ {goals.map((g) => (
+
+
+ {g.name}
+
+ {g.current_amount} / {g.target_amount} {g.currency}
+
+
+
+
+
+
+ ))}
+
+ {goals.length === 0 &&
No goals set yet.
}
+
+ );
+}
diff --git a/app/tsconfig.app.tsbuildinfo b/app/tsconfig.app.tsbuildinfo
index abdef379..7c8dea51 100644
--- a/app/tsconfig.app.tsbuildinfo
+++ b/app/tsconfig.app.tsbuildinfo
@@ -1 +1 @@
-{"root":["./src/app.tsx","./src/main.tsx","./src/setuptests.ts","./src/vite-env.d.ts","./src/__tests__/analytics.integration.test.tsx","./src/__tests__/dashboard.integration.test.tsx","./src/__tests__/expenses.integration.test.tsx","./src/__tests__/navbar.test.tsx","./src/__tests__/protectedroute.test.tsx","./src/__tests__/reminders.integration.test.tsx","./src/__tests__/signin.test.tsx","./src/__tests__/apiclient.test.ts","./src/__tests__/auth.test.ts","./src/api/auth.ts","./src/api/bills.ts","./src/api/categories.ts","./src/api/client.ts","./src/api/dashboard.ts","./src/api/expenses.ts","./src/api/insights.ts","./src/api/reminders.ts","./src/components/auth/protectedroute.tsx","./src/components/layout/footer.tsx","./src/components/layout/layout.tsx","./src/components/layout/navbar.tsx","./src/components/ui/accordion.tsx","./src/components/ui/alert-dailog.tsx","./src/components/ui/alert.tsx","./src/components/ui/aspect-ratio.tsx","./src/components/ui/avatar.tsx","./src/components/ui/badge.tsx","./src/components/ui/breadcrumb.tsx","./src/components/ui/button.tsx","./src/components/ui/calendar.tsx","./src/components/ui/card.tsx","./src/components/ui/carousel.tsx","./src/components/ui/chart.tsx","./src/components/ui/checkbox.tsx","./src/components/ui/collapsible.tsx","./src/components/ui/command.tsx","./src/components/ui/context-menu.tsx","./src/components/ui/dialog.tsx","./src/components/ui/drawer.tsx","./src/components/ui/dropdown-menu.tsx","./src/components/ui/financial-card.tsx","./src/components/ui/form.tsx","./src/components/ui/hover-card.tsx","./src/components/ui/input-otp.tsx","./src/components/ui/input.tsx","./src/components/ui/label.tsx","./src/components/ui/menubar.tsx","./src/components/ui/navigation-menu.tsx","./src/components/ui/pagination.tsx","./src/components/ui/popover.tsx","./src/components/ui/progress.tsx","./src/components/ui/radio-group.tsx","./src/components/ui/resizable.tsx","./src/components/ui/scroll-area.tsx","./src/components/ui/select.tsx","./src/components/ui/separator.tsx","./src/components/ui/sheet.tsx","./src/components/ui/sidebar.tsx","./src/components/ui/skeleton.tsx","./src/components/ui/slider.tsx","./src/components/ui/sonner.tsx","./src/components/ui/switch.tsx","./src/components/ui/table.tsx","./src/components/ui/tabs.tsx","./src/components/ui/textarea.tsx","./src/components/ui/toast.tsx","./src/components/ui/toaster.tsx","./src/components/ui/toggle-group.tsx","./src/components/ui/toggle.tsx","./src/components/ui/tooltip.tsx","./src/components/ui/use-toast.tsx","./src/hooks/use-mobile.tsx","./src/hooks/use-toast.ts","./src/lib/auth.ts","./src/lib/currency.ts","./src/lib/utils.ts","./src/pages/account.tsx","./src/pages/analytics.tsx","./src/pages/bills.tsx","./src/pages/budgets.tsx","./src/pages/categories.tsx","./src/pages/dashboard.tsx","./src/pages/expenses.tsx","./src/pages/index.tsx","./src/pages/landing.tsx","./src/pages/notfound.tsx","./src/pages/register.tsx","./src/pages/reminders.tsx","./src/pages/signin.tsx"],"version":"5.8.3"}
\ No newline at end of file
+{"root":["./src/app.tsx","./src/main.tsx","./src/setuptests.ts","./src/vite-env.d.ts","./src/__tests__/analytics.integration.test.tsx","./src/__tests__/dashboard.integration.test.tsx","./src/__tests__/expenses.integration.test.tsx","./src/__tests__/navbar.test.tsx","./src/__tests__/protectedroute.test.tsx","./src/__tests__/reminders.integration.test.tsx","./src/__tests__/signin.test.tsx","./src/__tests__/apiclient.test.ts","./src/__tests__/auth.test.ts","./src/api/auth.ts","./src/api/bills.ts","./src/api/categories.ts","./src/api/client.ts","./src/api/dashboard.ts","./src/api/expenses.ts","./src/api/goals.ts","./src/api/insights.ts","./src/api/reminders.ts","./src/components/auth/protectedroute.tsx","./src/components/layout/footer.tsx","./src/components/layout/layout.tsx","./src/components/layout/navbar.tsx","./src/components/ui/accordion.tsx","./src/components/ui/alert-dailog.tsx","./src/components/ui/alert.tsx","./src/components/ui/aspect-ratio.tsx","./src/components/ui/avatar.tsx","./src/components/ui/badge.tsx","./src/components/ui/breadcrumb.tsx","./src/components/ui/button.tsx","./src/components/ui/calendar.tsx","./src/components/ui/card.tsx","./src/components/ui/carousel.tsx","./src/components/ui/chart.tsx","./src/components/ui/checkbox.tsx","./src/components/ui/collapsible.tsx","./src/components/ui/command.tsx","./src/components/ui/context-menu.tsx","./src/components/ui/dialog.tsx","./src/components/ui/drawer.tsx","./src/components/ui/dropdown-menu.tsx","./src/components/ui/financial-card.tsx","./src/components/ui/form.tsx","./src/components/ui/hover-card.tsx","./src/components/ui/input-otp.tsx","./src/components/ui/input.tsx","./src/components/ui/label.tsx","./src/components/ui/menubar.tsx","./src/components/ui/navigation-menu.tsx","./src/components/ui/pagination.tsx","./src/components/ui/popover.tsx","./src/components/ui/progress.tsx","./src/components/ui/radio-group.tsx","./src/components/ui/resizable.tsx","./src/components/ui/scroll-area.tsx","./src/components/ui/select.tsx","./src/components/ui/separator.tsx","./src/components/ui/sheet.tsx","./src/components/ui/sidebar.tsx","./src/components/ui/skeleton.tsx","./src/components/ui/slider.tsx","./src/components/ui/sonner.tsx","./src/components/ui/switch.tsx","./src/components/ui/table.tsx","./src/components/ui/tabs.tsx","./src/components/ui/textarea.tsx","./src/components/ui/toast.tsx","./src/components/ui/toaster.tsx","./src/components/ui/toggle-group.tsx","./src/components/ui/toggle.tsx","./src/components/ui/tooltip.tsx","./src/components/ui/use-toast.tsx","./src/hooks/use-mobile.tsx","./src/hooks/use-toast.ts","./src/lib/auth.ts","./src/lib/currency.ts","./src/lib/utils.ts","./src/pages/account.tsx","./src/pages/analytics.tsx","./src/pages/bills.tsx","./src/pages/budgets.tsx","./src/pages/categories.tsx","./src/pages/dashboard.tsx","./src/pages/expenses.tsx","./src/pages/goals.tsx","./src/pages/index.tsx","./src/pages/landing.tsx","./src/pages/notfound.tsx","./src/pages/register.tsx","./src/pages/reminders.tsx","./src/pages/signin.tsx"],"version":"5.8.3"}
\ No newline at end of file
diff --git a/packages/backend/app/__init__.py b/packages/backend/app/__init__.py
index cdf76b45..9ec394df 100644
--- a/packages/backend/app/__init__.py
+++ b/packages/backend/app/__init__.py
@@ -103,13 +103,11 @@ def _ensure_schema_compatibility(app: Flask) -> None:
conn = db.engine.raw_connection()
try:
cur = conn.cursor()
- cur.execute(
- """
+ cur.execute("""
ALTER TABLE users
ADD COLUMN IF NOT EXISTS preferred_currency VARCHAR(10)
NOT NULL DEFAULT 'INR'
- """
- )
+ """)
conn.commit()
except Exception:
app.logger.exception(
diff --git a/packages/backend/app/extensions.py b/packages/backend/app/extensions.py
index bad98fae..550a15e6 100644
--- a/packages/backend/app/extensions.py
+++ b/packages/backend/app/extensions.py
@@ -3,7 +3,6 @@
import redis
from .config import Settings
-
db = SQLAlchemy()
jwt = JWTManager()
diff --git a/packages/backend/app/models.py b/packages/backend/app/models.py
index 64d44810..383b92e8 100644
--- a/packages/backend/app/models.py
+++ b/packages/backend/app/models.py
@@ -133,3 +133,29 @@ class AuditLog(db.Model):
user_id = db.Column(db.Integer, db.ForeignKey("users.id"), nullable=True)
action = db.Column(db.String(100), nullable=False)
created_at = db.Column(db.DateTime, default=datetime.utcnow, nullable=False)
+
+
+class Goal(db.Model):
+ __tablename__ = "goals"
+ id = db.Column(db.Integer, primary_key=True)
+ user_id = db.Column(db.Integer, db.ForeignKey("users.id"), nullable=False)
+ name = db.Column(db.String(255), nullable=False)
+ target_amount = db.Column(db.Numeric(12, 2), nullable=False)
+ current_amount = db.Column(db.Numeric(12, 2), default=0.00, nullable=False)
+ currency = db.Column(db.String(10), default="INR", nullable=False)
+ deadline = db.Column(db.Date, nullable=True)
+ created_at = db.Column(db.DateTime, default=datetime.utcnow, nullable=False)
+
+ milestones = db.relationship(
+ "GoalMilestone", backref="goal", lazy=True, cascade="all, delete-orphan"
+ )
+
+
+class GoalMilestone(db.Model):
+ __tablename__ = "goal_milestones"
+ id = db.Column(db.Integer, primary_key=True)
+ goal_id = db.Column(db.Integer, db.ForeignKey("goals.id"), nullable=False)
+ name = db.Column(db.String(255), nullable=False)
+ target_amount = db.Column(db.Numeric(12, 2), nullable=False)
+ achieved = db.Column(db.Boolean, default=False, nullable=False)
+ created_at = db.Column(db.DateTime, default=datetime.utcnow, nullable=False)
diff --git a/packages/backend/app/routes/__init__.py b/packages/backend/app/routes/__init__.py
index f13b0f89..22631662 100644
--- a/packages/backend/app/routes/__init__.py
+++ b/packages/backend/app/routes/__init__.py
@@ -7,6 +7,7 @@
from .categories import bp as categories_bp
from .docs import bp as docs_bp
from .dashboard import bp as dashboard_bp
+from .goals import bp as goals_bp
def register_routes(app: Flask):
@@ -18,3 +19,4 @@ def register_routes(app: Flask):
app.register_blueprint(categories_bp, url_prefix="/categories")
app.register_blueprint(docs_bp, url_prefix="/docs")
app.register_blueprint(dashboard_bp, url_prefix="/dashboard")
+ app.register_blueprint(goals_bp, url_prefix="/goals")
diff --git a/packages/backend/app/routes/goals.py b/packages/backend/app/routes/goals.py
new file mode 100644
index 00000000..ba44935d
--- /dev/null
+++ b/packages/backend/app/routes/goals.py
@@ -0,0 +1,118 @@
+from flask import Blueprint, jsonify, request
+from flask_jwt_extended import jwt_required, get_jwt_identity
+from ..extensions import db
+from ..models import Goal, GoalMilestone
+from datetime import datetime
+
+bp = Blueprint("goals", __name__)
+
+
+@bp.route("", methods=["GET"])
+@jwt_required()
+def get_goals():
+ uid = int(get_jwt_identity())
+ goals = Goal.query.filter_by(user_id=uid).all()
+ result = []
+ for g in goals:
+ milestones = GoalMilestone.query.filter_by(goal_id=g.id).all()
+ result.append(
+ {
+ "id": g.id,
+ "name": g.name,
+ "target_amount": float(g.target_amount),
+ "current_amount": float(g.current_amount),
+ "currency": g.currency,
+ "deadline": g.deadline.isoformat() if g.deadline else None,
+ "created_at": g.created_at.isoformat(),
+ "milestones": [
+ {
+ "id": m.id,
+ "name": m.name,
+ "target_amount": float(m.target_amount),
+ "achieved": m.achieved,
+ }
+ for m in milestones
+ ],
+ }
+ )
+ return jsonify(result), 200
+
+
+@bp.route("", methods=["POST"])
+@jwt_required()
+def create_goal():
+ uid = int(get_jwt_identity())
+ data = request.json
+ try:
+ deadline = (
+ datetime.strptime(data["deadline"], "%Y-%m-%d").date()
+ if data.get("deadline")
+ else None
+ )
+ except ValueError:
+ return jsonify({"error": "Invalid date format, use YYYY-MM-DD"}), 400
+
+ new_goal = Goal(
+ user_id=uid,
+ name=data["name"],
+ target_amount=data["target_amount"],
+ current_amount=data.get("current_amount", 0.0),
+ currency=data.get("currency", "INR"),
+ deadline=deadline,
+ )
+ db.session.add(new_goal)
+ db.session.commit()
+
+ if "milestones" in data:
+ for m in data["milestones"]:
+ new_ms = GoalMilestone(
+ goal_id=new_goal.id,
+ name=m["name"],
+ target_amount=m["target_amount"],
+ achieved=m.get("achieved", False),
+ )
+ db.session.add(new_ms)
+ db.session.commit()
+
+ return jsonify({"message": "Goal created successfully", "id": new_goal.id}), 201
+
+
+@bp.route("/