Compare commits
3 Commits
a59cef5c95
...
f53bf3d106
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f53bf3d106 | ||
|
|
2b330e4743 | ||
|
|
bc2a350e90 |
@@ -36,25 +36,44 @@ def _fetch_workout_data(db_executor, person_id, start_date, end_date, include_de
|
||||
"""Fetches workout data for a person within a date range."""
|
||||
if include_details:
|
||||
query = """
|
||||
SELECT
|
||||
w.workout_id,
|
||||
w.start_date,
|
||||
t.topset_id,
|
||||
t.repetitions,
|
||||
t.weight,
|
||||
e.name AS exercise_name,
|
||||
p.name AS person_name
|
||||
FROM
|
||||
person p
|
||||
LEFT JOIN workout w ON p.person_id = w.person_id AND w.start_date BETWEEN %s AND %s
|
||||
LEFT JOIN topset t ON w.workout_id = t.workout_id
|
||||
LEFT JOIN exercise e ON t.exercise_id = e.exercise_id
|
||||
WHERE
|
||||
p.person_id = %s
|
||||
ORDER BY
|
||||
w.start_date,
|
||||
t.topset_id;
|
||||
WITH workout_stats AS (
|
||||
SELECT
|
||||
w.workout_id,
|
||||
w.start_date,
|
||||
t.topset_id,
|
||||
t.repetitions,
|
||||
t.weight,
|
||||
e.name AS exercise_name,
|
||||
p.name AS person_name,
|
||||
-- Max weight ever for this exercise before this set
|
||||
MAX(t.weight) OVER (
|
||||
PARTITION BY p.person_id, e.exercise_id
|
||||
ORDER BY w.start_date, t.topset_id
|
||||
ROWS BETWEEN UNBOUNDED PRECEDING AND 1 PRECEDING
|
||||
) as prev_max_weight,
|
||||
-- Weight from the last time this exercise was performed
|
||||
LAG(t.weight) OVER (
|
||||
PARTITION BY p.person_id, e.exercise_id
|
||||
ORDER BY w.start_date, t.topset_id
|
||||
) as prev_session_weight,
|
||||
-- Reps from the last time this exercise was performed
|
||||
LAG(t.repetitions) OVER (
|
||||
PARTITION BY p.person_id, e.exercise_id
|
||||
ORDER BY w.start_date, t.topset_id
|
||||
) as prev_session_reps
|
||||
FROM
|
||||
person p
|
||||
LEFT JOIN workout w ON p.person_id = w.person_id
|
||||
LEFT JOIN topset t ON w.workout_id = t.workout_id
|
||||
LEFT JOIN exercise e ON t.exercise_id = e.exercise_id
|
||||
WHERE
|
||||
p.person_id = %s
|
||||
)
|
||||
SELECT * FROM workout_stats
|
||||
WHERE start_date BETWEEN %s AND %s
|
||||
ORDER BY start_date, topset_id;
|
||||
"""
|
||||
return db_executor(query, [person_id, start_date.strftime('%Y-%m-%d'), end_date.strftime('%Y-%m-%d')])
|
||||
else:
|
||||
query = """
|
||||
SELECT
|
||||
@@ -69,8 +88,7 @@ def _fetch_workout_data(db_executor, person_id, start_date, end_date, include_de
|
||||
ORDER BY
|
||||
w.start_date;
|
||||
"""
|
||||
# Ensure dates are passed in a format the DB understands (e.g., YYYY-MM-DD strings)
|
||||
return db_executor(query, [start_date.strftime('%Y-%m-%d'), end_date.strftime('%Y-%m-%d'), person_id])
|
||||
return db_executor(query, [start_date.strftime('%Y-%m-%d'), end_date.strftime('%Y-%m-%d'), person_id])
|
||||
|
||||
def _group_workouts_by_date(workouts_data):
|
||||
"""Groups workout data by date and workout ID."""
|
||||
@@ -97,10 +115,21 @@ def _group_workouts_by_date(workouts_data):
|
||||
|
||||
# Add set details if topset_id exists
|
||||
if row.get('topset_id'):
|
||||
weight = row.get('weight') or 0
|
||||
reps = row.get('repetitions') or 0
|
||||
prev_max = row.get('prev_max_weight') or 0
|
||||
prev_weight = row.get('prev_session_weight') or 0
|
||||
prev_reps = row.get('prev_session_reps') or 0
|
||||
|
||||
is_pr = weight > prev_max and prev_max > 0
|
||||
is_improvement = (weight > prev_weight) or (weight == prev_weight and reps > prev_reps) if prev_weight > 0 else False
|
||||
|
||||
workouts_by_date[workout_date][workout_id]['sets'].append({
|
||||
'repetitions': row.get('repetitions'),
|
||||
'weight': row.get('weight'),
|
||||
'exercise_name': row.get('exercise_name')
|
||||
'repetitions': reps,
|
||||
'weight': weight,
|
||||
'exercise_name': row.get('exercise_name'),
|
||||
'is_pr': is_pr,
|
||||
'is_improvement': is_improvement
|
||||
})
|
||||
|
||||
# Convert nested defaultdict to regular dict
|
||||
@@ -119,12 +148,38 @@ def _process_workouts_for_month_view(grouped_workouts, start_date, end_date, sel
|
||||
day_workouts_dict = grouped_workouts.get(current_date, {})
|
||||
day_workouts_list = list(day_workouts_dict.values()) # Convert workout dicts to list
|
||||
|
||||
total_sets = 0
|
||||
has_pr = False
|
||||
has_improvement = False
|
||||
pr_count = 0
|
||||
improvement_count = 0
|
||||
unique_exercise_names = []
|
||||
for workout in day_workouts_list:
|
||||
total_sets += len(workout.get('sets', []))
|
||||
for s in workout.get('sets', []):
|
||||
if s.get('is_pr'):
|
||||
has_pr = True
|
||||
pr_count += 1
|
||||
if s.get('is_improvement'):
|
||||
has_improvement = True
|
||||
improvement_count += 1
|
||||
name = s.get('exercise_name')
|
||||
if name and name not in unique_exercise_names:
|
||||
unique_exercise_names.append(name)
|
||||
|
||||
days_data.append({
|
||||
'date_obj': current_date, # Pass the date object for easier template logic
|
||||
'day': current_date.day,
|
||||
'is_today': current_date == today, # Correct comparison: date object == date object
|
||||
'is_in_current_month': current_date.month == selected_date.month,
|
||||
'has_workouts': len(day_workouts_list) > 0,
|
||||
'workout_count': len(day_workouts_list),
|
||||
'total_sets': total_sets,
|
||||
'has_pr': has_pr,
|
||||
'has_improvement': has_improvement,
|
||||
'pr_count': pr_count,
|
||||
'improvement_count': improvement_count,
|
||||
'exercise_names': unique_exercise_names[:3], # Limit to first 3 for summary
|
||||
'workouts': day_workouts_list
|
||||
})
|
||||
current_date += timedelta(days=1)
|
||||
@@ -212,6 +267,25 @@ def get_calendar(person_id):
|
||||
# Add view-specific data
|
||||
if selected_view == 'month':
|
||||
calendar_view_data['days'] = _process_workouts_for_month_view(grouped_workouts, start_date, end_date, selected_date)
|
||||
|
||||
# Calculate summary stats for the selected month
|
||||
total_workouts = 0
|
||||
total_sets = 0
|
||||
unique_exercises = set()
|
||||
for workout_date, workouts in grouped_workouts.items():
|
||||
if workout_date.month == selected_date.month and workout_date.year == selected_date.year:
|
||||
total_workouts += len(workouts)
|
||||
for workout in workouts.values():
|
||||
total_sets += len(workout.get('sets', []))
|
||||
for topset in workout.get('sets', []):
|
||||
if topset.get('exercise_name'):
|
||||
unique_exercises.add(topset.get('exercise_name'))
|
||||
|
||||
calendar_view_data['summary_stats'] = {
|
||||
'total_workouts': total_workouts,
|
||||
'total_sets': total_sets,
|
||||
'total_exercises': len(unique_exercises)
|
||||
}
|
||||
elif selected_view == 'year':
|
||||
calendar_view_data['months'] = _process_workouts_for_year_view(grouped_workouts, selected_date)
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -39,7 +39,32 @@
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="mr-4">
|
||||
<div class="flex items-center space-x-2 mr-4">
|
||||
{% if view == 'month' %}
|
||||
<div
|
||||
class="hidden lg:flex items-center space-x-3 text-xs font-medium text-gray-500 border-l border-gray-200 pl-4 h-6">
|
||||
<div class="flex items-center">
|
||||
<span class="text-blue-600 mr-1">{{ summary_stats.total_workouts }}</span>
|
||||
<span class="uppercase tracking-wider">Workouts</span>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<span class="text-blue-600 mr-1">{{ summary_stats.total_sets }}</span>
|
||||
<span class="uppercase tracking-wider">Sets</span>
|
||||
</div>
|
||||
{% if summary_stats.total_workouts > 0 %}
|
||||
<div class="flex items-center">
|
||||
<span class="text-blue-600 mr-1">{{ (summary_stats.total_sets / summary_stats.total_workouts) |
|
||||
round(1) }}</span>
|
||||
<span class="uppercase tracking-wider">Sets/Session</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="flex items-center">
|
||||
<span class="text-blue-600 mr-1">{{ summary_stats.total_exercises }}</span>
|
||||
<span class="uppercase tracking-wider">Exercises</span>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{{ render_partial('partials/custom_select.html',
|
||||
name='view',
|
||||
options=[
|
||||
@@ -60,62 +85,102 @@
|
||||
</div>
|
||||
|
||||
{% if view == 'month' %}
|
||||
<div class="flex flex-col px-2 py-2 -mb-px">
|
||||
<div class="grid grid-cols-7 pl-2 pr-2">
|
||||
<div class="flex flex-col px-1 sm:px-2 py-2 -mb-px">
|
||||
<div class="grid grid-cols-7">
|
||||
|
||||
<div class="p-2 h-10 text-center font-bold">
|
||||
<div class="p-1 h-8 text-center font-bold text-xs">
|
||||
<span class="xl:block lg:block md:block sm:block hidden">Sunday</span>
|
||||
<span class="xl:hidden lg:hidden md:hidden sm:hidden block">Sun</span>
|
||||
<span class="xl:hidden lg:hidden md:hidden sm:hidden block">S</span>
|
||||
</div>
|
||||
<div class="p-2 h-10 text-center font-bold">
|
||||
<div class="p-1 h-8 text-center font-bold text-xs">
|
||||
<span class="xl:block lg:block md:block sm:block hidden">Monday</span>
|
||||
<span class="xl:hidden lg:hidden md:hidden sm:hidden block">Mon</span>
|
||||
<span class="xl:hidden lg:hidden md:hidden sm:hidden block">M</span>
|
||||
</div>
|
||||
<div class="p-2 h-10 text-center font-bold">
|
||||
<div class="p-1 h-8 text-center font-bold text-xs">
|
||||
<span class="xl:block lg:block md:block sm:block hidden">Tuesday</span>
|
||||
<span class="xl:hidden lg:hidden md:hidden sm:hidden block">Tue</span>
|
||||
<span class="xl:hidden lg:hidden md:hidden sm:hidden block">T</span>
|
||||
</div>
|
||||
<div class="p-2 h-10 text-center font-bold">
|
||||
<div class="p-1 h-8 text-center font-bold text-xs">
|
||||
<span class="xl:block lg:block md:block sm:block hidden">Wednesday</span>
|
||||
<span class="xl:hidden lg:hidden md:hidden sm:hidden block">Wed</span>
|
||||
<span class="xl:hidden lg:hidden md:hidden sm:hidden block">W</span>
|
||||
</div>
|
||||
<div class="p-2 h-10 text-center font-bold">
|
||||
<div class="p-1 h-8 text-center font-bold text-xs">
|
||||
<span class="xl:block lg:block md:block sm:block hidden">Thursday</span>
|
||||
<span class="xl:hidden lg:hidden md:hidden sm:hidden block">Thu</span>
|
||||
<span class="xl:hidden lg:hidden md:hidden sm:hidden block">T</span>
|
||||
</div>
|
||||
<div class="p-2 h-10 text-center font-bold">
|
||||
<div class="p-1 h-8 text-center font-bold text-xs">
|
||||
<span class="xl:block lg:block md:block sm:block hidden">Friday</span>
|
||||
<span class="xl:hidden lg:hidden md:hidden sm:hidden block">Fri</span>
|
||||
<span class="xl:hidden lg:hidden md:hidden sm:hidden block">F</span>
|
||||
</div>
|
||||
<div class="p-2 h-10 text-center font-bold">
|
||||
<div class="p-1 h-8 text-center font-bold text-xs">
|
||||
<span class="xl:block lg:block md:block sm:block hidden">Saturday</span>
|
||||
<span class="xl:hidden lg:hidden md:hidden sm:hidden block">Sat</span>
|
||||
<span class="xl:hidden lg:hidden md:hidden sm:hidden block">S</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-7 overflow-hidden flex-1 pl-2 pr-2 w-full">
|
||||
<div class="grid grid-cols-7 overflow-hidden flex-1 w-full border-t border-l">
|
||||
|
||||
{% for day in days %}
|
||||
<div
|
||||
class="{% if day.is_today %}rounded-md border-4 border-green-50{% endif %} border flex flex-col h-36 sm:h-40 md:h-30 lg:h-30 mx-auto mx-auto overflow-hidden w-full pt-2 pl-1 cursor-pointer {% if day.is_in_current_month %}bg-gray-100{% endif %}">
|
||||
<div class="top h-5 w-full">
|
||||
<span class="text-gray-500 font-semibold">{{ day.day }}</span>
|
||||
class="{% if day.is_today %}ring-2 ring-green-100 ring-inset{% endif %} border-b border-r flex flex-col h-20 sm:h-40 md:h-30 lg:h-30 mx-auto overflow-hidden w-full pt-1 px-1 cursor-pointer relative {% if not day.is_in_current_month %}opacity-40{% else %}bg-gray-50/50{% endif %}">
|
||||
|
||||
<div class="flex justify-between items-start mb-0.5">
|
||||
<span class="text-gray-400 font-medium text-[9px] sm:text-xs leading-none">{{ day.day }}</span>
|
||||
|
||||
{% if day.has_workouts and (day.pr_count > 0 or day.improvement_count > 0) %}
|
||||
<div
|
||||
class="flex items-center bg-white/80 border border-gray-100 rounded-full px-1 shadow-sm h-3.5 sm:h-4">
|
||||
{% if day.pr_count > 0 %}
|
||||
<span class="text-[8px] sm:text-[9px] font-bold text-yellow-600 flex items-center">
|
||||
🏆<span class="ml-0.5">{{ day.pr_count }}</span>
|
||||
</span>
|
||||
{% endif %}
|
||||
{% if day.pr_count > 0 and day.improvement_count > 0 %}
|
||||
<span class="mx-0.5 text-gray-300 text-[8px]">|</span>
|
||||
{% endif %}
|
||||
{% if day.improvement_count > 0 %}
|
||||
<span class="text-[8px] sm:text-[9px] font-bold text-green-600 flex items-center">
|
||||
↑<span class="ml-0.5">{{ day.improvement_count }}</span>
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% for workout in day.workouts %}
|
||||
<div class="bottom flex-grow py-1 w-full"
|
||||
hx-get="{{ url_for('workout.show_workout', person_id=person_id, workout_id=workout.workout_id) }}"
|
||||
hx-push-url="true" hx-target="#container">
|
||||
{% for set in workout.sets %}
|
||||
<button
|
||||
class="flex flex-col xl:flex-row items-start lg:items-center flex-shrink-0 px-0 sm:px-0.5 md:px-0.5 lg:px-0.5 text-xs">
|
||||
<span class="ml-0 sm:ml-0.5 md:ml-2 lg:ml-2 font-medium leading-none truncate">{{
|
||||
set.exercise_name }}</span>
|
||||
<span class="ml-0 sm:ml-0.5 md:ml-2 lg:ml-2 font-light leading-none">{{ set.repetitions }} x {{
|
||||
set.weight }}kg</span>
|
||||
</button>
|
||||
|
||||
{% if day.has_workouts %}
|
||||
<!-- Mobile Summary -->
|
||||
<div
|
||||
class="sm:hidden flex flex-col flex-grow text-[8px] text-gray-500 font-medium leading-tight overflow-hidden pb-1 space-y-0.5">
|
||||
{% for name in day.exercise_names %}
|
||||
<div class="truncate pl-0.5 border-l border-blue-200">{{ name }}</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
<!-- Desktop Detailed List -->
|
||||
<div class="hidden sm:block flex-1 overflow-hidden">
|
||||
{% for workout in day.workouts %}
|
||||
<div class="py-1 w-full"
|
||||
hx-get="{{ url_for('workout.show_workout', person_id=person_id, workout_id=workout.workout_id) }}"
|
||||
hx-push-url="true" hx-target="#container">
|
||||
{% for set in workout.sets %}
|
||||
<div class="flex flex-col w-full px-0.5 text-[9px] lg:text-[10px] leading-tight mb-1">
|
||||
<span class="truncate flex items-center min-w-0">
|
||||
<span class="truncate">{{ set.exercise_name }}</span>
|
||||
</span>
|
||||
<span class="font-light text-gray-400 text-[8px] lg:text-[9px] flex items-center">
|
||||
<span>{{ set.repetitions }} x {{ set.weight }}kg</span>
|
||||
{% if set.is_pr %}
|
||||
<span class="ml-1 text-yellow-500 shrink-0 text-[8px]">🏆</span>
|
||||
{% elif set.is_improvement %}
|
||||
<span class="ml-1 text-green-500 font-bold shrink-0 text-[8px]">↑</span>
|
||||
{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user