diff --git a/api/activity.py b/api/activity.py index 036fbc1..8e2afe6 100644 --- a/api/activity.py +++ b/api/activity.py @@ -27,7 +27,10 @@ def create_activity(): elevation_gain=data['elevation_gain'], elevation_loss=data['elevation_loss'], max_elevation=data['max_elevation'], - min_elevation=data['min_elevation'] + min_elevation=data['min_elevation'], + steps=data.get('steps'), + floors=data.get('floors'), + zone_minutes=data.get('zone_minutes') ) # Optional time series diff --git a/api/body_insight.py b/api/body_insight.py index ca6cdee..8f271ec 100644 --- a/api/body_insight.py +++ b/api/body_insight.py @@ -27,6 +27,11 @@ def add_body_insight(): if not latest_activity: return jsonify({"error": "No activities found for user"}), 404 activity_id = latest_activity.id + + # Check for existing body_insight for activity + existing = BodyInsight.query.filter_by(activity_id=activity_id).first() + if existing: + return jsonify({"error": "BodyInsight already exists for this activity"}), 409 try: insight = BodyInsight( diff --git a/api/sessions.py b/api/sessions.py index a2ccc49..b93e382 100644 --- a/api/sessions.py +++ b/api/sessions.py @@ -1,10 +1,13 @@ from flask import Blueprint, jsonify from models.activity import Activity +from models.body_insight import BodyInsight +from models.sleep_data import SleepData from models import db from datetime import datetime sessions_bp = Blueprint('sessions_api', __name__) +# Get all sessions summary for table. @sessions_bp.route('', methods=['GET']) def get_sessions(): # replace with authenticated user ID from session/token @@ -23,3 +26,30 @@ def get_sessions(): }) return jsonify(sessions), 200 + + + +# Get activity, body insight and sleep data details for a single session +@sessions_bp.route('//details', methods=['GET']) +def get_session_details(session_id): + # Fetch the activity + activity = Activity.query.get(session_id) + if not activity: + return jsonify({"error": "Session not found"}), 404 + + # Fetch the linked BodyInsight, if it exists + body_insight = BodyInsight.query.filter_by(activity_id=activity.id).first() + + # Fetch sleep data for the same date as the activity + sleep_entry = SleepData.query.filter_by( + user_id=activity.user_id, + date=activity.begin_time.date() + ).first() + + response = { + "activity": activity.as_dict(), + "body_insight": body_insight.as_dict() if body_insight else None, + "sleep_data": sleep_entry.as_dict() if sleep_entry else None + } + + return jsonify(response), 200 \ No newline at end of file diff --git a/api/sleep_data.py b/api/sleep_data.py new file mode 100644 index 0000000..3203974 --- /dev/null +++ b/api/sleep_data.py @@ -0,0 +1,56 @@ +# /api/sleep_data.py +from flask import Blueprint, request, jsonify +from models.sleep_data import SleepData +from models import db +from datetime import datetime + +sleep_data_bp = Blueprint('sleep_data', __name__, url_prefix='/api/sleep_data') + +# For now, mimic authenticated user +def get_current_user_id(): + return 1 # Replace with token/session in future + + +# POST new sleep record +@sleep_data_bp.route('', methods=['POST']) +def add_sleep_data(): + data = request.get_json() + user_id = get_current_user_id() + + if not data.get('date') or not data.get('duration_minutes'): + return jsonify({"error": "Missing required fields: date, duration_minutes"}), 400 + + try: + sleep_date = datetime.strptime(data['date'], "%Y-%m-%d").date() + except ValueError: + return jsonify({"error": "Invalid date format. Use YYYY-MM-DD"}), 400 + + sleep_entry = SleepData( + user_id=user_id, + date=sleep_date, + duration_minutes=data['duration_minutes'], + sleep_score=data.get('sleep_score') + ) + + db.session.add(sleep_entry) + db.session.commit() + + return jsonify(sleep_entry.as_dict()), 201 + + +# GET all sleep records for the current user +@sleep_data_bp.route('', methods=['GET']) +def get_all_sleep(): + user_id = get_current_user_id() + entries = SleepData.query.filter_by(user_id=user_id).order_by(SleepData.date.desc()).all() + return jsonify([e.as_dict() for e in entries]), 200 + + +# GET sleep record by ID (scoped to current user) +@sleep_data_bp.route('/', methods=['GET']) +def get_sleep_by_id(id): + user_id = get_current_user_id() + entry = SleepData.query.filter_by(id=id, user_id=user_id).first() + if not entry: + return jsonify({"error": "Sleep record not found for this user"}), 404 + return jsonify(entry.as_dict()), 200 diff --git a/app.py b/app.py index fa5415b..cceed38 100644 --- a/app.py +++ b/app.py @@ -4,6 +4,11 @@ from api.goals import goals_bp from api.profile import api as profile_api from api.dashboard import dashboard_bp +from api.body_insight import body_insight_bp +from api.activity import activity_bp +from api.sessions import sessions_bp +from api.sleep_data import sleep_data_bp +from api.sync import sync_bp from models import db from dotenv import load_dotenv from services.firebase_admin import init_firebase_admin @@ -73,6 +78,11 @@ app.register_blueprint(goals_bp, url_prefix='/api/goals') app.register_blueprint(dashboard_bp, url_prefix='/api/dashboard') app.register_blueprint(profile_api, url_prefix='/api/profile') +app.register_blueprint(sync_bp, url_prefix='/api/synced') +app.register_blueprint(body_insight_bp, url_prefix='/api/body_insight') +app.register_blueprint(activity_bp, url_prefix='/api/activity') +app.register_blueprint(sessions_bp, url_prefix='/api/sessions') +app.register_blueprint(sleep_data_bp, url_prefix='/api/sleep_data') # Main index route (login + welcome) @app.route('/', methods=['GET', 'POST']) diff --git a/models/__init__.py b/models/__init__.py index b25ac4f..9cc31e6 100644 --- a/models/__init__.py +++ b/models/__init__.py @@ -7,3 +7,4 @@ from .activity_summary import ActivitySummary from .metadata import ActivityMetadata from .body_insight import BodyInsight +from .sleep_data import SleepData \ No newline at end of file diff --git a/models/activity.py b/models/activity.py index 0f64fbd..4347f5f 100644 --- a/models/activity.py +++ b/models/activity.py @@ -25,6 +25,10 @@ class Activity(db.Model): elevation_loss = db.Column(db.Float, nullable=False) max_elevation = db.Column(db.Float, nullable=False) min_elevation = db.Column(db.Float, nullable=False) + steps = db.Column(db.Integer,nullable=True) + floors = db.Column(db.Integer,nullable=True) + zone_minutes = db.Column(db.Integer, nullable=True) + @@ -51,6 +55,9 @@ def as_dict(self): "elevation_loss": self.elevation_loss, "max_elevation": self.max_elevation, "min_elevation": self.min_elevation, + "steps" : self.steps, + "floors" : self.floors, + "zone_minutes": self.zone_minutes, "time_series": [ts.as_dict() for ts in self.time_series] } diff --git a/models/body_insight.py b/models/body_insight.py index 643f6ab..bfae0c0 100644 --- a/models/body_insight.py +++ b/models/body_insight.py @@ -4,8 +4,13 @@ class BodyInsight(db.Model): __tablename__ = 'body_insight' id = db.Column(db.Integer, primary_key=True) - activity_id = db.Column(db.Integer, db.ForeignKey('activity.id'), nullable=False) + activity_id = db.Column(db.Integer, db.ForeignKey('activity.id'), nullable=False, unique=True) + activity = db.relationship( + 'Activity', + backref=db.backref('body_insight', uselist=False) # Makes SQLAlchemy treat it as one-to-one + ) + # Performance Metrics vo2_max = db.Column(db.Float, nullable=True) lactate_threshold = db.Column(db.Float, nullable=True) diff --git a/models/sleep_data.py b/models/sleep_data.py new file mode 100644 index 0000000..bf1ad53 --- /dev/null +++ b/models/sleep_data.py @@ -0,0 +1,22 @@ +from models import db +from datetime import date + +class SleepData(db.Model): + __tablename__ = 'sleep_data' + + id = db.Column(db.Integer, primary_key=True) + user_id = db.Column(db.Integer, db.ForeignKey('user_profile.id'), nullable=False) + date = db.Column(db.Date, nullable=False) # The night the sleep occurred + duration_minutes = db.Column(db.Integer, nullable=False) # Total sleep in minutes + sleep_score = db.Column(db.Float, nullable=True) # Optional sleep quality score (0-100) + + def as_dict(self): + return { + "id": self.id, + "user_id": self.user_id, + "date": self.date.isoformat(), + "duration_minutes": self.duration_minutes, + "hours": self.duration_minutes // 60, + "minutes": self.duration_minutes % 60, + "sleep_score": self.sleep_score + }