Skip to main content

Overview

FitAiid’s progress tracking system provides users with detailed analytics about their fitness journey, including workout history, streak tracking, achievement unlocking, and visual charts.

Features

Workout History

Complete history of all workouts with dates, duration, calories, and exercises completed

Streak Tracking

Track consecutive workout days and maintain motivation with streak counters

Achievement System

Unlock achievements based on milestones and consistency

Visual Charts

Interactive charts showing weekly, monthly, and distribution data

Statistics Dashboard

Main Metrics

The statistics page displays key performance indicators:
  • Total Workouts: All completed training sessions
  • Total Exercises: Sum of all exercises completed
  • Total Minutes: Cumulative training time
  • Current Streak: Consecutive days with workouts
  • Max Streak: Longest streak ever achieved
  • This Week: Workouts completed in the current week
  • This Month: Workouts completed in the current month

Frontend Implementation

Loading User Statistics

async function cargarDatosUsuario() {
  try {
    console.log('Loading statistics from backend...');

    // Load complete statistics from backend
    const statsResponse = await fetch(`${CONFIG.API_URL}/api/estadisticas/${userId}`, {
      headers: {
        'Authorization': `Bearer ${token}`
      }
    });
    const statsBackend = await statsResponse.json();

    if (statsBackend.success) {
      statsData = {
        workouts: statsBackend.data.workoutHistory || [],
        totalExercises: statsBackend.data.totalExercises || 0,
        totalMinutes: statsBackend.data.totalMinutes || 0,
        currentStreak: statsBackend.data.currentStreak || 0,
        maxStreak: statsBackend.data.maxStreak || 0,
        achievements: statsBackend.data.achievements || [],
        thisWeekWorkouts: statsBackend.data.thisWeekWorkouts || 0,
        thisMonthWorkouts: statsBackend.data.thisMonthWorkouts || 0
      };

      console.log('Statistics loaded:', statsData);
    }

    // Update UI
    actualizarEstadisticas();
    cargarLogros();
    await generarGraficos();
    mostrarActividadReciente();

  } catch (error) {
    console.error('Error loading data:', error);
    cargarDatosLocales(); // Fallback to localStorage
  }
}

Updating UI Metrics

function actualizarEstadisticas() {
  // Total workouts completed
  document.getElementById('totalWorkouts').textContent = statsData.workouts.length || 0;

  // Weekly progress
  document.getElementById('weeklyProgress').textContent =
    `This week: ${statsData.thisWeekWorkouts || 0}`;

  // Current streak
  document.getElementById('currentStreak').textContent = statsData.currentStreak || 0;

  // Total time (convert minutes to hours)
  const totalHours = ((statsData.totalMinutes || 0) / 60).toFixed(1);
  document.getElementById('totalTime').textContent = totalHours;

  // Total exercises
  document.getElementById('totalExercises').textContent = statsData.totalExercises || 0;

  // Update progress bars
  actualizarBarrasProgreso();
}

Backend Implementation

Statistics Controller

exports.getEstadisticas = async (req, res) => {
  const { userId } = req.params;

  // Security validation
  if (req.user._id.toString() !== userId) {
    throw new AppError('Not authorized to access this data', 403);
  }

  const user = await User.findById(userId);
  
  if (!user) {
    throw new AppError('User not found', 404);
  }

  const workoutHistory = user.fitnessStats?.workoutHistory || [];
  
  // Calculate this week's workouts
  const now = new Date();
  const oneWeekAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
  
  const thisWeekWorkouts = workoutHistory.filter(w => {
    const workoutDate = new Date(w.date);
    return workoutDate >= oneWeekAgo;
  }).length;

  // Calculate this month's workouts
  const oneMonthAgo = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000);
  
  const thisMonthWorkouts = workoutHistory.filter(w => {
    const workoutDate = new Date(w.date);
    return workoutDate >= oneMonthAgo;
  }).length;

  res.json({
    success: true,
    data: {
      totalWorkouts: user.fitnessStats?.totalWorkouts || 0,
      totalExercises: user.fitnessStats?.totalExercises || 0,
      totalMinutes: user.fitnessStats?.totalMinutes || 0,
      currentStreak: user.fitnessStats?.currentStreak || 0,
      maxStreak: user.fitnessStats?.maxStreak || 0,
      thisWeekWorkouts: thisWeekWorkouts,
      thisMonthWorkouts: thisMonthWorkouts,
      workoutHistory: workoutHistory.slice(-20), // Last 20 workouts
      achievements: user.fitnessStats?.achievements || []
    }
  });
};

Chart Data Endpoint

exports.getGraficos = async (req, res) => {
  const { userId } = req.params;
  const user = await User.findById(userId);
  const workouts = user.fitnessStats?.workoutHistory || [];

  // 1. WEEKLY CHART (by day of week)
  const diasSemana = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];
  const countsPorDia = [0, 0, 0, 0, 0, 0, 0];
  
  workouts.forEach(w => {
    const day = new Date(w.date).getDay();
    const adjustedDay = day === 0 ? 6 : day - 1;
    countsPorDia[adjustedDay]++;
  });

  // 2. MONTHLY CHART (last 4 weeks)
  const now = new Date();
  const semanasCounts = [0, 0, 0, 0];
  
  workouts.forEach(w => {
    const workoutDate = new Date(w.date);
    const weeksDiff = Math.floor((now - workoutDate) / (1000 * 60 * 60 * 24 * 7));
    
    if (weeksDiff >= 0 && weeksDiff < 4) {
      semanasCounts[3 - weeksDiff]++;
    }
  });

  // 3. FOCUS DISTRIBUTION
  const enfoques = {};
  workouts.forEach(w => {
    const enfoque = w.enfoque || 'No category';
    enfoques[enfoque] = (enfoques[enfoque] || 0) + 1;
  });

  // 4. TIME DISTRIBUTION
  const horarios = {
    'Morning': 0,
    'Noon': 0,
    'Afternoon': 0,
    'Night': 0
  };
  
  workouts.forEach(w => {
    const hour = new Date(w.date).getHours();
    
    if (hour >= 5 && hour < 12) horarios['Morning']++;
    else if (hour >= 12 && hour < 17) horarios['Noon']++;
    else if (hour >= 17 && hour < 21) horarios['Afternoon']++;
    else horarios['Night']++;
  });

  res.json({
    success: true,
    data: {
      weekly: {
        labels: diasSemana,
        data: countsPorDia
      },
      monthly: {
        labels: ['Week 1', 'Week 2', 'Week 3', 'Week 4'],
        data: semanasCounts
      },
      focus: {
        labels: Object.keys(enfoques),
        data: Object.values(enfoques)
      },
      time: {
        labels: Object.keys(horarios),
        data: Object.values(horarios)
      }
    }
  });
};

Streak System

How Streaks Work

1

First Workout

When a user completes their first workout of the day, streak is set to 1
2

Same Day Check

If user completes another workout the same day, streak remains unchanged
3

Next Day

If user works out the next day, streak increases by 1
4

Missed Day

If user skips a day, streak resets to 0 (handled by notification scheduler)

Streak Calculation Logic

// Calculate improved streak (counts completed workouts, not consecutive days)
const totalExercises = workoutData.ejercicios ? workoutData.ejercicios.length : 0;
const today = new Date();
today.setHours(0, 0, 0, 0);

const workoutHistory = user.fitnessStats?.workoutHistory || [];

// Check if user already trained today
const yaEntrenoHoy = workoutHistory.some(w => {
  const workoutDate = new Date(w.date);
  workoutDate.setHours(0, 0, 0, 0);
  return workoutDate.getTime() === today.getTime();
});

let newStreak = user.fitnessStats?.currentStreak || 0;

if (yaEntrenoHoy) {
  // Already completed a workout today, maintain streak
  newStreak = user.fitnessStats?.currentStreak || 1;
} else {
  // New training day, increase streak
  newStreak = (user.fitnessStats?.currentStreak || 0) + 1;
}

// Update max streak
const newMaxStreak = Math.max(newStreak, user.fitnessStats?.maxStreak || 0);
The streak system counts training days, not consecutive calendar days. Multiple workouts on the same day don’t increase the streak.

Achievement System

Available Achievements

Unlock Condition: Complete 1 workoutDescription: You completed your first training session!
{
  id: 'first_workout',
  nombre: 'First Routine',
  descripcion: 'You completed your first session',
  icon: '🎯',
  condition: totalWorkouts >= 1
}
Unlock Condition: Maintain a 7-day streakDescription: One week without stopping!
{
  id: 'week_streak',
  nombre: '7-Day Streak',
  descripcion: 'One week without stopping',
  icon: '🔥',
  condition: currentStreak >= 7
}
Unlock Condition: Complete 10 workoutsDescription: 10 routines completed
{
  id: 'ten_workouts',
  nombre: 'Dedication',
  descripcion: '10 routines completed',
  icon: '💪',
  condition: totalWorkouts >= 10
}
Unlock Condition: Complete 50 workoutsDescription: 50 routines completed
{
  id: 'fifty_workouts',
  nombre: 'Warrior',
  descripcion: '50 routines completed',
  icon: '👑',
  condition: totalWorkouts >= 50
}

Achievement Verification

async function verificarLogros(userId, totalWorkouts, currentStreak) {
  const user = await User.findById(userId);
    
  const logrosDisponibles = [
    {
      id: 'first_workout',
      nombre: 'First Routine',
      descripcion: 'You completed your first session',
      icon: '🎯',
      condition: totalWorkouts >= 1
    },
    {
      id: 'week_streak',
      nombre: '7-Day Streak',
      descripcion: 'One week without stopping',
      icon: '🔥',
      condition: currentStreak >= 7
    },
    {
      id: 'ten_workouts',
      nombre: 'Dedication',
      descripcion: '10 routines completed',
      icon: '💪',
      condition: totalWorkouts >= 10
    },
    {
      id: 'fifty_workouts',
      nombre: 'Warrior',
      descripcion: '50 routines completed',
      icon: '👑',
      condition: totalWorkouts >= 50
    }
  ];

  const achievementsDesbloqueados = [];
  const achievementsActuales = user.fitnessStats?.achievements || [];

  for (const logro of logrosDisponibles) {
    if (logro.condition) {
      const yaDesbloqueado = achievementsActuales.find(
        a => a.achievementId === logro.id
      );

      if (!yaDesbloqueado) {
        await User.findByIdAndUpdate(userId, {
          $push: {
            'fitnessStats.achievements': {
              achievementId: logro.id,
              unlockedAt: new Date()
            }
          }
        });

        achievementsDesbloqueados.push({
          id: logro.id,
          nombre: logro.nombre,
          descripcion: logro.descripcion,
          icon: logro.icon
        });

        console.log(`🏆 Achievement unlocked: ${logro.nombre}`);
      }
    }
  }

  return achievementsDesbloqueados;
}

Visual Charts

Chart Integration with Chart.js

The frontend uses Chart.js to display interactive charts:
async function generarGraficos() {
  try {
    const response = await fetch(`${CONFIG.API_URL}/api/estadisticas/graficos/${userId}`, {
      headers: {
        'Authorization': `Bearer ${token}`
      }
    });
    const data = await response.json();

    if (data.success) {
      generarGraficoSemanal(data.data.weekly);
      generarGraficoMensual(data.data.monthly);
      generarGraficoEnfoques(data.data.focus);
      generarGraficoHorarios(data.data.time);
    }
  } catch (error) {
    console.error('Error loading chart data:', error);
  }
}

function generarGraficoSemanal(dataGrafico) {
  const ctx = document.getElementById('weeklyChart').getContext('2d');

  new Chart(ctx, {
    type: 'bar',
    data: {
      labels: dataGrafico?.labels || ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'],
      datasets: [{
        label: 'Workouts',
        data: dataGrafico?.data || [0, 0, 0, 0, 0, 0, 0],
        backgroundColor: 'rgba(255, 61, 0, 0.8)',
        borderColor: 'rgba(255, 61, 0, 1)',
        borderWidth: 2,
        borderRadius: 8
      }]
    },
    options: {
      responsive: true,
      maintainAspectRatio: true,
      scales: {
        y: {
          beginAtZero: true,
          ticks: { stepSize: 1 }
        }
      }
    }
  });
}

Chart Types

Weekly Bar Chart

Shows workouts completed each day of the week

Monthly Line Chart

Displays workout trend over the last 4 weeks

Focus Doughnut Chart

Distribution of workout types (upper body, lower body, full body)

Time Polar Chart

Shows preferred workout times (morning, noon, afternoon, night)

API Endpoints

Get User Statistics

GET /api/estadisticas/{userId}
Authorization: Bearer {token}
Response:
{
  "success": true,
  "data": {
    "totalWorkouts": 25,
    "totalExercises": 200,
    "totalMinutes": 1125,
    "currentStreak": 5,
    "maxStreak": 12,
    "thisWeekWorkouts": 3,
    "thisMonthWorkouts": 12,
    "workoutHistory": [...],
    "achievements": [
      {
        "achievementId": "first_workout",
        "unlockedAt": "2026-01-15T10:30:00.000Z"
      }
    ]
  }
}

Get Chart Data

GET /api/estadisticas/graficos/{userId}
Authorization: Bearer {token}
Response:
{
  "success": true,
  "data": {
    "weekly": {
      "labels": ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"],
      "data": [2, 1, 3, 2, 1, 0, 1]
    },
    "monthly": {
      "labels": ["Week 1", "Week 2", "Week 3", "Week 4"],
      "data": [3, 4, 2, 3]
    },
    "focus": {
      "labels": ["Upper body", "Lower body", "Full body"],
      "data": [8, 7, 10]
    },
    "time": {
      "labels": ["Morning", "Noon", "Afternoon", "Night"],
      "data": [5, 3, 12, 5]
    }
  }
}
All statistics are calculated in real-time from the workout history stored in MongoDB, ensuring accurate and up-to-date data.

User Data Structure

fitnessStats: {
  totalWorkouts: {
    type: Number,
    default: 0
  },
  totalExercises: {
    type: Number,
    default: 0
  },
  totalMinutes: {
    type: Number,
    default: 0
  },
  currentStreak: {
    type: Number,
    default: 0
  },
  maxStreak: {
    type: Number,
    default: 0
  },
  workoutHistory: [
    {
      date: Date,
      nombre: String,
      enfoque: String,
      duracionTotal: Number,
      caloriasEstimadas: Number,
      ejerciciosCompletados: Number,
      ejercicios: [Object]
    }
  ],
  achievements: [
    {
      achievementId: String,
      unlockedAt: Date
    }
  ]
}

Build docs developers (and LLMs) love