Update programs functionality

This commit is contained in:
Peter Stockings
2026-02-03 15:10:59 +11:00
parent b26ae1e319
commit ac093ec2e0
7 changed files with 849 additions and 210 deletions

334
templates/program_edit.html Normal file
View File

@@ -0,0 +1,334 @@
{% extends "base.html" %}
{% block title %}Edit {{ program.name }}{% endblock %}
{% block content %}
<div class="container mx-auto px-4 py-8 max-w-3xl">
<h1 class="text-3xl font-bold mb-8 text-center text-gray-800">Edit Workout Program</h1>
<form method="POST" action="{{ url_for('programs.edit_program', program_id=program.program_id) }}"
id="edit-program-form" class="bg-white shadow-md rounded-lg px-8 pt-6 pb-8 mb-4">
{# Program Details Section #}
<div class="mb-6 border-b border-gray-200 pb-4">
<h2 class="text-xl font-semibold mb-4 text-gray-700">Program Details</h2>
<div class="mb-4">
<label for="program_name" class="block text-gray-700 text-sm font-bold mb-2">Program Name:</label>
<input type="text" id="program_name" name="program_name" required value="{{ program.name }}"
class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline">
</div>
<div>
<label for="description" class="block text-gray-700 text-sm font-bold mb-2">Description
(Optional):</label>
<textarea id="description" name="description" rows="3"
class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline">{{ program.description or '' }}</textarea>
</div>
</div>
{# Sessions Section #}
<div class="mb-6">
<h2 class="text-xl font-semibold mb-4 text-gray-700">Sessions</h2>
<div id="sessions-container" class="space-y-6 mb-4">
{% for session in sessions %}
{% set session_index = loop.index0 %}
<div class="session-row bg-gray-50 border border-gray-300 rounded-lg shadow-sm overflow-hidden"
data-index="{{ session_index }}">
{# Session Header #}
<div class="px-4 py-3 bg-gray-100 border-b border-gray-300 flex justify-between items-center">
<h3 class="session-day-number text-lg font-semibold text-gray-700">Day {{ session.session_order
}}</h3>
<input type="hidden" name="session_order_{{ session_index }}"
value="{{ session.session_order }}">
<button type="button" class="remove-session-btn text-red-500 hover:text-red-700"
title="Remove Session">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24"
stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round"
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
</svg>
</button>
</div>
{# Session Body #}
<div class="p-4 space-y-4">
<div>
<label for="session_name_{{ session_index }}"
class="block text-sm font-medium text-gray-700 mb-1">Session Name (Optional):</label>
<input type="text" id="session_name_{{ session_index }}"
name="session_name_{{ session_index }}" value="{{ session.session_name or '' }}"
class="shadow-sm focus:ring-indigo-500 focus:border-indigo-500 block w-full sm:text-sm border-gray-300 rounded-md">
</div>
{# Container for individual exercise selects #}
<div class="space-y-3">
<label class="block text-sm font-medium text-gray-700 mb-1">Exercises:</label>
<div
class="session-exercises-container space-y-2 border border-gray-200 p-3 rounded-md bg-white">
{% for exercise in session.exercises %}
<div
class="exercise-row flex items-center space-x-4 bg-white p-2 rounded border border-gray-100 shadow-sm">
<div class="flex-grow relative">
{{ render_partial('partials/custom_select.html',
name='exercises_' ~ session_index,
options=exercises,
multiple=false,
search=true,
selected_values=[exercise.exercise_id],
placeholder='Select Exercise...')
}}
</div>
<div class="w-16">
<input type="number" name="sets_{{ session_index }}" placeholder="Sets"
class="block w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"
min="1" value="{{ exercise.sets or 3 }}">
</div>
<div class="w-24">
<input type="text" name="reps_{{ session_index }}"
placeholder="Reps (e.g. 8-10)"
class="block w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"
value="{{ exercise.rep_range or '8-10' }}">
</div>
<button type="button"
class="remove-exercise-btn text-red-500 hover:text-red-700 flex-shrink-0"
title="Remove Exercise">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20"
fill="currentColor">
<path fill-rule="evenodd"
d="M10 18a8 8 0 100-16 8 8 0 000 16zM7 9a1 1 0 000 2h6a1 1 0 100-2H7z"
clip-rule="evenodd" />
</svg>
</button>
</div>
{% endfor %}
</div>
<button type="button" data-session-index="{{ session_index }}"
class="add-exercise-btn mt-1 inline-flex items-center px-3 py-1 border border-transparent text-xs font-medium rounded shadow-sm text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
Add Exercise to Session
</button>
</div>
</div>
</div>
{% endfor %}
</div>
<button type="button" id="add-session-btn"
class="mt-2 inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-green-600 hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd"
d="M10 5a1 1 0 011 1v3h3a1 1 0 110 2h-3v3a1 1 0 11-2 0v-3H6a1 1 0 110-2h3V6a1 1 0 011-1z"
clip-rule="evenodd" />
</svg>
Add Session
</button>
</div>
{# Form Actions #}
<div class="flex items-center justify-between pt-4 border-t border-gray-200">
<a href="{{ url_for('programs.view_program', program_id=program.program_id) }}"
hx-get="{{ url_for('programs.view_program', program_id=program.program_id) }}" hx-target="#container"
hx-push-url="true" class="text-gray-600 hover:text-gray-900 font-medium">Cancel</a>
<button type="submit"
class="bg-indigo-600 hover:bg-indigo-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline">
Save Changes
</button>
</div>
</form>
{# HTML Template for a single session row #}
<template id="session-row-template">
<div class="session-row bg-gray-50 border border-gray-300 rounded-lg shadow-sm overflow-hidden"
data-index="SESSION_INDEX_PLACEHOLDER">
{# Session Header #}
<div class="px-4 py-3 bg-gray-100 border-b border-gray-300 flex justify-between items-center">
<h3 class="session-day-number text-lg font-semibold text-gray-700">Day SESSION_DAY_NUMBER_PLACEHOLDER
</h3>
<input type="hidden" name="session_order_SESSION_INDEX_PLACEHOLDER"
value="SESSION_DAY_NUMBER_PLACEHOLDER">
<button type="button" class="remove-session-btn text-red-500 hover:text-red-700" title="Remove Session">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24"
stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round"
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
</svg>
</button>
</div>
{# Session Body #}
<div class="p-4 space-y-4">
<div>
<label for="session_name_SESSION_INDEX_PLACEHOLDER"
class="block text-sm font-medium text-gray-700 mb-1">Session Name (Optional):</label>
<input type="text" id="session_name_SESSION_INDEX_PLACEHOLDER"
name="session_name_SESSION_INDEX_PLACEHOLDER" value=""
class="shadow-sm focus:ring-indigo-500 focus:border-indigo-500 block w-full sm:text-sm border-gray-300 rounded-md">
</div>
{# Container for individual exercise selects #}
<div class="space-y-3">
<label class="block text-sm font-medium text-gray-700 mb-1">Exercises:</label>
<div class="session-exercises-container space-y-2 border border-gray-200 p-3 rounded-md bg-white">
{# Exercise rows will be added here by JS #}
</div>
<button type="button"
class="add-exercise-btn mt-1 inline-flex items-center px-3 py-1 border border-transparent text-xs font-medium rounded shadow-sm text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
Add Exercise to Session
</button>
</div>
</div>
</div>
</template>
{# Nested Template for a single exercise row within a session #}
<template id="exercise-row-template">
<div class="exercise-row flex items-center space-x-4 bg-white p-2 rounded border border-gray-100 shadow-sm">
<div class="flex-grow relative">
{{ render_partial('partials/custom_select.html',
name='exercises_SESSION_INDEX_PLACEHOLDER',
options=exercises,
multiple=false,
search=true,
placeholder='Select Exercise...')
}}
</div>
<div class="w-16">
<input type="number" name="sets_SESSION_INDEX_PLACEHOLDER" placeholder="Sets"
class="block w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"
min="1" value="3">
</div>
<div class="w-24">
<input type="text" name="reps_SESSION_INDEX_PLACEHOLDER" placeholder="Reps (e.g. 8-10)"
class="block w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"
value="8-10">
</div>
<button type="button" class="remove-exercise-btn text-red-500 hover:text-red-700 flex-shrink-0"
title="Remove Exercise">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM7 9a1 1 0 000 2h6a1 1 0 100-2H7z"
clip-rule="evenodd" />
</svg>
</button>
</div>
</template>
</div>
<script>
document.addEventListener('DOMContentLoaded', function () {
const sessionsContainer = document.getElementById('sessions-container');
const addSessionBtn = document.getElementById('add-session-btn');
const sessionTemplate = document.getElementById('session-row-template');
const exerciseTemplate = document.getElementById('exercise-row-template');
let sessionCounter = sessionsContainer.querySelectorAll('.session-row').length;
// --- Function to add a new session row ---
function addSessionRow() {
const newRowFragment = sessionTemplate.content.cloneNode(true);
const newRow = newRowFragment.querySelector('.session-row');
const currentSessionIndex = sessionCounter;
if (!newRow) return;
newRow.dataset.index = currentSessionIndex;
const dayNumberSpan = newRow.querySelector('.session-day-number');
if (dayNumberSpan) dayNumberSpan.textContent = `Day ${currentSessionIndex + 1}`;
const orderInput = newRow.querySelector('input[type="hidden"]');
if (orderInput) {
orderInput.name = `session_order_${currentSessionIndex}`;
orderInput.value = currentSessionIndex + 1;
}
const nameInput = newRow.querySelector('input[id^="session_name_"]');
if (nameInput) {
nameInput.id = `session_name_${currentSessionIndex}`;
nameInput.name = `session_name_${currentSessionIndex}`;
}
const addExerciseBtn = newRow.querySelector('.add-exercise-btn');
const exercisesContainer = newRow.querySelector('.session-exercises-container');
if (addExerciseBtn && exercisesContainer) {
addExerciseBtn.dataset.sessionIndex = currentSessionIndex;
addExerciseBtn.addEventListener('click', handleAddExerciseClick);
addExerciseSelect(exercisesContainer, currentSessionIndex);
}
sessionsContainer.appendChild(newRowFragment);
attachRemoveListener(newRow.querySelector('.remove-session-btn'));
sessionCounter++;
}
function addExerciseSelect(container, sessionIndex) {
const newExFragment = exerciseTemplate.content.cloneNode(true);
const nativeSelect = newExFragment.querySelector('.native-select');
const setsInput = newExFragment.querySelector('input[name^="sets_"]');
const repsInput = newExFragment.querySelector('input[name^="reps_"]');
const removeBtn = newExFragment.querySelector('.remove-exercise-btn');
if (nativeSelect) nativeSelect.name = `exercises_${sessionIndex}`;
if (setsInput) setsInput.name = `sets_${sessionIndex}`;
if (repsInput) repsInput.name = `reps_${sessionIndex}`;
container.appendChild(newExFragment);
attachExerciseRemoveListener(removeBtn);
}
function handleAddExerciseClick(event) {
const btn = event.currentTarget;
const sessionIndex = parseInt(btn.dataset.sessionIndex, 10);
const exercisesContainer = btn.closest('.session-row').querySelector('.session-exercises-container');
if (!isNaN(sessionIndex) && exercisesContainer) {
addExerciseSelect(exercisesContainer, sessionIndex);
}
}
function attachRemoveListener(button) {
button.addEventListener('click', function () {
this.closest('.session-row').remove();
updateSessionNumbers();
});
}
function attachExerciseRemoveListener(button) {
if (button) {
button.addEventListener('click', function () {
this.closest('.exercise-row').remove();
});
}
}
function updateSessionNumbers() {
const rows = sessionsContainer.querySelectorAll('.session-row');
rows.forEach((row, index) => {
const newIndex = index;
const daySpan = row.querySelector('.session-day-number');
if (daySpan) daySpan.textContent = `Day ${newIndex + 1}`;
const orderInput = row.querySelector('input[type="hidden"]');
if (orderInput) {
orderInput.name = `session_order_${newIndex}`;
orderInput.value = newIndex + 1;
}
const nameInput = row.querySelector('input[id^="session_name_"]');
if (nameInput) {
nameInput.id = `session_name_${newIndex}`;
nameInput.name = `session_name_${newIndex}`;
}
row.querySelectorAll('.native-select').forEach(s => s.name = `exercises_${newIndex}`);
row.querySelectorAll('input[name^="sets_"]').forEach(i => i.name = `sets_${newIndex}`);
row.querySelectorAll('input[name^="reps_"]').forEach(i => i.name = `reps_${newIndex}`);
const addExerciseBtn = row.querySelector('.add-exercise-btn');
if (addExerciseBtn) addExerciseBtn.dataset.sessionIndex = newIndex;
row.dataset.index = newIndex;
});
sessionCounter = rows.length;
}
addSessionBtn.addEventListener('click', addSessionRow);
sessionsContainer.querySelectorAll('.session-row .remove-session-btn').forEach(attachRemoveListener);
sessionsContainer.querySelectorAll('.exercise-row .remove-exercise-btn').forEach(attachExerciseRemoveListener);
sessionsContainer.querySelectorAll('.session-row .add-exercise-btn').forEach(btn => {
btn.addEventListener('click', handleAddExerciseClick);
});
});
</script>
{% endblock %}