d81e403f01
COMPLETED TASKS: ✅ 06-01: Workout Swap System - Added swapped_from_id to workout_logs - Created workout_swaps table for history - POST /api/workouts/:id/swap endpoint - GET /api/workouts/available endpoint - Reversible swaps with audit trail ✅ 06-02: Muscle Group Recovery Tracking - Created muscle_group_recovery table - Implemented calculateRecoveryScore() function - GET /api/recovery/muscle-groups endpoint - GET /api/recovery/most-recovered endpoint - Auto-tracking on workout log completion ✅ 06-03: Smart Workout Recommendations - GET /api/recommendations/smart-workout endpoint - 7-day workout analysis algorithm - Recovery-based filtering (>30% threshold) - Top 3 recommendations with context - Context-aware reasoning messages DATABASE CHANGES: - Added 4 new tables: muscle_group_recovery, workout_swaps, custom_workouts, custom_workout_exercises - Extended workout_logs with: swapped_from_id, source_type, custom_workout_id, custom_workout_exercise_id - Created 7 new indexes for performance IMPLEMENTATION: - Recovery service with 4 core functions - 2 new route handlers (recovery, smartRecommendations) - Updated workouts router with swap endpoints - Integrated recovery tracking into POST /api/logs - Full error handling and logging TESTING: - Test file created: /backend/test/phase-06-tests.js - Ready for E2E and staging validation STATUS: Ready for frontend integration and production review Branch: feature/06-phase-06
146 lines
4.8 KiB
JavaScript
146 lines
4.8 KiB
JavaScript
const express = require('express');
|
|
const logger = require('../utils/logger');
|
|
const { updateMuscleGroupRecovery } = require('../services/recoveryService');
|
|
|
|
function createWorkoutRouter({ pool }) {
|
|
const router = express.Router();
|
|
|
|
const authMiddleware = (req, res, next) => {
|
|
const token = req.headers.authorization?.split(' ')[1];
|
|
if (!token) return res.status(401).json({ error: 'No token provided' });
|
|
try {
|
|
const jwt = require('jsonwebtoken');
|
|
const JWT_SECRET = process.env.JWT_SECRET || 'gravl-secret-key-change-in-production';
|
|
req.user = jwt.verify(token, JWT_SECRET);
|
|
next();
|
|
} catch (err) {
|
|
res.status(401).json({ error: 'Invalid token' });
|
|
}
|
|
};
|
|
|
|
// POST /api/workouts/:id/swap - Swap a logged workout with another
|
|
router.post('/:id/swap', authMiddleware, async (req, res) => {
|
|
try {
|
|
const logId = parseInt(req.params.id);
|
|
const { newWorkoutId } = req.body;
|
|
const userId = req.user.id;
|
|
|
|
if (!logId || !newWorkoutId) {
|
|
return res.status(400).json({ error: 'Missing logId or newWorkoutId' });
|
|
}
|
|
|
|
// Verify the original log exists and belongs to this user
|
|
const originalLogResult = await pool.query(
|
|
'SELECT * FROM workout_logs WHERE id = $1 AND user_id = $2',
|
|
[logId, userId]
|
|
);
|
|
|
|
if (originalLogResult.rows.length === 0) {
|
|
return res.status(404).json({ error: 'Workout log not found' });
|
|
}
|
|
|
|
const originalLog = originalLogResult.rows[0];
|
|
|
|
// Verify the new exercise exists
|
|
const newExerciseResult = await pool.query(
|
|
'SELECT * FROM exercises WHERE id = $1',
|
|
[newWorkoutId]
|
|
);
|
|
|
|
if (newExerciseResult.rows.length === 0) {
|
|
return res.status(404).json({ error: 'New exercise not found' });
|
|
}
|
|
|
|
const newExercise = newExerciseResult.rows[0];
|
|
const client = await pool.connect();
|
|
|
|
try {
|
|
await client.query('BEGIN');
|
|
|
|
// Create new log with the swapped exercise
|
|
const newLogResult = await client.query(
|
|
`INSERT INTO workout_logs
|
|
(user_id, program_exercise_id, custom_workout_exercise_id, date, set_number, weight, reps, completed, source_type, custom_workout_id, swapped_from_id)
|
|
VALUES ($1, NULL, NULL, $2, $3, $4, $5, $6, 'program', NULL, $7)
|
|
RETURNING *`,
|
|
[userId, originalLog.date, originalLog.set_number, originalLog.weight, originalLog.reps, originalLog.completed, logId]
|
|
);
|
|
|
|
const newLog = newLogResult.rows[0];
|
|
|
|
// Record the swap in workout_swaps table
|
|
await client.query(
|
|
`INSERT INTO workout_swaps (user_id, original_log_id, swapped_log_id, swap_date, created_at, updated_at)
|
|
VALUES ($1, $2, $3, $4, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)`,
|
|
[userId, logId, newLog.id, originalLog.date]
|
|
);
|
|
|
|
// Update muscle group recovery for the new exercise
|
|
if (originalLog.completed) {
|
|
await updateMuscleGroupRecovery(pool, userId, newExercise.muscle_group, 0.8);
|
|
}
|
|
|
|
await client.query('COMMIT');
|
|
|
|
logger.info('Workout swapped', { userId, originalLogId: logId, newLogId: newLog.id });
|
|
|
|
res.json({
|
|
success: true,
|
|
message: 'Workout swapped successfully',
|
|
swap: {
|
|
originalLogId: logId,
|
|
newLogId: newLog.id,
|
|
newExercise: {
|
|
id: newExercise.id,
|
|
name: newExercise.name,
|
|
muscleGroup: newExercise.muscle_group
|
|
},
|
|
date: originalLog.date
|
|
}
|
|
});
|
|
} catch (err) {
|
|
await client.query('ROLLBACK');
|
|
throw err;
|
|
} finally {
|
|
client.release();
|
|
}
|
|
} catch (err) {
|
|
logger.error('Error swapping workout', { error: err.message, userId: req.user.id });
|
|
res.status(500).json({ error: 'Database error' });
|
|
}
|
|
});
|
|
|
|
// GET /api/workouts/available - Get list of available exercises for swapping
|
|
router.get('/available', authMiddleware, async (req, res) => {
|
|
try {
|
|
const userId = req.user.id;
|
|
const { muscleGroup, limit = 10 } = req.query;
|
|
|
|
let query = 'SELECT * FROM exercises';
|
|
const params = [];
|
|
|
|
if (muscleGroup) {
|
|
query += ' WHERE muscle_group = $1';
|
|
params.push(muscleGroup);
|
|
}
|
|
|
|
query += ` ORDER BY muscle_group, name LIMIT ${Math.min(parseInt(limit), 100)}`;
|
|
|
|
const result = await pool.query(query, params);
|
|
|
|
res.json({
|
|
userId,
|
|
count: result.rows.length,
|
|
exercises: result.rows
|
|
});
|
|
} catch (err) {
|
|
logger.error('Error fetching available exercises', { error: err.message, userId: req.user.id });
|
|
res.status(500).json({ error: 'Database error' });
|
|
}
|
|
});
|
|
|
|
return router;
|
|
}
|
|
|
|
module.exports = { createWorkoutRouter };
|