diff --git a/app.py b/app.py index 29e7921..e782a53 100644 --- a/app.py +++ b/app.py @@ -119,6 +119,40 @@ def get_person(person_id): return render_template('person.html', person=person, selected_exercise_ids=selected_exercise_ids, max_date=max_date, min_date=min_date, tags=tags), 200, {"HX-Trigger": "updatedPeople"} +@ app.route("/person//workout/overview", methods=['GET']) +def person_overview(person_id): + min_date = request.args.get('min_date', type=convert_str_to_date) + max_date = request.args.get('max_date', type=convert_str_to_date) + selected_exercise_ids = request.args.getlist('exercise_id', type=int) + + if not min_date or not max_date: + db_min_date, db_max_date = db.person_overview.get_earliest_and_latest_workout_dates(person_id) + min_date = min_date or db_min_date + max_date = max_date or db_max_date + + if not selected_exercise_ids and htmx.trigger_name != 'exercise_id': + selected_exercise_ids = db.person_overview.list_of_performed_exercise_ids(person_id, min_date, max_date) + + person = db.person_overview.get(person_id, min_date, max_date, selected_exercise_ids) + exercises = db.person_overview.get_exercises_with_selection(person_id, min_date, max_date, selected_exercise_ids) + tags = db.get_tags_for_person(person_id) + + # Render the appropriate response for HTMX or full page + render_args = { + **person, + "exercises": exercises, + "tags": tags, + "selected_exercise_ids": selected_exercise_ids, + "max_date": max_date, + "min_date": min_date + } + + if htmx: + return render_block(app.jinja_env, 'person_overview.html', 'content', **render_args), 200, {"HX-Trigger": "updatedPeople"} + + return render_template('person_overview.html', **render_args), 200, {"HX-Trigger": "updatedPeople"} + + @ app.route("/person//calendar") @ validate_person @@ -146,17 +180,6 @@ def get_person_notes(person_id): return render_block(app.jinja_env, 'notes.html', 'content', person_id=person_id, person_name=person_name, workout_notes=workout_notes) return render_template('notes.html', person_id=person_id, person_name=person_name, workout_notes=workout_notes) - -@ app.route("/person//workout//modal", methods=['GET']) -@ validate_workout -def get_workout_modal(person_id, workout_id): - workout = db.get_workout(person_id, workout_id) - (person_tags, workout_tags, selected_workout_tag_ids) = db.get_workout_tags( - person_id, workout_id) - exercises = db.get_all_exercises() - return render_template('partials/workout_modal.html', **workout, person_tags=person_tags, workout_tags=workout_tags, selected_workout_tag_ids=selected_workout_tag_ids, exercises=exercises) - - @ app.route("/person//workout", methods=['POST']) def create_workout(person_id): new_workout_id = db.create_workout(person_id) @@ -324,7 +347,7 @@ def goto_tag(): person_id = request.args.get("person_id") tag_filter = request.args.get('filter') if person_id: - return redirect(url_for('get_person', person_id=int(person_id)) + tag_filter) + return redirect(url_for('person_overview', person_id=int(person_id)) + tag_filter) return redirect(url_for('dashboard') + tag_filter) diff --git a/db.py b/db.py index 609611d..1b150dd 100644 --- a/db.py +++ b/db.py @@ -9,6 +9,7 @@ from flask import g import pandas as pd from features.calendar import Calendar from features.exercises import Exercises +from features.person_overview import PersonOverview from features.stats import Stats from features.workout import Workout from features.sql_explorer import SQLExplorer @@ -22,6 +23,7 @@ class DataBase(): self.workout = Workout(self.execute) self.exercises = Exercises(self.execute) self.sql_explorer = SQLExplorer(self.execute) + self.person_overview = PersonOverview(self.execute) db_url = urlparse(os.environ['DATABASE_URL']) # if db_url is null then throw error if not db_url: @@ -577,7 +579,6 @@ class DataBase(): # Convert to a list for the final output workout_notes_list = list(workout_notes.values()) return person_name, workout_notes_list - def get_exercise_earliest_and_latest_dates(self, person_id, exercise_id): sql_query = """ diff --git a/features/person_overview.py b/features/person_overview.py new file mode 100644 index 0000000..9851cee --- /dev/null +++ b/features/person_overview.py @@ -0,0 +1,164 @@ + +class PersonOverview: + def __init__(self, db_connection_method): + self.execute = db_connection_method + + def get_earliest_and_latest_workout_dates(self, person_id): + sql_query = """ + SELECT + MIN(w.start_date) AS earliest_date, + MAX(w.start_date) AS latest_date + FROM workout w + INNER JOIN topset t ON w.workout_id = t.workout_id + WHERE w.person_id = %s; + """ + result = self.execute(sql_query, [person_id]) + + if not result or not result[0]: + return None, None + + return result[0]['earliest_date'], result[0]['latest_date'] + + def list_of_performed_exercise_ids(self, person_id, min_date, max_date): + sql_query = """ + SELECT + ARRAY_AGG(DISTINCT e.exercise_id) AS exercise_ids + FROM workout w + LEFT JOIN topset t ON w.workout_id = t.workout_id + LEFT JOIN exercise e ON t.exercise_id = e.exercise_id + WHERE w.start_date BETWEEN %s AND %s + AND w.person_id = %s + """ + result = self.execute(sql_query, [min_date, max_date, person_id]) + + if not result or not result[0]: + return [] + + return result[0]['exercise_ids'] + + def get_exercises_with_selection(self, person_id, start_date, end_date, selected_exercise_ids): + # SQL query to fetch all exercises performed by the person in the given time range + sql_query = """ + SELECT DISTINCT + e.exercise_id, + e.name AS exercise_name + FROM + workout w + JOIN + topset t ON w.workout_id = t.workout_id + JOIN + exercise e ON t.exercise_id = e.exercise_id + WHERE + w.person_id = %s + AND w.start_date BETWEEN %s AND %s + ORDER BY + e.name ASC; + """ + + # Execute the query with parameters + result = self.execute(sql_query, [person_id, start_date, end_date]) + + if not result: + return [] # No exercises found in the given time range + + # Add the "selected" property to each exercise + exercises = [] + for row in result: + exercises.append({ + "id": row["exercise_id"], + "name": row["exercise_name"], + "selected": row["exercise_id"] in selected_exercise_ids + }) + + return exercises + + + def get(self, person_id, start_date, end_date, selected_exercise_ids): + # Build placeholders for exercise IDs + placeholders = ", ".join(["%s"] * len(selected_exercise_ids)) + + # Dynamically inject placeholders into the query + sql_query = f""" + SELECT + p.person_id, + p.name AS person_name, + w.workout_id, + w.start_date, + w.note AS workout_note, + e.exercise_id, + e.name AS exercise_name, + t.topset_id, + t.repetitions, + t.weight + FROM + person p + JOIN + workout w ON p.person_id = w.person_id + JOIN + topset t ON w.workout_id = t.workout_id + JOIN + exercise e ON t.exercise_id = e.exercise_id + WHERE + p.person_id = %s + AND w.start_date BETWEEN %s AND %s + AND e.exercise_id IN ({placeholders}) + ORDER BY + w.start_date DESC, e.exercise_id ASC, t.topset_id ASC; + """ + + # Add parameters for the query + params = [person_id, start_date, end_date] + selected_exercise_ids + result = self.execute(sql_query, params) + + if not result: + return {"person_id": person_id, "person_name": None, "workouts": [], "selected_exercises": []} + + # Extract person info from the first row + person_info = {"person_id": result[0]["person_id"], "person_name": result[0]["person_name"]} + + # Extract and sort all unique exercises by name + exercises = [] + unique_exercise_ids = set() + for row in result: + if row["exercise_id"] not in unique_exercise_ids: + unique_exercise_ids.add(row["exercise_id"]) + exercises.append({"id": row["exercise_id"], "name": row["exercise_name"]}) + + # Sort the exercises by name + exercises = sorted(exercises, key=lambda ex: ex["name"]) + + # Initialize the table structure + workouts = [] + workout_map = {} # Map to track workouts + + for row in result: + workout_id = row["workout_id"] + + # Initialize the workout if not already present + if workout_id not in workout_map: + workout_map[workout_id] = { + "id": workout_id, + "start_date": row["start_date"], + "note": row["workout_note"], + "exercises": {exercise["id"]: [] for exercise in exercises} # Keyed by exercise_id + } + + # Add topset to the corresponding exercise + if row["exercise_id"] and row["topset_id"]: + workout_map[workout_id]["exercises"][row["exercise_id"]].append({ + "repetitions": row["repetitions"], + "weight": row["weight"] + }) + + # Transform into a list of rows + for workout_id, workout in workout_map.items(): + workouts.append(workout) + + return {**person_info, "workouts": workouts, "selected_exercises": exercises} + + + + + + + diff --git a/templates/calendar.html b/templates/calendar.html index 150ee33..ddfc221 100644 --- a/templates/calendar.html +++ b/templates/calendar.html @@ -2,10 +2,6 @@ {% block content %} - -
diff --git a/templates/notes.html b/templates/notes.html index 6b3de64..ebe7b0c 100644 --- a/templates/notes.html +++ b/templates/notes.html @@ -2,9 +2,6 @@ {% block content %} - -
@@ -47,8 +44,8 @@ {% for workout_note in workout_notes %} + hx-get="{{ url_for('show_workout', person_id=person_id, workout_id=workout_note.workout_id) }}" + hx-push-url="true" hx-target="#container">
-
- -
- -
- - - - - - - - - - {% for topset in top_sets %} - {{ render_partial('partials/topset.html', person_id=person_id, - workout_id=workout_id, - topset_id=topset.topset_id, exercise_id=topset.exercise_id, exercise_name=topset.exercise_name, - repetitions=topset.repetitions, - weight=topset.weight) }} - {% endfor %} - -
- Exercise - Top Set -
- - {% if top_sets|length == 0 %} - - {% endif %} -
- -
- {{ render_partial('partials/new_set_form.html', person_id=person_id, - workout_id=workout_id, - exercises=exercises, - has_value=False) }} -
-
- -
-
-
-
- -
\ No newline at end of file diff --git a/templates/person.html b/templates/person.html index 140c55c..1124367 100644 --- a/templates/person.html +++ b/templates/person.html @@ -10,10 +10,6 @@ {% endfor %}
- -
diff --git a/templates/person_overview.html b/templates/person_overview.html new file mode 100644 index 0000000..af1d9fd --- /dev/null +++ b/templates/person_overview.html @@ -0,0 +1,168 @@ +{% extends 'base.html' %} + +{% block content %} + +
+
+ +
+
+

{{ person_name }}

+ List of workouts +
+
+
+ +
+
+
+ +
+
+
+ + +
+
+
+ +
+
+ +
+ +
+
+
+ +
+
+ +
+ +
+
+
+ + {{ render_partial('partials/tags.html',person_id=person_id, tags=tags) }} + +
+
+
+
+ + {% if workouts|length > 0 %} + + + + + {% for exercise in selected_exercises %} + + {% endfor %} + + + + + {% for workout in workouts %} + + + + {% for exercise in selected_exercises %} + + {% endfor %} + + {% endfor %} + + +
+ Date + + {{ exercise.name }} +
+ {{ workout.start_date | strftime("%b %d %Y") }} + + {% for set in workout.exercises[exercise.id] %} + {{ set.repetitions }} x {{ set.weight }}kg + {% endfor %} +
+ {% endif %} + + {% if workouts|length == 0 %} + + {% endif %} + +
+
+
+
+ +
+
+ +{{ render_partial('partials/stats.html', stats=stats) }} + + + +{% endblock %} \ No newline at end of file